diff --git a/setup.py b/setup.py index 12c1a2d68c..a5135f99e7 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.83" +VERSION = "0.0.84" setup( diff --git a/tests/test_ikea.py b/tests/test_ikea.py new file mode 100644 index 0000000000..37126d9a46 --- /dev/null +++ b/tests/test_ikea.py @@ -0,0 +1,74 @@ +"""Tests for Ikea Starkvind quirks.""" + +import zhaquirks.ikea.starkvind + + +def test_ikea_starkvind(assert_signature_matches_quirk): + """Test new 'STARKVIND Air purifier table' signature is matched to its quirk.""" + + signature = { + "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4476, 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": "0x0007", + "in_clusters": [ + "0x0000", + "0x0003", + "0x0004", + "0x0005", + "0x0202", + "0xfc57", + "0xfc7d", + ], + "out_clusters": ["0x0019", "0x0400", "0x042a"], + }, + "242": { + "profile_id": 41440, + "device_type": "0x0061", + "in_clusters": [], + "out_clusters": ["0x0021"], + }, + }, + "manufacturer": "IKEA of Sweden", + "model": "STARKVIND Air purifier", + "class": "ikea.starkvind.IkeaSTARKVIND", + } + + assert_signature_matches_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND, signature) + + +def test_ikea_starkvind_v2(assert_signature_matches_quirk): + """Test new 'STARKVIND Air purifier table' signature is matched to its quirk.""" + + signature = { + "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4476, 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": "0x0007", + "in_clusters": [ + "0x0000", + "0x0003", + "0x0004", + "0x0005", + "0x0202", + "0xfc57", + "0xfc7c", + "0xfc7d", + ], + "out_clusters": ["0x0019", "0x0400", "0x042a"], + }, + "242": { + "profile_id": 41440, + "device_type": "0x0061", + "in_clusters": [], + "out_clusters": ["0x0021"], + }, + }, + "manufacturer": "IKEA of Sweden", + "model": "STARKVIND Air purifier table", + "class": "ikea.starkvind.IkeaSTARKVIND_v2", + } + + assert_signature_matches_quirk(zhaquirks.ikea.starkvind.IkeaSTARKVIND_v2, signature) diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 5931facedf..5ebf7ca839 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -93,7 +93,7 @@ async def write_attributes(self, attributes, manufacturer=None): """Prevent remote writes.""" for attrid, value in attributes.items(): if isinstance(attrid, str): - attrid = self.attributes[attrid].id + attrid = self.attributes_by_name[attrid].id elif attrid not in self.attributes: self.error("%d is not a valid attribute id", attrid) continue diff --git a/zhaquirks/heiman/smoke.py b/zhaquirks/heiman/smoke.py index 680cbba326..179b2a3f9e 100644 --- a/zhaquirks/heiman/smoke.py +++ b/zhaquirks/heiman/smoke.py @@ -69,3 +69,60 @@ class HeimanSmokYDLV10(CustomDevice): }, }, } + + +class HeimanSmokCO_V15(CustomDevice): + """CO_V15 quirk.""" + + # NodeDescriptor( + # logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, + # frequency_band=, mac_capability_flags=, + # manufacturer_code=48042, maximum_buffer_size=64, maximum_incoming_transfer_size=0, server_mask=0, maximum_outgoing_transfer_size=0, + # descriptor_capability_field=, + # *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, + # *is_mains_powered=True, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False + # )" + signature = { + MODELS_INFO: [(HEIMAN, "CO_V15")], + ENDPOINTS: { + # "profile_id": 260,"device_type": "0x0402", + # "in_clusters": ["0x0000","0x0001","0x0003","0x0009","0x0500"], + # "out_clusters": ["0x0019"] + 1: { + PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, + DEVICE_TYPE: zigpy.profiles.zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Alarms.cluster_id, + IasZone.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } + + replacement = { + NODE_DESCRIPTOR: zigpy.zdo.types.NodeDescriptor( + 0x02, 0x40, 0x84 & 0b1111_1011, 0xBBAA, 0x40, 0x0000, 0x0000, 0x0000, 0x03 + ), + ENDPOINTS: { + 1: { + PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, + DEVICE_TYPE: zigpy.profiles.zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Alarms.cluster_id, + IasZone.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + }, + } diff --git a/zhaquirks/ikea/__init__.py b/zhaquirks/ikea/__init__.py index d531ff6450..93dccc47b1 100644 --- a/zhaquirks/ikea/__init__.py +++ b/zhaquirks/ikea/__init__.py @@ -10,7 +10,10 @@ from zhaquirks import DoublingPowerConfigurationCluster _LOGGER = logging.getLogger(__name__) + IKEA = "IKEA of Sweden" +IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 +WWAH_CLUSTER_ID = 0xFC57 # decimal = 64599 ('Works with all Hubs' cluster) class LightLinkCluster(CustomCluster, LightLink): diff --git a/zhaquirks/ikea/blinds.py b/zhaquirks/ikea/blinds.py index 64158bc00a..e161246c4f 100644 --- a/zhaquirks/ikea/blinds.py +++ b/zhaquirks/ikea/blinds.py @@ -22,9 +22,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.ikea import IKEA - -IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 +from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID class IkeaTradfriRollerBlinds(CustomDevice): diff --git a/zhaquirks/ikea/fivebtnremotezha.py b/zhaquirks/ikea/fivebtnremotezha.py index a18ca26abf..cffe216c3d 100644 --- a/zhaquirks/ikea/fivebtnremotezha.py +++ b/zhaquirks/ikea/fivebtnremotezha.py @@ -44,13 +44,12 @@ ) from zhaquirks.ikea import ( IKEA, + IKEA_CLUSTER_ID, LightLinkCluster, PowerConfiguration1CRCluster, ScenesCluster, ) -IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 - class IkeaTradfriRemote1(CustomDevice): """Custom device representing IKEA of Sweden TRADFRI remote control.""" diff --git a/zhaquirks/ikea/fourbtnremote.py b/zhaquirks/ikea/fourbtnremote.py index 7cb33d8b11..0e13237be1 100644 --- a/zhaquirks/ikea/fourbtnremote.py +++ b/zhaquirks/ikea/fourbtnremote.py @@ -40,9 +40,12 @@ TURN_OFF, TURN_ON, ) -from zhaquirks.ikea import IKEA, PowerConfiguration2AAACluster, ScenesCluster - -WWAH_CLUSTER_ID = 0xFC57 # decimal = 64599 +from zhaquirks.ikea import ( + IKEA, + WWAH_CLUSTER_ID, + PowerConfiguration2AAACluster, + ScenesCluster, +) class IkeaTradfriRemote(CustomDevice): diff --git a/zhaquirks/ikea/motionzha.py b/zhaquirks/ikea/motionzha.py index c802ddee48..79954815cb 100644 --- a/zhaquirks/ikea/motionzha.py +++ b/zhaquirks/ikea/motionzha.py @@ -23,9 +23,12 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.ikea import IKEA, LightLinkCluster, PowerConfiguration2CRCluster - -IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 +from zhaquirks.ikea import ( + IKEA, + IKEA_CLUSTER_ID, + LightLinkCluster, + PowerConfiguration2CRCluster, +) class IkeaTradfriMotion(CustomDevice): diff --git a/zhaquirks/ikea/opencloseremote.py b/zhaquirks/ikea/opencloseremote.py index fb40f841cd..aefdd691a9 100644 --- a/zhaquirks/ikea/opencloseremote.py +++ b/zhaquirks/ikea/opencloseremote.py @@ -35,13 +35,12 @@ SHORT_PRESS, ZHA_SEND_EVENT, ) -from zhaquirks.ikea import IKEA, PowerConfiguration1CRCluster +from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID, PowerConfiguration1CRCluster COMMAND_CLOSE = "down_close" COMMAND_STOP_OPENING = "stop_opening" COMMAND_STOP_CLOSING = "stop_closing" COMMAND_OPEN = "up_open" -IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 class IkeaWindowCovering(CustomCluster, WindowCovering): diff --git a/zhaquirks/ikea/starkvind.py b/zhaquirks/ikea/starkvind.py index f83def539a..88d6a57171 100644 --- a/zhaquirks/ikea/starkvind.py +++ b/zhaquirks/ikea/starkvind.py @@ -27,9 +27,8 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.ikea import IKEA +from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID, WWAH_CLUSTER_ID -WWAH_CLUSTER_ID = 0xFC57 # decimal = 64599 _LOGGER = logging.getLogger(__name__) @@ -83,7 +82,7 @@ async def write_attributes( """Override wrong writes to thermostat attributes.""" if "fan_mode" in attributes: fan_mode = attributes.get("fan_mode") - if fan_mode > 1 and fan_mode < 11: + if fan_mode and fan_mode > 1 and fan_mode < 11: fan_mode = fan_mode * 5 return await super().write_attributes( {"fan_mode": fan_mode}, manufacturer @@ -220,3 +219,80 @@ def __init__(self, *args, **kwargs): }, }, } + + +class IkeaSTARKVIND_v2(IkeaSTARKVIND): + """STARKVIND Air purifier by IKEA of Sweden.""" + + signature = { + # + MODELS_INFO: IkeaSTARKVIND.signature[MODELS_INFO].copy(), + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE, + INPUT_CLUSTERS: [ + Basic.cluster_id, # 0 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + Fan.cluster_id, # 514 0x0202 + WWAH_CLUSTER_ID, # 64599 0xFC57 + IKEA_CLUSTER_ID, # 64636 0xFC7C + IkeaAirpurifier.cluster_id, # 64637 0xFC7D + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, # 25 0x0019 + IlluminanceMeasurement.cluster_id, # 1024 0x0400 + PM25.cluster_id, # 1066 0x042A PM2.5 Measurement Cluster + ], + }, + # + 242: { + PROFILE_ID: 0xA1E0, # 41440 (dec) + DEVICE_TYPE: 0x0061, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, # 0x0021 = GreenPowerProxy.cluster_id + ], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COMBINED_INTERFACE, + INPUT_CLUSTERS: [ + Basic.cluster_id, # 0 + Identify.cluster_id, # 3 + Groups.cluster_id, # 4 + Scenes.cluster_id, # 5 + WWAH_CLUSTER_ID, # 64599 0xFC57 + IKEA_CLUSTER_ID, # 64636 0xFC7C + IkeaAirpurifier, # 64637 0xFC7D control air purifier with manufacturer-specific attributes + PM25Cluster, # 1066 0x042A PM2.5 Measurement Cluster + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, # 25 0x0019 + IlluminanceMeasurement.cluster_id, # 1024 0x0400 + ], + }, + # + 242: { + PROFILE_ID: 0xA1E0, # 41440 (dec) + DEVICE_TYPE: 0x0061, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, # 0x0021 = GreenPowerProxy.cluster_id + ], + }, + }, + } diff --git a/zhaquirks/ikea/symfonisk.py b/zhaquirks/ikea/symfonisk.py index 213feb971c..cb82f7e9e5 100644 --- a/zhaquirks/ikea/symfonisk.py +++ b/zhaquirks/ikea/symfonisk.py @@ -38,9 +38,7 @@ TRIPLE_PRESS, TURN_ON, ) -from zhaquirks.ikea import IKEA, PowerConfiguration1CRCluster - -IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 +from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID, PowerConfiguration1CRCluster class IkeaSYMFONISK1(CustomDevice): diff --git a/zhaquirks/ikea/tradfriplug.py b/zhaquirks/ikea/tradfriplug.py index 2935859580..906f8e4348 100644 --- a/zhaquirks/ikea/tradfriplug.py +++ b/zhaquirks/ikea/tradfriplug.py @@ -13,9 +13,7 @@ ) from zigpy.zcl.clusters.lightlink import LightLink -from zhaquirks.ikea import IKEA - -IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 +from zhaquirks.ikea import IKEA, IKEA_CLUSTER_ID class TradfriPlug(CustomDevice): diff --git a/zhaquirks/ikea/twobtnremote.py b/zhaquirks/ikea/twobtnremote.py index a6c539c4ba..c655cecb5a 100644 --- a/zhaquirks/ikea/twobtnremote.py +++ b/zhaquirks/ikea/twobtnremote.py @@ -40,9 +40,12 @@ TURN_OFF, TURN_ON, ) -from zhaquirks.ikea import IKEA, LightLinkCluster, PowerConfiguration1CRCluster - -IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 +from zhaquirks.ikea import ( + IKEA, + IKEA_CLUSTER_ID, + LightLinkCluster, + PowerConfiguration1CRCluster, +) class IkeaTradfriRemote2Btn(CustomDevice): diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py index d55420e376..090027a3d9 100644 --- a/zhaquirks/tuya/__init__.py +++ b/zhaquirks/tuya/__init__.py @@ -529,9 +529,13 @@ async def command( TUYA_MCU_COMMAND, cmd_payload, ) - return foundation.Status.SUCCESS + return foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Default_Response + ].schema(command_id=command_id, status=foundation.Status.SUCCESS) - return foundation.Status.UNSUP_CLUSTER_COMMAND + return foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Default_Response + ].schema(command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND) class TuyaManufacturerClusterOnOff(TuyaManufCluster): @@ -688,7 +692,9 @@ async def command( try: current = success[attrid] except KeyError: - return foundation.Status.FAILURE + return foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Default_Response + ].schema(command_id=command_id, status=foundation.Status.FAILURE) # offset is given in decidegrees, see Zigbee cluster specification (res,) = await self.write_attributes( @@ -1286,10 +1292,11 @@ def command( ): """Override the default Cluster command.""" _LOGGER.debug( - "%s Sending Tuya Cluster Command.. Cluster Command is %x, Arguments are %s", + "%s Sending Tuya Cluster Command.. Cluster Command is %x, Arguments are %s, %s", self.endpoint.device.ieee, command_id, args, + kwargs, ) # Move to level # move_to_level_with_on_off diff --git a/zhaquirks/tuya/mcu/__init__.py b/zhaquirks/tuya/mcu/__init__.py index a5ee6a6341..db72b3ff84 100644 --- a/zhaquirks/tuya/mcu/__init__.py +++ b/zhaquirks/tuya/mcu/__init__.py @@ -459,9 +459,10 @@ async def command( ): """Override the default Cluster command.""" self.debug( - "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s", + "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s, %s", command_id, args, + kwargs, ) # getting the level value diff --git a/zhaquirks/tuya/ts000x.py b/zhaquirks/tuya/ts000x.py index 59ae925404..3b9497325a 100644 --- a/zhaquirks/tuya/ts000x.py +++ b/zhaquirks/tuya/ts000x.py @@ -23,7 +23,9 @@ ) from zhaquirks.tuya import ( TuyaZBE000Cluster, + TuyaZBElectricalMeasurement, TuyaZBExternalSwitchTypeCluster, + TuyaZBMeteringCluster, TuyaZBOnOffAttributeCluster, ) from zhaquirks.tuya.mcu import EnchantedDevice @@ -85,6 +87,65 @@ class Switch_1G_GPP(EnchantedDevice, CustomDevice): } +class Switch_1G_Metering(EnchantedDevice, CustomDevice): + """Tuya 1 gang switch with metering support.""" + + signature = { + MODEL: "TS0001", + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBMeteringCluster.cluster_id, + TuyaZBElectricalMeasurement.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + # + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBMeteringCluster, + TuyaZBElectricalMeasurement, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + }, + } + + class Switch_2G_GPP(EnchantedDevice, CustomDevice): """Tuya 2 gang switch module with restore power state support.""" @@ -173,6 +234,94 @@ class Switch_2G_GPP(EnchantedDevice, CustomDevice): } +class Switch_2G_Metering(EnchantedDevice, CustomDevice): + """Tuya 2 gang switch with metering support.""" + + signature = { + MODEL: "TS0002", + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TuyaZBMeteringCluster.cluster_id, + TuyaZBElectricalMeasurement.cluster_id, + TuyaZBE000Cluster.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + # + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + # + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + TuyaZBMeteringCluster, + TuyaZBElectricalMeasurement, + TuyaZBE000Cluster, + TuyaZBExternalSwitchTypeCluster, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Groups.cluster_id, + Scenes.cluster_id, + TuyaZBOnOffAttributeCluster, + ], + OUTPUT_CLUSTERS: [], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + class Switch_3G_GPP(EnchantedDevice, CustomDevice): """Tuya 3 gang switch module with restore power state support.""" diff --git a/zhaquirks/tuya/ts0601_illuminance.py b/zhaquirks/tuya/ts0601_illuminance.py new file mode 100644 index 0000000000..560d184fda --- /dev/null +++ b/zhaquirks/tuya/ts0601_illuminance.py @@ -0,0 +1,124 @@ +"""Tuya HOME-LUX illuminance sensor.""" + +import math +from typing import Dict + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time +from zigpy.zcl.clusters.measurement import IlluminanceMeasurement + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.tuya import TuyaLocalCluster +from zhaquirks.tuya.mcu import DPToAttributeMapping, TuyaDPType, TuyaMCUCluster + +TUYA_BRIGHTNESS_LEVEL_DP = 0x01 # 0-2 "Low, Medium, High" +TUYA_ILLUMINANCE_DP = 0x02 # [0, 0, 3, 232] illuminance + + +class BrightnessLevel(t.enum8): + """Brightness level enum.""" + + LOW = 0x00 + MEDIUM = 0x01 + HIGH = 0x02 + + +class TuyaIlluminanceMeasurement(IlluminanceMeasurement, TuyaLocalCluster): + """Tuya local IlluminanceMeasurement cluster.""" + + attributes = IlluminanceMeasurement.attributes.copy() + attributes.update( + { + 0xFF00: ("manufacturer_brightness_level", BrightnessLevel, True), + } + ) + + +class TuyaIlluminanceCluster(TuyaMCUCluster): + """Tuya Illuminance cluster.""" + + dp_to_attribute: Dict[int, DPToAttributeMapping] = { + TUYA_BRIGHTNESS_LEVEL_DP: DPToAttributeMapping( + TuyaIlluminanceMeasurement.ep_attribute, + "manufacturer_brightness_level", + dp_type=TuyaDPType.ENUM, + converter=lambda x: BrightnessLevel(x), + ), + TUYA_ILLUMINANCE_DP: DPToAttributeMapping( + TuyaIlluminanceMeasurement.ep_attribute, + "measured_value", + dp_type=TuyaDPType.VALUE, + converter=lambda x: (10000.0 * math.log10(x) + 1.0 if x != 0 else 0), + ), + } + + data_point_handlers = { + TUYA_BRIGHTNESS_LEVEL_DP: "_dp_2_attr_update", + TUYA_ILLUMINANCE_DP: "_dp_2_attr_update", + } + + +class TuyaIlluminance(CustomDevice): + """HOME-LUX illuminance sensor. Sense maximum 1000 lumen.""" + + signature = { + # endpoint=1, profile=260, device_type=81, device_version=1, + # input_clusters=[4, 5, 61184, 0], output_clusters=[25, 10]) + MODELS_INFO: [ + ("_TZE200_yi4jtqq1", "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, + TuyaMCUCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 242: { + #