diff --git a/tests/test_ikea.py b/tests/test_ikea.py index fa4470090e..54a393c8f5 100644 --- a/tests/test_ikea.py +++ b/tests/test_ikea.py @@ -10,6 +10,7 @@ from tests.common import ClusterListener import zhaquirks import zhaquirks.ikea.starkvind +from zhaquirks.ikea.starkvind import IkeaAirpurifier zhaquirks.setup() @@ -85,6 +86,33 @@ def test_ikea_starkvind_v2(assert_signature_matches_quirk): assert_signature_matches_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND_v2, signature) +@pytest.mark.parametrize("attribute", ["fan_speed", "fan_mode"]) +@pytest.mark.parametrize("value,expected", [ + (0, 0), # off + (1, 1), # auto + (10, 2), + (20, 4), + (50, 10), + ] + ) +async def test_fan_speed_mode_update(zigpy_device_from_quirk, attribute, value, expected): + """Test reading the fan speed and mode.""" + + starkvind_device = zigpy_device_from_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND) + assert starkvind_device.model == "STARKVIND Air purifier" + + ikea_cluster = starkvind_device.endpoints[1].in_clusters[ + zhaquirks.ikea.starkvind.IkeaAirpurifier.cluster_id + ] + ikea_listener = ClusterListener(ikea_cluster) + + attr_id = getattr(IkeaAirpurifier.AttributeDefs, attribute).id + + ikea_cluster.update_attribute(attr_id, value) + assert len(ikea_listener.attribute_updates) == 1 + assert ikea_listener.attribute_updates[0] == (attr_id, expected) + + async def test_pm25_cluster_read(zigpy_device_from_quirk): """Test reading from PM25 cluster.""" @@ -131,14 +159,14 @@ def mock_read(attributes, manufacturer=None): @pytest.mark.parametrize( "firmware, pct_device, pct_correct, expected_pct_updates, expect_log_warning", ( - ("1.0.024", 50, 100, 1, False), # old firmware, doubling - ("2.3.075", 50, 100, 1, False), # old firmware, doubling - ("2.4.5", 50, 50, 2, False), # new firmware, no doubling - ("3.0.0", 50, 50, 2, False), # new firmware, no doubling - ("24.4.5", 50, 50, 2, False), # new firmware, no doubling - ("invalid_fw_string_1", 50, 50, 2, False), # treated as new, no doubling - ("invalid.fw.string.2", 50, 50, 2, True), # treated as new, no doubling + log - ("", 50, 100, 1, False), # treated as old fw, doubling + ("1.0.024", 50, 100, 2, False), # old firmware, doubling + ("2.3.075", 50, 100, 2, False), # old firmware, doubling + ("2.4.5", 50, 50, 1, False), # new firmware, no doubling + ("3.0.0", 50, 50, 1, False), # new firmware, no doubling + ("24.4.5", 50, 50, 1, False), # new firmware, no doubling + ("invalid_fw_string_1", 50, 50, 1, False), # treated as new, no doubling + ("invalid.fw.string.2", 50, 50, 1, True), # treated as new, no doubling + log + ("", 50, 50, 1, False), # treated as new fw, no doubling ), ) async def test_double_power_config_firmware( @@ -178,10 +206,10 @@ def mock_read(attributes, manufacturer=None): ) with p1 as mock_task, p2 as request_mock: - # update battery percentage with no firmware in attr cache, check pct doubled for now + # update battery percentage with no firmware in attr cache, check pct not doubled for now power_cluster.update_attribute(battery_pct_id, pct_device) assert len(power_listener.attribute_updates) == 1 - assert power_listener.attribute_updates[0] == (battery_pct_id, pct_device * 2) + assert power_listener.attribute_updates[0] == (battery_pct_id, pct_device) # but also check that sw_build_id read is requested in the background for next update assert mock_task.call_count == 1 diff --git a/tests/test_quirks.py b/tests/test_quirks.py index f66e47089c..09ab4f5c68 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -641,12 +641,6 @@ def test_migrated_lighting_automation_triggers(quirk: CustomDevice) -> None: (zhaquirks.aurora.aurora_dimmer.COLOR_DOWN, const.LEFT), ], ], - zhaquirks.ikea.fourbtnremote.IkeaTradfriRemoteV1: [ - [ - (const.LONG_RELEASE, const.DIM_UP), - (const.LONG_RELEASE, const.DIM_DOWN), - ] - ], zhaquirks.paulmann.fourbtnremote.PaulmannRemote4Btn: [ [ (const.LONG_RELEASE, const.BUTTON_1), @@ -666,6 +660,7 @@ def test_migrated_lighting_automation_triggers(quirk: CustomDevice) -> None: } +# XXX: Test does not handle v2 quirks @pytest.mark.parametrize( "quirk", [q for q in ALL_QUIRK_CLASSES if getattr(q, "device_automation_triggers", None)], diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 3836c79c47..f534e0932a 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -1135,12 +1135,13 @@ async def test_xiaomi_e1_thermostat_schedule_settings_deserialization( ( (zhaquirks.xiaomi.aqara.motion_ac02.LumiMotionAC02, 0), (zhaquirks.xiaomi.aqara.motion_agl02.MotionT1, -1), + (zhaquirks.xiaomi.aqara.motion_acn001.MotionE1, -1), ), ) async def test_xiaomi_p1_t1_motion_sensor( zigpy_device_from_quirk, quirk, invalid_iilluminance_report ): - """Test Aqara P1 and T1 motion sensors.""" + """Test Aqara P1, T1, and E1 motion sensors.""" device = zigpy_device_from_quirk(quirk) @@ -1192,12 +1193,12 @@ async def test_xiaomi_p1_t1_motion_sensor( opple_cluster.update_attribute(274, 0xFFFF) # confirm invalid illuminance report is interpreted as 0 for P1 sensor, - # and -1 for the T1 sensor, as it doesn't seem like the T1 sensor sends invalid illuminance reports + # and -1 for the T1/E1 sensors, as they don't seem to send invalid illuminance reports assert len(illuminance_listener.attribute_updates) == 2 assert illuminance_listener.attribute_updates[1][0] == zcl_iilluminance_id assert illuminance_listener.attribute_updates[1][1] == invalid_iilluminance_report - # send illuminance report only + # send illuminance report only, parsed via Xiaomi cluster implementation opple_cluster.update_attribute( XIAOMI_AQARA_ATTRIBUTE_E1, create_aqara_attr_report({101: 20}) ) diff --git a/zhaquirks/ikea/__init__.py b/zhaquirks/ikea/__init__.py index 65e2151da9..d584269ccf 100644 --- a/zhaquirks/ikea/__init__.py +++ b/zhaquirks/ikea/__init__.py @@ -215,9 +215,9 @@ def _is_firmware_new(self): # get sw_build_id from attribute cache if available sw_build_id = self.endpoint.basic.get(Basic.AttributeDefs.sw_build_id.id) - # sw_build_id is not cached or empty, so we consider it old firmware for now + # sw_build_id is not cached or empty, so we consider it new firmware for now if not sw_build_id: - return False + return True # split sw_build_id into parts to check for new firmware split_fw_version = sw_build_id.split(".") @@ -247,19 +247,19 @@ async def _read_fw_and_update_battery_pct(self, reported_battery_pct): # read sw_build_id from device await self.endpoint.basic.read_attributes([Basic.AttributeDefs.sw_build_id.id]) - # check if sw_build_id was read successfully and new firmware is installed - # if so, update cache with reported battery percentage (non-doubled) - if self._is_firmware_new(): + # check if sw_build_id was read successfully and old firmware is installed + # if so, update cache with reported battery percentage (doubled) + if not self._is_firmware_new(): self._update_attribute( PowerConfiguration.AttributeDefs.battery_percentage_remaining.id, - reported_battery_pct, + reported_battery_pct * 2, ) def _update_attribute(self, attrid, value): - """Update attribute to double battery percentage if firmware is old/unknown. + """Update attribute to double battery percentage if firmware is old. If the firmware version is unknown, a background task to read the firmware version is also started, - but the percentage is also doubled for now then, as that task happens asynchronously. + but the percentage is not doubled for now then, as that task happens asynchronously. """ if attrid == PowerConfiguration.AttributeDefs.battery_percentage_remaining.id: # if sw_build_id is not cached, create task to read from device, since it should be awake now @@ -269,9 +269,9 @@ def _update_attribute(self, attrid, value): ): self.create_catching_task(self._read_fw_and_update_battery_pct(value)) - # double percentage if the firmware is old or unknown - # the coroutine above will not have executed yet if the firmware is unknown, - # so we double for now in that case too, and it updates again later if our doubling was wrong + # double percentage if the firmware is confirmed old + # The coroutine above will not have executed yet if the firmware is unknown, + # so we don't double for now. The coro doubles the value later if needed. if not self._is_firmware_new(): value = value * 2 super()._update_attribute(attrid, value) diff --git a/zhaquirks/ikea/fourbtnremote.py b/zhaquirks/ikea/fourbtnremote.py index ee37f4b9b9..afe88f8012 100644 --- a/zhaquirks/ikea/fourbtnremote.py +++ b/zhaquirks/ikea/fourbtnremote.py @@ -1,16 +1,6 @@ """Device handler for IKEA of Sweden TRADFRI remote control.""" -from zigpy.profiles import zha -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import ( - Basic, - Identify, - LevelControl, - OnOff, - Ota, - PollControl, - PowerConfiguration, -) -from zigpy.zcl.clusters.lightlink import LightLink +from zigpy.quirks.v2 import add_to_registry_v2 +from zigpy.zcl import ClusterType from zhaquirks.const import ( CLUSTER_ID, @@ -23,214 +13,96 @@ COMMAND_PRESS, COMMAND_STOP, COMMAND_STOP_ON_OFF, - DEVICE_TYPE, DIM_DOWN, DIM_UP, ENDPOINT_ID, - ENDPOINTS, - INPUT_CLUSTERS, LEFT, LONG_PRESS, LONG_RELEASE, - MODELS_INFO, - OUTPUT_CLUSTERS, PARAMS, - PROFILE_ID, RIGHT, SHORT_PRESS, TURN_OFF, TURN_ON, ) -from zhaquirks.ikea import ( - IKEA, - IKEA_CLUSTER_ID, - WWAH_CLUSTER_ID, - DoublingPowerConfig2AAACluster, - ScenesCluster, -) - - -class IkeaTradfriRemoteV1(CustomDevice): - """Custom device representing IKEA of Sweden TRADFRI remote control V1.0.024.""" - - signature = { - # - MODELS_INFO: [(IKEA, "Remote Control N2")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - WWAH_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - LightLink.cluster_id, - ], - } - }, - } +from zhaquirks.ikea import IKEA, DoublingPowerConfig2AAACluster, ScenesCluster - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DoublingPowerConfig2AAACluster, - Identify.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - WWAH_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - ScenesCluster, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - LightLink.cluster_id, - ], - } - } - } - - device_automation_triggers = { - (SHORT_PRESS, TURN_ON): {COMMAND: COMMAND_ON, CLUSTER_ID: 6, ENDPOINT_ID: 1}, - (LONG_PRESS, DIM_UP): { - COMMAND: COMMAND_MOVE_ON_OFF, - CLUSTER_ID: 8, - ENDPOINT_ID: 1, - PARAMS: {"move_mode": 0}, - }, - (LONG_RELEASE, DIM_UP): { - COMMAND: COMMAND_STOP_ON_OFF, - CLUSTER_ID: 8, - ENDPOINT_ID: 1, - }, - (SHORT_PRESS, TURN_OFF): {COMMAND: COMMAND_OFF, CLUSTER_ID: 6, ENDPOINT_ID: 1}, - (LONG_PRESS, DIM_DOWN): { - COMMAND: COMMAND_MOVE, - CLUSTER_ID: 8, - ENDPOINT_ID: 1, - PARAMS: {"move_mode": 1}, - }, - (LONG_RELEASE, DIM_DOWN): { - COMMAND: COMMAND_STOP, - CLUSTER_ID: 8, - ENDPOINT_ID: 1, - }, - (SHORT_PRESS, LEFT): { - COMMAND: COMMAND_PRESS, - CLUSTER_ID: 5, - ENDPOINT_ID: 1, - PARAMS: { - "param1": 257, - "param2": 13, - "param3": 0, +( + add_to_registry_v2(IKEA, "Remote Control N2") + .replaces(DoublingPowerConfig2AAACluster) # will only double for old firmware + .replaces(ScenesCluster, cluster_type=ClusterType.Client) + .device_automation_triggers( + { + (SHORT_PRESS, TURN_ON): { + COMMAND: COMMAND_ON, + CLUSTER_ID: 6, + ENDPOINT_ID: 1, }, - }, - (LONG_PRESS, LEFT): { - COMMAND: COMMAND_HOLD, - CLUSTER_ID: 5, - ENDPOINT_ID: 1, - PARAMS: { - "param1": 3329, - "param2": 0, + (LONG_PRESS, DIM_UP): { + COMMAND: COMMAND_MOVE_ON_OFF, + CLUSTER_ID: 8, + ENDPOINT_ID: 1, + PARAMS: {"move_mode": 0}, }, - }, - (SHORT_PRESS, RIGHT): { - COMMAND: COMMAND_PRESS, - CLUSTER_ID: 5, - ENDPOINT_ID: 1, - PARAMS: { - "param1": 256, - "param2": 13, - "param3": 0, + (LONG_RELEASE, DIM_UP): { + COMMAND: COMMAND_STOP_ON_OFF, + CLUSTER_ID: 8, + ENDPOINT_ID: 1, }, - }, - (LONG_PRESS, RIGHT): { - COMMAND: COMMAND_HOLD, - CLUSTER_ID: 5, - ENDPOINT_ID: 1, - PARAMS: { - "param1": 3328, - "param2": 0, + (SHORT_PRESS, TURN_OFF): { + COMMAND: COMMAND_OFF, + CLUSTER_ID: 6, + ENDPOINT_ID: 1, + }, + (LONG_PRESS, DIM_DOWN): { + COMMAND: COMMAND_MOVE, + CLUSTER_ID: 8, + ENDPOINT_ID: 1, + PARAMS: {"move_mode": 1}, + }, + (LONG_RELEASE, DIM_DOWN): { + COMMAND: COMMAND_STOP, + CLUSTER_ID: 8, + ENDPOINT_ID: 1, + }, + (SHORT_PRESS, LEFT): { + COMMAND: COMMAND_PRESS, + CLUSTER_ID: 5, + ENDPOINT_ID: 1, + PARAMS: { + "param1": 257, + "param2": 13, + "param3": 0, + }, + }, + (LONG_PRESS, LEFT): { + COMMAND: COMMAND_HOLD, + CLUSTER_ID: 5, + ENDPOINT_ID: 1, + PARAMS: { + "param1": 3329, + "param2": 0, + }, + }, + (SHORT_PRESS, RIGHT): { + COMMAND: COMMAND_PRESS, + CLUSTER_ID: 5, + ENDPOINT_ID: 1, + PARAMS: { + "param1": 256, + "param2": 13, + "param3": 0, + }, + }, + (LONG_PRESS, RIGHT): { + COMMAND: COMMAND_HOLD, + CLUSTER_ID: 5, + ENDPOINT_ID: 1, + PARAMS: { + "param1": 3328, + "param2": 0, + }, }, - }, - } - - -class IkeaTradfriRemoteV2(CustomDevice): - """Custom device representing IKEA of Sweden TRADFRI remote control Version 2.4.5.""" - - signature = { - # - MODELS_INFO: [(IKEA, "Remote Control N2")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - WWAH_CLUSTER_ID, - IKEA_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - ScenesCluster.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - LightLink.cluster_id, - ], - } - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.NON_COLOR_CONTROLLER, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - WWAH_CLUSTER_ID, - IKEA_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - ScenesCluster, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - LightLink.cluster_id, - ], - } } - } - - device_automation_triggers = IkeaTradfriRemoteV1.device_automation_triggers.copy() + ) +) diff --git a/zhaquirks/ikea/plug.py b/zhaquirks/ikea/plug.py new file mode 100644 index 0000000000..cd3cd9dcb4 --- /dev/null +++ b/zhaquirks/ikea/plug.py @@ -0,0 +1,12 @@ +"""IKEA plugs quirk.""" +from zigpy.quirks.v2 import add_to_registry_v2 +from zigpy.zcl.clusters.general import LevelControl + +from zhaquirks.ikea import IKEA + +# remove LevelControl for plugs to not show config options in ZHA +( + add_to_registry_v2(IKEA, "TRADFRI control outlet") + .also_applies_to(IKEA, "TRETAKT Smart plug") + .removes(LevelControl.cluster_id) +) diff --git a/zhaquirks/ikea/starkvind.py b/zhaquirks/ikea/starkvind.py index 95b228db9d..11a1d1f631 100644 --- a/zhaquirks/ikea/starkvind.py +++ b/zhaquirks/ikea/starkvind.py @@ -68,9 +68,9 @@ def _update_attribute(self, attrid, value): value is not None and value < 5500 ): # > 5500 = out of scale; if value is 65535 (0xFFFF), device is off self.endpoint.device.pm25_bus.listener_event("update_state", value) - elif attrid == 0x0006: - if value > 9 and value < 51: - value = value / 5 + elif attrid in (0x0006, 0x0007): + if value >= 10 and value <= 50: + value = value // 5 super()._update_attribute(attrid, value) async def write_attributes( diff --git a/zhaquirks/ikea/tradfriplug.py b/zhaquirks/ikea/tradfriplug.py deleted file mode 100644 index 963bbff51a..0000000000 --- a/zhaquirks/ikea/tradfriplug.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Tradfri Plug Quirk.""" -from zigpy.profiles import zgp, zha, zll -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import ( - Basic, - GreenPowerProxy, - Groups, - Identify, - LevelControl, - OnOff, - Ota, - PollControl, - Scenes, -) -from zigpy.zcl.clusters.lightlink import LightLink - -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID - - -class TradfriPlug1(CustomDevice): - """Tradfri Plug.""" - - signature = { - MODELS_INFO: [(IKEA, "TRADFRI control outlet")], - ENDPOINTS: { - # - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - IKEA_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Scenes.cluster_id, - Ota.cluster_id, - PollControl.cluster_id, - ], - }, - # - 2: { - PROFILE_ID: zll.PROFILE_ID, - DEVICE_TYPE: zll.DeviceType.ON_OFF_PLUGIN_UNIT, - INPUT_CLUSTERS: [LightLink.cluster_id], - OUTPUT_CLUSTERS: [LightLink.cluster_id], - }, - # - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - IKEA_CLUSTER_ID, - ], - OUTPUT_CLUSTERS: [ - Scenes.cluster_id, - Ota.cluster_id, - PollControl.cluster_id, - ], - } - } - } - - -class TradfriPlug2(CustomDevice): - """Tradfri Plug.""" - - signature = { - MODELS_INFO: [(IKEA, "TRADFRI control outlet")], - ENDPOINTS: { - # - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Scenes.cluster_id, - Ota.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - ], - }, - # - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - LightLink.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Scenes.cluster_id, - Ota.cluster_id, - PollControl.cluster_id, - LightLink.cluster_id, - ], - }, - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - }, - }, - } diff --git a/zhaquirks/sinope/light.py b/zhaquirks/sinope/light.py index 72496350c4..381fedf59e 100644 --- a/zhaquirks/sinope/light.py +++ b/zhaquirks/sinope/light.py @@ -42,7 +42,7 @@ class SinopeTechnologiesManufacturerCluster(CustomCluster): """SinopeTechnologiesManufacturerCluster manufacturer cluster.""" class KeypadLock(t.enum8): - """keypad_lockout values.""" + """Keypad_lockout values.""" Unlocked = 0x00 Locked = 0x01 @@ -61,7 +61,7 @@ class DoubleFull(t.enum8): On = 0x01 class Action(t.enum8): - """action_report values.""" + """Action_report values.""" Single_on = 0x01 Single_release_on = 0x02 diff --git a/zhaquirks/sinope/sensor.py b/zhaquirks/sinope/sensor.py index 3fea113764..b28fccf9f9 100644 --- a/zhaquirks/sinope/sensor.py +++ b/zhaquirks/sinope/sensor.py @@ -52,7 +52,7 @@ class SinopeTechnologiesIasZoneCluster(CustomCluster, IasZone): """SinopeTechnologiesIasZoneCluster custom cluster.""" class LeakStatus(t.enum8): - """leak_status values.""" + """Leak_status values.""" Dry = 0x00 Leak = 0x01 diff --git a/zhaquirks/sinope/switch.py b/zhaquirks/sinope/switch.py index fa40d7303a..ee345b8c93 100644 --- a/zhaquirks/sinope/switch.py +++ b/zhaquirks/sinope/switch.py @@ -51,7 +51,7 @@ class SinopeManufacturerCluster(CustomCluster): """SinopeManufacturerCluster manufacturer cluster.""" class KeypadLock(t.enum8): - """keypad_lockout values.""" + """Keypad_lockout values.""" Unlocked = 0x00 Locked = 0x01 @@ -70,6 +70,7 @@ class AlarmAction(t.enum8): Notify = 0x01 Close = 0x02 Close_notify = 0x03 + No_flow = 0x04 class PowerSource(t.uint32_t): """Valve power source types.""" @@ -93,7 +94,7 @@ class AbnormalAction(t.bitmap16): Close_notify = 0x0003 class ColdStatus(t.enum8): - """cold_load_pickup_status values.""" + """Cold_load_pickup_status values.""" Active = 0x00 Off = 0x01 @@ -188,14 +189,14 @@ class CustomMeteringCluster(CustomCluster, Metering): """Custom Metering Cluster.""" class ValveStatus(t.bitmap8): - """valve_status.""" + """Valve_status.""" Off = 0x00 Off_armed = 0x01 On = 0x02 class UnitOfMeasure(t.enum8): - """unit_of_measure.""" + """Unit_of_measure.""" KWh = 0x00 Lh = 0x07 @@ -326,6 +327,67 @@ class SinopeTechnologiesLoadController(CustomDevice): } +class SinopeTechnologiesLoadController_V2(CustomDevice): + """SinopeTechnologiesLoadController version 2 custom device.""" + + signature = { + # + MODELS_INFO: [ + (SINOPE, "RM3250ZB"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.ON_OFF_OUTPUT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + Diagnostic.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Ota.cluster_id, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic.cluster_id, + CustomDeviceTemperatureCluster, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + Diagnostic.cluster_id, + SinopeManufacturerCluster, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Ota.cluster_id, + ], + } + } + } + + class SinopeTechnologiesValve(CustomDevice): """SinopeTechnologiesValve custom device.""" @@ -605,8 +667,8 @@ class SinopeTechnologiesCalypso(CustomDevice): } -class SinopeTechnologiesNewSwitch(CustomDevice): - """SinopeTechnologiesNewSwitch custom device.""" +class SinopeTechnologiesSwitch_V2(CustomDevice): + """SinopeTechnologiesSwitch version 2 custom device.""" signature = { # , *user_descriptor_available=False)", + # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=260, device_version=1, input_clusters=[0, 1, 3, 4, 6, 4096, 57345], output_clusters=[25, 10, 3, 4, 6, 8, 4096]) + MODELS_INFO: [ + ("_TZ3000_ja5osu5g", "TS004F"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + LightLink.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + Time.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + ], + }, + }, + } class TuyaSmartRemote004F(EnchantedDevice): """Tuya 4-button New version remote device.""" diff --git a/zhaquirks/tuya/ts011f_plug.py b/zhaquirks/tuya/ts011f_plug.py index a0734de5b4..3d80c13dee 100644 --- a/zhaquirks/tuya/ts011f_plug.py +++ b/zhaquirks/tuya/ts011f_plug.py @@ -1249,7 +1249,6 @@ class Plug_CB_Metering(EnchantedDevice): signature = { MODEL: "TS011F", - MODELS_INFO: [("_TZ3000_qeuvnohg", "TS011F")], ENDPOINTS: { # , complex_descriptor_available=0, + # user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, + # mac_capability_flags=, + # manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, + # maximum_outgoing_transfer_size=82, descriptor_capability_field=, + # *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, + # *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)", + # "endpoints": { + # "1": { "profile_id": 260, "device_type": "0x0051", "in_clusters": [ "0x0000", "0x0004","0x0005","0x0102","0xef00"], "out_clusters": ["0x000a","0x0019"] } + # }, + # "manufacturer": "_TZE200_2odrmqwq", + # "model": "TS0601", + # "class": "zigpy.device.Device" + # } + MODELS_INFO: [ + ("_TZE200_2odrmqwq", "TS0601"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufacturerWindowCover, + TuyaWindowCoverControl, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + } + } + class TuyaMoesCover0601_inv_position(TuyaWindowCover): """Tuya blind controller device.""" diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py index 7359a065d1..9d60d2ebb9 100644 --- a/zhaquirks/tuya/ts0601_dimmer.py +++ b/zhaquirks/tuya/ts0601_dimmer.py @@ -151,7 +151,9 @@ class TuyaSingleSwitchDimmerGP(TuyaDimmerSwitch): ("_TZE200_ip2akl4w", "TS0601"), ("_TZE200_vucankjx", "TS0601"), # Loratap ("_TZE200_y8yjulon", "TS0601"), - ("_TZE204_n9ctkb6j", "TS0601"), # For BSEED switch + ("_TZE204_n9ctkb6j", "TS0601"), # BSEED + ("_TZE204_vevc4c6g", "TS0601"), # BSEED + ("_TZE204_5cuocqty", "TS0601"), # Avattto ZDMS16-1 ], ENDPOINTS: { # + MODELS_INFO: [ + ("_TZE200_leaqthqq", "TS0601"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaOnOffManufCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaOnOffManufCluster, + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [], + }, + 5: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + TuyaOnOffNM, + ], + OUTPUT_CLUSTERS: [], + }, + } + } + + class TuyaSextupleSwitchTO(TuyaSwitch): """Tuya sextuple channel switch time on out cluster device.""" @@ -659,6 +735,7 @@ class TuyaSextupleSwitchTO(TuyaSwitch): # output_clusters=[0x000A,0x0019]> MODELS_INFO: [ ("_TZE200_9mahtqtg", "TS0601"), + ("_TZE200_wnp4d4va", "TS0601"), ], ENDPOINTS: { 1: { diff --git a/zhaquirks/tuya/ts130f.py b/zhaquirks/tuya/ts130f.py index ba97ce2469..c8668df72b 100644 --- a/zhaquirks/tuya/ts130f.py +++ b/zhaquirks/tuya/ts130f.py @@ -8,6 +8,7 @@ Basic, GreenPowerProxy, Groups, + Identify, OnOff, Ota, Scenes, @@ -268,6 +269,45 @@ class TuyaTS130FTI2(CustomDevice): }, } +class TuyaTS130FUL(CustomDevice): + """Tuya Cover variant from UseeLink.""" + + signature = { + # This signature was copied from TuyaTS130FTO and changed to fit the UseeLink device. + MODEL: "TS130F", + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + WindowCovering.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id], + }, + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaWithBacklightOnOffCluster, + TuyaCoveringCluster, + ], + OUTPUT_CLUSTERS: [Time.cluster_id], + }, + }, + } class TuyaTS130FTO(CustomDevice): """Tuya smart curtain roller shutter Time Out.""" diff --git a/zhaquirks/tuya/ty0201.py b/zhaquirks/tuya/ty0201.py new file mode 100644 index 0000000000..55b6ac198c --- /dev/null +++ b/zhaquirks/tuya/ty0201.py @@ -0,0 +1,62 @@ +"""Tuya TY0201 temperature and humidity sensor.""" + +from zigpy.profiles import zha +from zigpy.profiles.zha import DeviceType +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration +from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.tuya.air import TuyaAirQualityHumidity, TuyaAirQualityTemperature + + +class TuyaTempHumiditySensor(CustomDevice): + """Temu/Aliexpress temperature and humidity sensor.""" + + signature = { + # + MODELS_INFO: [("_TZ3000_bjawzodf", "TY0201"), + ("_TZ3000_zl1kmjqx", "TY0201")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + TemperatureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + TuyaAirQualityTemperature, + TuyaAirQualityHumidity, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index 91607cfa7b..251d13742b 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -24,6 +24,7 @@ from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.measurement import ( IlluminanceMeasurement, + OccupancySensing, PressureMeasurement, RelativeHumidity, TemperatureMeasurement, @@ -389,7 +390,7 @@ def _parse_aqara_attributes(self, value): attribute_names.update({11: ILLUMINANCE_MEASUREMENT}) elif self.endpoint.device.model == "lumi.curtain.acn002": attribute_names.update({101: BATTERY_PERCENTAGE_REMAINING_ATTRIBUTE}) - elif self.endpoint.device.model in ["lumi.motion.agl02", "lumi.motion.ac02"]: + elif self.endpoint.device.model in ["lumi.motion.agl02", "lumi.motion.ac02", "lumi.motion.acn001"]: attribute_names.update({101: ILLUMINANCE_MEASUREMENT}) if self.endpoint.device.model == "lumi.motion.ac02": attribute_names.update({105: DETECTION_INTERVAL}) @@ -464,6 +465,22 @@ class XiaomiAqaraE1Cluster(XiaomiCluster): ep_attribute = "opple_cluster" +class XiaomiMotionManufacturerCluster(XiaomiAqaraE1Cluster): + """Xiaomi manufacturer cluster to parse motion and illuminance reports.""" + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == 274: + value = value - 65536 + self.endpoint.illuminance.update_attribute( + IlluminanceMeasurement.AttributeDefs.measured_value.id, value + ) + self.endpoint.occupancy.update_attribute( + OccupancySensing.AttributeDefs.occupancy.id, + OccupancySensing.Occupancy.Occupied, + ) + + class BinaryOutputInterlock(CustomCluster, BinaryOutput): """Xiaomi binaryoutput cluster with added interlock attribute.""" diff --git a/zhaquirks/xiaomi/aqara/motion_ac02.py b/zhaquirks/xiaomi/aqara/motion_ac02.py index efb52175b6..c372691828 100644 --- a/zhaquirks/xiaomi/aqara/motion_ac02.py +++ b/zhaquirks/xiaomi/aqara/motion_ac02.py @@ -8,7 +8,6 @@ from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration -from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing from zhaquirks import Bus, LocalDataCluster from zhaquirks.const import ( @@ -23,7 +22,7 @@ LocalIlluminanceMeasurementCluster, MotionCluster, OccupancyCluster, - XiaomiAqaraE1Cluster, + XiaomiMotionManufacturerCluster, XiaomiPowerConfiguration, ) @@ -34,8 +33,12 @@ _LOGGER = logging.getLogger(__name__) -class OppleCluster(XiaomiAqaraE1Cluster): - """Opple cluster.""" +class OppleCluster(XiaomiMotionManufacturerCluster): + """Xiaomi manufacturer cluster. + + This uses the shared XiaomiMotionManufacturerCluster implementation + which parses motion and illuminance reports from Xiaomi devices. + """ attributes = { DETECTION_INTERVAL: ("detection_interval", types.uint8_t, True), @@ -43,18 +46,6 @@ class OppleCluster(XiaomiAqaraE1Cluster): TRIGGER_INDICATOR: ("trigger_indicator", types.uint8_t, True), } - def _update_attribute(self, attrid: int, value: Any) -> None: - super()._update_attribute(attrid, value) - if attrid == MOTION_ATTRIBUTE: - value = value - 65536 - self.endpoint.illuminance.update_attribute( - IlluminanceMeasurement.AttributeDefs.measured_value.id, value - ) - self.endpoint.occupancy.update_attribute( - OccupancySensing.AttributeDefs.occupancy.id, - OccupancySensing.Occupancy.Occupied, - ) - async def write_attributes( self, attributes: dict[str | int, Any], manufacturer: int | None = None ) -> list: diff --git a/zhaquirks/xiaomi/aqara/motion_acn001.py b/zhaquirks/xiaomi/aqara/motion_acn001.py new file mode 100644 index 0000000000..168372aa01 --- /dev/null +++ b/zhaquirks/xiaomi/aqara/motion_acn001.py @@ -0,0 +1,68 @@ +"""Xiaomi Aqara E1 motion sensor device.""" +from zigpy.profiles import zha +from zigpy.zcl.clusters.general import Identify, Ota + +from zhaquirks import Bus +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.xiaomi import ( + LUMI, + BasicCluster, + IlluminanceMeasurementCluster, + LocalOccupancyCluster, + MotionCluster, + XiaomiAqaraE1Cluster, + XiaomiCustomDevice, + XiaomiMotionManufacturerCluster, + XiaomiPowerConfiguration, +) + + +class MotionE1(XiaomiCustomDevice): + """Xiaomi motion sensor device lumi.motion.acn001.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.battery_size = 11 + self.motion_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + MODELS_INFO: [(LUMI, "lumi.motion.acn001")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + BasicCluster.cluster_id, + XiaomiPowerConfiguration.cluster_id, + Identify.cluster_id, + XiaomiAqaraE1Cluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + BasicCluster, + XiaomiPowerConfiguration, + Identify.cluster_id, + LocalOccupancyCluster, + MotionCluster, + IlluminanceMeasurementCluster, + XiaomiMotionManufacturerCluster, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } diff --git a/zhaquirks/xiaomi/aqara/motion_agl02.py b/zhaquirks/xiaomi/aqara/motion_agl02.py index e8c0fd1ac0..30223320e5 100644 --- a/zhaquirks/xiaomi/aqara/motion_agl02.py +++ b/zhaquirks/xiaomi/aqara/motion_agl02.py @@ -3,7 +3,7 @@ from zigpy.profiles import zha from zigpy.zcl.clusters.general import Identify, Ota -from zigpy.zcl.clusters.measurement import IlluminanceMeasurement, OccupancySensing +from zigpy.zcl.clusters.measurement import OccupancySensing from zhaquirks import Bus from zhaquirks.const import ( @@ -20,29 +20,11 @@ IlluminanceMeasurementCluster, LocalOccupancyCluster, MotionCluster, - XiaomiAqaraE1Cluster, XiaomiCustomDevice, + XiaomiMotionManufacturerCluster, XiaomiPowerConfiguration, ) -XIAOMI_CLUSTER_ID = 0xFCC0 - - -class XiaomiManufacturerCluster(XiaomiAqaraE1Cluster): - """Xiaomi manufacturer cluster.""" - - def _update_attribute(self, attrid, value): - super()._update_attribute(attrid, value) - if attrid == 274: - value = value - 65536 - self.endpoint.illuminance.update_attribute( - IlluminanceMeasurement.AttributeDefs.measured_value.id, value - ) - self.endpoint.occupancy.update_attribute( - OccupancySensing.AttributeDefs.occupancy.id, - OccupancySensing.Occupancy.Occupied, - ) - class MotionT1(XiaomiCustomDevice): """Xiaomi motion sensor device.""" @@ -73,6 +55,7 @@ def __init__(self, *args, **kwargs): } }, } + replacement = { ENDPOINTS: { 1: { @@ -83,7 +66,7 @@ def __init__(self, *args, **kwargs): LocalOccupancyCluster, MotionCluster, IlluminanceMeasurementCluster, - XiaomiManufacturerCluster, + XiaomiMotionManufacturerCluster, ], OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], } diff --git a/zhaquirks/xiaomi/aqara/remote_h1.py b/zhaquirks/xiaomi/aqara/remote_h1.py index d2500cc4a8..2223a3df98 100644 --- a/zhaquirks/xiaomi/aqara/remote_h1.py +++ b/zhaquirks/xiaomi/aqara/remote_h1.py @@ -1,7 +1,15 @@ """Aqara H1-series wireless remote.""" from zigpy.profiles import zha import zigpy.types as t -from zigpy.zcl.clusters.general import Basic, Identify, OnOff, PowerConfiguration +from zigpy.zcl.clusters.general import ( + Basic, + Identify, + LevelControl, + MultistateInput, + OnOff, + PowerConfiguration, +) +from zigpy.zcl.clusters.lighting import Color from zhaquirks import PowerConfigurationCluster from zhaquirks.const import ( @@ -147,9 +155,6 @@ class RemoteH1DoubleRocker1(XiaomiCustomDevice): MODELS_INFO: [(LUMI, "lumi.remote.b28ac1")], ENDPOINTS: { 1: { - # SizePrefixedSimpleDescriptor( - # endpoint=1, profile=260, device_type=259, device_version=1, - # input_clusters=[0, 3, 1], output_clusters=[3, 6]) PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [ @@ -163,9 +168,6 @@ class RemoteH1DoubleRocker1(XiaomiCustomDevice): ], }, 3: { - # SizePrefixedSimpleDescriptor( - # endpoint=3, profile=260, device_type=259, device_version=1, - # input_clusters=[3], output_clusters=[6]) PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [Identify.cluster_id], @@ -218,7 +220,7 @@ class RemoteH1DoubleRocker1(XiaomiCustomDevice): } device_automation_triggers = { # triggers when operation_mode == event - # the button doesn't send an release event after hold + # the button doesn't send a release event after hold (SHORT_PRESS, LEFT): {COMMAND: COMMAND_1_SINGLE}, (DOUBLE_PRESS, LEFT): {COMMAND: COMMAND_1_DOUBLE}, (TRIPLE_PRESS, LEFT): {COMMAND: COMMAND_1_TRIPLE}, @@ -238,17 +240,13 @@ class RemoteH1DoubleRocker1(XiaomiCustomDevice): } -class RemoteH1DoubleRocker2(XiaomiCustomDevice): +class RemoteH1DoubleRocker2(RemoteH1DoubleRocker1): """Aqara H1 Wireless Remote Double Rocker Version WRS-R02, variant 2.""" signature = { MODELS_INFO: [(LUMI, "lumi.remote.b28ac1")], ENDPOINTS: { 1: { - # "1": { - # "profile_id": 260, "device_type": "0x0103", - # "in_clusters": [ "0x0000", "0x0001", "0x0003" ], - # "out_clusters": [ "0x0003", "0x0006" ] } PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [ @@ -262,10 +260,6 @@ class RemoteH1DoubleRocker2(XiaomiCustomDevice): ], }, 2: { - # "2": { - # "profile_id": 260, "device_type": "0x0103", - # "in_clusters": [ "0x0003" ], - # "out_clusters": [ "0x0003", "0x0006" ] } PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [ @@ -277,10 +271,6 @@ class RemoteH1DoubleRocker2(XiaomiCustomDevice): ], }, 3: { - # "3": { - # "profile_id": 260, "device_type": "0x0103", - # "in_clusters": [ "0x0003" ], - # "out_clusters": [ "0x0006" ] } PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, INPUT_CLUSTERS: [Identify.cluster_id], @@ -288,5 +278,117 @@ class RemoteH1DoubleRocker2(XiaomiCustomDevice): }, }, } - replacement = RemoteH1DoubleRocker1.replacement - device_automation_triggers = RemoteH1DoubleRocker1.device_automation_triggers + + +class RemoteH1DoubleRocker3(RemoteH1DoubleRocker1): + """Aqara H1 Wireless Remote Double Rocker Version WRS-R02, variant 3.""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.remote.b28ac1")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + ], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [Identify.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 4: {}, + 5: {}, + 6: {}, + }, + } + + +class RemoteH1DoubleRocker4(RemoteH1DoubleRocker1): + """Aqara H1 Wireless Remote Double Rocker Version WRS-R02, variant 4.""" + + signature = { + MODELS_INFO: [(LUMI, "lumi.remote.b28ac1")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + ], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [Identify.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + MultistateInput.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 5: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + MultistateInput.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 6: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Identify.cluster_id, + MultistateInput.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + }, + }