From 8e3bde4b547e7f1b608056a1d97bb3ce52b78395 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 3 Apr 2020 09:34:22 -0400 Subject: [PATCH 001/113] Add Battery quirk for Zen thermostat (#320) * Add Battery quirk for Zen thermostat. * Make lint happy again. * Black. --- zhaquirks/zen/__init__.py | 12 +++++++ zhaquirks/zen/thermostat.py | 69 +++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 zhaquirks/zen/__init__.py create mode 100644 zhaquirks/zen/thermostat.py diff --git a/zhaquirks/zen/__init__.py b/zhaquirks/zen/__init__.py new file mode 100644 index 0000000000..2ed747855e --- /dev/null +++ b/zhaquirks/zen/__init__.py @@ -0,0 +1,12 @@ +"""Module for Zen Within quirks implementations.""" + +from .. import PowerConfigurationCluster + +ZEN = "Zen Within" + + +class ZenPowerConfiguration(PowerConfigurationCluster): + """Common use power configuration cluster.""" + + MIN_VOLTS = 3.6 + MAX_VOLTS = 6.0 diff --git a/zhaquirks/zen/thermostat.py b/zhaquirks/zen/thermostat.py new file mode 100644 index 0000000000..c5fc508b78 --- /dev/null +++ b/zhaquirks/zen/thermostat.py @@ -0,0 +1,69 @@ +"""This module handles quirks of the Zen Within thermostat.""" + +import zigpy.profiles.zha as zha_p +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters import general, homeautomation, hvac + +from . import ZEN, ZenPowerConfiguration +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class ZenThermostat(CustomDevice): + """Zen Within Thermostat custom device.""" + + signature = { + # Node Descriptor: + # + MODELS_INFO: [(ZEN, "Zen-01")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.THERMOSTAT, + INPUT_CLUSTERS: [ + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.Groups.cluster_id, + general.Scenes.cluster_id, + general.PollControl.cluster_id, + hvac.Thermostat.cluster_id, + hvac.Fan.cluster_id, + hvac.UserInterface.cluster_id, + homeautomation.Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [general.Time.cluster_id, general.Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + general.Basic.cluster_id, + ZenPowerConfiguration, + general.Identify.cluster_id, + general.Groups.cluster_id, + general.Scenes.cluster_id, + general.PollControl.cluster_id, + hvac.Thermostat.cluster_id, + hvac.Fan.cluster_id, + hvac.UserInterface.cluster_id, + homeautomation.Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [general.Time.cluster_id, general.Ota.cluster_id], + } + } + } From 1577b94249fb9b6e71864df0697c9d3233a3ec0c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 3 Apr 2020 09:35:56 -0400 Subject: [PATCH 002/113] Update travis-ci urls --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8df16c612..71ce3c5a52 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/dmulcahey/zha-device-handlers.svg?branch=master)](https://travis-ci.org/dmulcahey/zha-device-handlers) +[![Build Status](https://travis-ci.org/zigpy/zha-device-handlers.svg?branch=master)](https://travis-ci.org/zigpy/zha-device-handlers) # ZHA Device Handlers For Home Assistant From c7ff50c6c6b7c7caad68d621c668c7a0842f73fe Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 3 Apr 2020 12:48:02 -0400 Subject: [PATCH 003/113] New Konke model of temp/humidity sensor. (#321) --- zhaquirks/konke/temp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/konke/temp.py b/zhaquirks/konke/temp.py index c1324a58c3..986c56dc67 100644 --- a/zhaquirks/konke/temp.py +++ b/zhaquirks/konke/temp.py @@ -25,7 +25,7 @@ class KonkeTempHumidity(CustomDevice): # device_version=0 # input_clusters=[0, 1, 3, 1026, 1029] # output_clusters=[3]> - MODELS_INFO: [(KONKE, "3AFE140103020000")], + MODELS_INFO: [(KONKE, "3AFE140103020000"), (KONKE, "3AFE220103020000")], ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, From 90ea8bcfacb29cb5f3b275a28d0ff1078e63637b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 8 Apr 2020 09:33:55 -0400 Subject: [PATCH 004/113] Update Zigpy and cleanup (#331) * update zigpy version * tox reqs bump * disable too many ancestors * fix signatures --- pylintrc | 1 + requirements_test_all.txt | 2 +- setup.py | 2 +- zhaquirks/xbee/__init__.py | 16 ++++++++++------ zhaquirks/xiaomi/aqara/ctrl_neutral1.py | 6 ++++-- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pylintrc b/pylintrc index 1ccde090e8..8cc5301b82 100644 --- a/pylintrc +++ b/pylintrc @@ -26,6 +26,7 @@ disable= not-context-manager, redefined-variable-type, too-few-public-methods, + too-many-ancestors, too-many-arguments, too-many-branches, too-many-instance-attributes, diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 255312c8b5..e717df40aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -20,5 +20,5 @@ pytest-timeout==1.3.3 pytest==5.1.2 requests_mock==1.6.0 -zigpy-homeassistant==0.8.0 +zigpy-homeassistant==0.18.1 homeassistant diff --git a/setup.py b/setup.py index ed5e0feea2..99bd58aeea 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,6 @@ def readme(): keywords="zha quirks homeassistant hass", packages=find_packages(exclude=["*.tests"]), python_requires=">=3", - install_requires=["zigpy-homeassistant>=0.8.0"], + install_requires=["zigpy-homeassistant>=0.18.1"], tests_require=["pytest"], ) diff --git a/zhaquirks/xbee/__init__.py b/zhaquirks/xbee/__init__.py index 54d373b1d5..af2a5fb816 100644 --- a/zhaquirks/xbee/__init__.py +++ b/zhaquirks/xbee/__init__.py @@ -78,18 +78,20 @@ class XBeeOnOff(LocalDataCluster, OnOff): """XBee on/off cluster.""" - async def command(self, command, *args, manufacturer=None, expect_reply=True): + async def command( + self, command_id, *args, manufacturer=None, expect_reply=True, tsn=None + ): """Xbee change pin state command, requires zigpy_xbee.""" pin_name = ENDPOINT_TO_AT.get(self._endpoint.endpoint_id) - if command not in [0, 1] or pin_name is None: - return super().command(command, *args) - if command == 0: + if command_id not in [0, 1] or pin_name is None: + return super().command(command_id, *args) + if command_id == 0: pin_cmd = DIO_PIN_LOW else: pin_cmd = DIO_PIN_HIGH result = await self._endpoint.device.remote_at(pin_name, pin_cmd) if result == foundation.Status.SUCCESS: - self._update_attribute(ATTR_ON_OFF, command) + self._update_attribute(ATTR_ON_OFF, command_id) return 0, result @@ -298,7 +300,9 @@ def deserialize(cls, data): data = str(data, encoding="latin1") return (cls(data), b"") - def command(self, command, *args, manufacturer=None, expect_reply=False): + def command( + self, command_id, *args, manufacturer=None, expect_reply=False, tsn=None + ): """Handle outgoing data.""" data = self.BinaryString(args[0]).serialize() return self._endpoint.device.application.request( diff --git a/zhaquirks/xiaomi/aqara/ctrl_neutral1.py b/zhaquirks/xiaomi/aqara/ctrl_neutral1.py index f23251dbd1..c9ca1c0bdc 100644 --- a/zhaquirks/xiaomi/aqara/ctrl_neutral1.py +++ b/zhaquirks/xiaomi/aqara/ctrl_neutral1.py @@ -50,7 +50,9 @@ class XiaomiOnOffCluster(OnOff, CustomCluster): server_commands = {0x0000: ("off", (), False), 0x0001: ("on", (), False)} - def command(self, command, *args, manufacturer=None, expect_reply=True): + def command( + self, command_id, *args, manufacturer=None, expect_reply=True, tsn=None + ): """Command handler.""" src_ep = 1 dst_ep = 2 @@ -62,7 +64,7 @@ def command(self, command, *args, manufacturer=None, expect_reply=True): src_ep, dst_ep, seq, - bytes([src_ep, seq, command]), + bytes([src_ep, seq, command_id]), expect_reply=expect_reply, ) From e0204867c9274b8b88e5e7e18a084000108277f2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 15 Apr 2020 09:00:06 -0400 Subject: [PATCH 005/113] Make Plaid Systems soil sensor use mains voltage (#329) * Make Plaid Systems soil sensor use mains voltage * make battery voltage work too --- zhaquirks/plaid/soil.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/zhaquirks/plaid/soil.py b/zhaquirks/plaid/soil.py index 5ac3659c8d..a85fc142bd 100644 --- a/zhaquirks/plaid/soil.py +++ b/zhaquirks/plaid/soil.py @@ -17,6 +17,17 @@ ) +class PowerConfigurationClusterMains(PowerConfigurationCluster): + """Common use power configuration cluster.""" + + MAINS_VOLTAGE_ATTR = 0x0000 + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == self.MAINS_VOLTAGE_ATTR: + super()._update_attribute(self.self.BATTERY_VOLTAGE_ATTR, value) + + class SoilMoisture(CustomDevice): """Custom device representing plaid systems soil sensors.""" @@ -49,7 +60,7 @@ class SoilMoisture(CustomDevice): DEVICE_TYPE: 1029, INPUT_CLUSTERS: [ Basic.cluster_id, - PowerConfigurationCluster, + PowerConfigurationClusterMains, Identify.cluster_id, TemperatureMeasurement.cluster_id, RelativeHumidity.cluster_id, From 86d00cf45e7269f007bf7769043b759bcd26db29 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 15 Apr 2020 09:00:29 -0400 Subject: [PATCH 006/113] add alternate osram switch (#328) --- zhaquirks/osram/lightifyx4.py | 369 ++++++++++++++++------------------ 1 file changed, 171 insertions(+), 198 deletions(-) diff --git a/zhaquirks/osram/lightifyx4.py b/zhaquirks/osram/lightifyx4.py index 9bf652ceff..dcec905f53 100644 --- a/zhaquirks/osram/lightifyx4.py +++ b/zhaquirks/osram/lightifyx4.py @@ -1,4 +1,5 @@ """Osram Lightify X4 device.""" +import copy import logging from zigpy.profiles import zha @@ -48,55 +49,71 @@ _LOGGER = logging.getLogger(__name__) -class LightifyX4(CustomDevice): - """Osram Lightify X4 device.""" +class OsramButtonCluster(CustomCluster): + """OsramButtonCluster.""" - class OsramButtonCluster(CustomCluster): - """OsramButtonCluster.""" + cluster_id = OSRAM_CLUSTER + name = "OsramCluster" + ep_attribute = "osram_cluster" + attributes = { + 0x000A: ("osram_1", t.uint8_t), + 0x000B: ("osram_2", t.uint8_t), + 0x000C: ("osram_3", t.uint16_t), + 0x000D: ("osram_4", t.uint16_t), + 0x0019: ("osram_5", t.uint8_t), + 0x001A: ("osram_6", t.uint16_t), + 0x001B: ("osram_7", t.uint16_t), + 0x001C: ("osram_8", t.uint8_t), + 0x001D: ("osram_9", t.uint16_t), + 0x001E: ("osram_10", t.uint16_t), + 0x002C: ("osram_11", t.uint16_t), + 0x002D: ("osram_12", t.uint16_t), + 0x002E: ("osram_13", t.uint16_t), + 0x002F: ("osram_14", t.uint16_t), + } + server_commands = {} + client_commands = {} + attr_config = { + 0x000A: 0x01, + 0x000B: 0x00, + 0x000C: 0xFFFF, + 0x000D: 0xFFFF, + 0x0019: 0x06, + 0x001A: 0x0001, + 0x001B: 0x0026, + 0x001C: 0x07, + 0x001D: 0xFFFF, + 0x001E: 0xFFFF, + 0x002C: 0xFFFF, + 0x002D: 0xFFFF, + 0x002E: 0xFFFF, + 0x002F: 0xFFFF, + } - cluster_id = OSRAM_CLUSTER - name = "OsramCluster" - ep_attribute = "osram_cluster" - attributes = { - 0x000A: ("osram_1", t.uint8_t), - 0x000B: ("osram_2", t.uint8_t), - 0x000C: ("osram_3", t.uint16_t), - 0x000D: ("osram_4", t.uint16_t), - 0x0019: ("osram_5", t.uint8_t), - 0x001A: ("osram_6", t.uint16_t), - 0x001B: ("osram_7", t.uint16_t), - 0x001C: ("osram_8", t.uint8_t), - 0x001D: ("osram_9", t.uint16_t), - 0x001E: ("osram_10", t.uint16_t), - 0x002C: ("osram_11", t.uint16_t), - 0x002D: ("osram_12", t.uint16_t), - 0x002E: ("osram_13", t.uint16_t), - 0x002F: ("osram_14", t.uint16_t), - } - server_commands = {} - client_commands = {} - attr_config = { - 0x000A: 0x01, - 0x000B: 0x00, - 0x000C: 0xFFFF, - 0x000D: 0xFFFF, - 0x0019: 0x06, - 0x001A: 0x0001, - 0x001B: 0x0026, - 0x001C: 0x07, - 0x001D: 0xFFFF, - 0x001E: 0xFFFF, - 0x002C: 0xFFFF, - 0x002D: 0xFFFF, - 0x002E: 0xFFFF, - 0x002F: 0xFFFF, - } + async def bind(self): + """Bind cluster.""" + result = await super().bind() + await self.write_attributes(self.attr_config, manufacturer=OSRAM_MFG_CODE) + return result - async def bind(self): - """Bind cluster.""" - result = await super().bind() - await self.write_attributes(self.attr_config, manufacturer=OSRAM_MFG_CODE) - return result + +class LightifyX4(CustomDevice): + """Osram Lightify X4 device.""" + + SIGNATURE_ENDPOINT = { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: OSRAM_DEVICE, + INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], + OUTPUT_CLUSTERS: [ + Groups.cluster_id, + Identify.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Color.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + ], + } signature = { # - 2: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, + 2: copy.deepcopy(SIGNATURE_ENDPOINT), # - 3: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, + 3: copy.deepcopy(SIGNATURE_ENDPOINT), # - 4: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, + 4: copy.deepcopy(SIGNATURE_ENDPOINT), # - 5: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, + 5: copy.deepcopy(SIGNATURE_ENDPOINT), # - 6: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, + 6: copy.deepcopy(SIGNATURE_ENDPOINT), }, } + REPLACEMENT_ENDPOINT = { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: OSRAM_DEVICE, + INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OsramButtonCluster], + OUTPUT_CLUSTERS: [ + Groups.cluster_id, + Identify.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Color.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + ], + } + replacement = { ENDPOINTS: { 1: { @@ -242,88 +209,11 @@ async def bind(self): LightLink.cluster_id, ], }, - 2: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - LightLink.cluster_id, - OsramButtonCluster, - ], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, - 3: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - LightLink.cluster_id, - OsramButtonCluster, - ], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, - 4: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [ - Basic.cluster_id, - LightLink.cluster_id, - OsramButtonCluster, - ], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, - 5: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, - 6: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: OSRAM_DEVICE, - INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], - OUTPUT_CLUSTERS: [ - Groups.cluster_id, - Identify.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Color.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - ], - }, + 2: copy.deepcopy(REPLACEMENT_ENDPOINT), + 3: copy.deepcopy(REPLACEMENT_ENDPOINT), + 4: copy.deepcopy(REPLACEMENT_ENDPOINT), + 5: copy.deepcopy(REPLACEMENT_ENDPOINT), + 6: copy.deepcopy(REPLACEMENT_ENDPOINT), } } @@ -341,3 +231,86 @@ async def bind(self): (LONG_RELEASE, BUTTON_3): {COMMAND: COMMAND_STOP, ENDPOINT_ID: 3}, (LONG_RELEASE, BUTTON_4): {COMMAND: COMMAND_STOP, ENDPOINT_ID: 4}, } + + +class LightifySwitch(CustomDevice): + """Osram Lightify Switch device.""" + + SIGNATURE_ENDPOINT = { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: OSRAM_DEVICE, + INPUT_CLUSTERS: [LightLink.cluster_id, OSRAM_CLUSTER], + OUTPUT_CLUSTERS: [ + Groups.cluster_id, + Identify.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Color.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + ], + } + + signature = { + # + MODELS_INFO: [(OSRAM, "Switch-LIGHTIFY")], + ENDPOINTS: { + 1: copy.deepcopy(LightifyX4.signature[ENDPOINTS][1]), + # + 2: copy.deepcopy(SIGNATURE_ENDPOINT), + # + 3: copy.deepcopy(SIGNATURE_ENDPOINT), + # + 4: copy.deepcopy(SIGNATURE_ENDPOINT), + # + 5: copy.deepcopy(SIGNATURE_ENDPOINT), + # + 6: copy.deepcopy(SIGNATURE_ENDPOINT), + }, + } + + REPLACEMENT_ENDPOINT = { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: OSRAM_DEVICE, + INPUT_CLUSTERS: [LightLink.cluster_id, OsramButtonCluster], + OUTPUT_CLUSTERS: [ + Groups.cluster_id, + Identify.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Color.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + ], + } + + replacement = { + ENDPOINTS: { + 1: copy.deepcopy(LightifyX4.replacement[ENDPOINTS][1]), + 2: copy.deepcopy(REPLACEMENT_ENDPOINT), + 3: copy.deepcopy(REPLACEMENT_ENDPOINT), + 4: copy.deepcopy(REPLACEMENT_ENDPOINT), + 5: copy.deepcopy(REPLACEMENT_ENDPOINT), + 6: copy.deepcopy(REPLACEMENT_ENDPOINT), + } + } + + device_automation_triggers = copy.deepcopy(LightifyX4.device_automation_triggers) From b09d9f80cfe5cf0d7c44addc484876729a60d67c Mon Sep 17 00:00:00 2001 From: Robert Chmielowiec Date: Mon, 20 Apr 2020 13:43:47 +0200 Subject: [PATCH 007/113] Add lumi.sensor_motion.aq2 illuminance reports (#341) --- zhaquirks/xiaomi/__init__.py | 39 ++++++++++++++++++++++++--- zhaquirks/xiaomi/aqara/__init__.py | 14 ---------- zhaquirks/xiaomi/aqara/motion_aq2.py | 3 ++- zhaquirks/xiaomi/aqara/motion_aq2b.py | 11 ++++++-- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index f7576e5cb9..d47eeae869 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -2,19 +2,21 @@ import asyncio import binascii import logging +import math +import zigpy.zcl.foundation as foundation from zigpy import types as t from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import AnalogInput, Basic, PowerConfiguration from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.measurement import ( + IlluminanceMeasurement, OccupancySensing, PressureMeasurement, RelativeHumidity, TemperatureMeasurement, ) from zigpy.zcl.clusters.security import IasZone -import zigpy.zcl.foundation as foundation from .. import Bus, LocalDataCluster from ..const import ( @@ -56,6 +58,8 @@ POWER_REPORTED = "power_reported" CONSUMPTION_REPORTED = "consumption_reported" VOLTAGE_REPORTED = "voltage_reported" +ILLUMINANCE_MEASUREMENT = "illuminance_measurement" +ILLUMINANCE_REPORTED = "illuminance_reported" XIAOMI_AQARA_ATTRIBUTE = 0xFF01 XIAOMI_ATTR_3 = "X-attrib-3" XIAOMI_ATTR_4 = "X-attrib-4" @@ -181,6 +185,10 @@ def _update_attribute(self, attrid, value): self.endpoint.device.voltage_bus.listener_event( VOLTAGE_REPORTED, attributes[VOLTAGE] * 0.1 ) + if ILLUMINANCE_MEASUREMENT in attributes: + self.endpoint.device.illuminance_bus.listener_event( + ILLUMINANCE_REPORTED, attributes[ILLUMINANCE_MEASUREMENT] + ) def _parse_aqara_attributes(self, value): """Parse non standard atrributes.""" @@ -213,6 +221,11 @@ def _parse_aqara_attributes(self, value): "lumi.relay.c2acn01", ]: attribute_names.update({149: CONSUMPTION, 150: VOLTAGE, 152: POWER}) + elif ( + MODEL in self._attr_cache + and self._attr_cache[MODEL] == "lumi.sensor_motion.aq2" + ): + attribute_names.update({11: ILLUMINANCE_MEASUREMENT}) result = {} while value: @@ -235,8 +248,7 @@ def _parse_aqara_attributes(self, value): return attributes def _parse_mija_attributes(self, value): - """Parse non standard atrributes.""" - attributes = {} + """Parse non standard attributes.""" attribute_names = ( STATE, BATTERY_VOLTAGE_MV, @@ -457,3 +469,24 @@ def voltage_reported(self, value): def consumption_reported(self, value): """Consumption reported.""" self._update_attribute(self.CONSUMPTION_ID, value) + + +class IlluminanceMeasurementCluster(CustomCluster, IlluminanceMeasurement): + """Multistate input cluster.""" + + cluster_id = IlluminanceMeasurement.cluster_id + ATTR_ID = 0 + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.illuminance_bus.add_listener(self) + + def _update_attribute(self, attrid, value): + if attrid == self.ATTR_ID and value > 0: + value = 10000 * math.log10(value) + 1 + super()._update_attribute(attrid, value) + + def illuminance_reported(self, value): + """Illuminance reported.""" + self._update_attribute(self.ATTR_ID, value) diff --git a/zhaquirks/xiaomi/aqara/__init__.py b/zhaquirks/xiaomi/aqara/__init__.py index 4771c748bc..d54b08ce73 100644 --- a/zhaquirks/xiaomi/aqara/__init__.py +++ b/zhaquirks/xiaomi/aqara/__init__.py @@ -1,15 +1 @@ """Module for Xiaomi Aqara quirks implementations.""" -import math -from zigpy.zcl.clusters.measurement import IlluminanceMeasurement -from zhaquirks import CustomCluster - - -class IlluminanceMeasurementCluster(CustomCluster, IlluminanceMeasurement): - """Multistate input cluster.""" - - cluster_id = IlluminanceMeasurement.cluster_id - - def _update_attribute(self, attrid, value): - if attrid == 0 and value > 0: - value = 10000 * math.log10(value) + 1 - super()._update_attribute(attrid, value) diff --git a/zhaquirks/xiaomi/aqara/motion_aq2.py b/zhaquirks/xiaomi/aqara/motion_aq2.py index d707f5c48d..52b35a6e14 100644 --- a/zhaquirks/xiaomi/aqara/motion_aq2.py +++ b/zhaquirks/xiaomi/aqara/motion_aq2.py @@ -5,10 +5,10 @@ from zigpy.zcl.clusters.measurement import OccupancySensing from zigpy.zcl.clusters.security import IasZone -from . import IlluminanceMeasurementCluster from .. import ( LUMI, BasicCluster, + IlluminanceMeasurementCluster, MotionCluster, OccupancyCluster, PowerConfigurationCluster, @@ -35,6 +35,7 @@ def __init__(self, *args, **kwargs): """Init.""" self.battery_size = 9 self.motion_bus = Bus() + self.illuminance_bus = Bus() super().__init__(*args, **kwargs) signature = { diff --git a/zhaquirks/xiaomi/aqara/motion_aq2b.py b/zhaquirks/xiaomi/aqara/motion_aq2b.py index 08fcea33a6..8f5ed9d502 100644 --- a/zhaquirks/xiaomi/aqara/motion_aq2b.py +++ b/zhaquirks/xiaomi/aqara/motion_aq2b.py @@ -4,8 +4,14 @@ from zigpy.zcl.clusters.general import Basic, Ota from zigpy.zcl.clusters.measurement import OccupancySensing -from . import IlluminanceMeasurementCluster -from .. import LUMI, BasicCluster, MotionCluster, OccupancyCluster, XiaomiCustomDevice +from .. import ( + LUMI, + BasicCluster, + IlluminanceMeasurementCluster, + MotionCluster, + OccupancyCluster, + XiaomiCustomDevice, +) from ... import Bus from ...const import ( DEVICE_TYPE, @@ -27,6 +33,7 @@ def __init__(self, *args, **kwargs): """Init.""" self.battery_size = 9 self.motion_bus = Bus() + self.illuminance_bus = Bus() super().__init__(*args, **kwargs) signature = { From 6b1bfd8f71e36f6a7e3c99737351a58fa877bce1 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 20 Apr 2020 08:02:25 -0400 Subject: [PATCH 008/113] Add Legrand dimmer without neutral (#330) * Add Legrand dimmer without neutral * fix comment * Update zhaquirks/legrand/dimmer.py Co-Authored-By: Patrick Decat * leading spaces Co-authored-by: Patrick Decat --- zhaquirks/legrand/__init__.py | 2 + zhaquirks/legrand/dimmer.py | 95 +++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 zhaquirks/legrand/__init__.py create mode 100644 zhaquirks/legrand/dimmer.py diff --git a/zhaquirks/legrand/__init__.py b/zhaquirks/legrand/__init__.py new file mode 100644 index 0000000000..c5c474299e --- /dev/null +++ b/zhaquirks/legrand/__init__.py @@ -0,0 +1,2 @@ +"""Module for Legrand devices.""" +LEGRAND = "Legrand" diff --git a/zhaquirks/legrand/dimmer.py b/zhaquirks/legrand/dimmer.py new file mode 100644 index 0000000000..5ac196eeed --- /dev/null +++ b/zhaquirks/legrand/dimmer.py @@ -0,0 +1,95 @@ +"""Device handler for Legrand Dimmer switch w/o neutral.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import ( + Basic, + BinaryInput, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + Scenes, +) +from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster + +from . import LEGRAND +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFC01 # decimal = 64513 + + +class LegrandCluster(CustomCluster, ManufacturerSpecificCluster): + """LegrandCluster.""" + + cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID + name = "LegrandCluster" + ep_attribute = "legrand_cluster" + attributes = { + 0x0000: ("dimmer", t.data16), + 0x0001: ("led_dark", t.Bool), + 0x0002: ("led_on", t.Bool), + } + server_commands = {} + client_commands = {} + + +class DimmerWithoutNeutral(CustomDevice): + """Dimmer switch w/o neutral.""" + + signature = { + # + MODELS_INFO: [(f" {LEGRAND}", " Dimmer switch w/o neutral")], + 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, + OnOff.cluster_id, + LevelControl.cluster_id, + Scenes.cluster_id, + BinaryInput.cluster_id, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + Ota.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, + OnOff.cluster_id, + LevelControl.cluster_id, + Scenes.cluster_id, + BinaryInput.cluster_id, + LegrandCluster, + ], + OUTPUT_CLUSTERS: [Basic.cluster_id, LegrandCluster, Ota.cluster_id], + } + } + } From a845264a0b82d7574f01c061c26c6ad99be6ff09 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 20 Apr 2020 08:25:20 -0400 Subject: [PATCH 009/113] use zigpy (#342) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 99bd58aeea..1c5e45900e 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,6 @@ def readme(): keywords="zha quirks homeassistant hass", packages=find_packages(exclude=["*.tests"]), python_requires=">=3", - install_requires=["zigpy-homeassistant>=0.18.1"], + install_requires=["zigpy>=0.20.0"], tests_require=["pytest"], ) From 773b760c5b1b31a5246c3b88ecb7c2092123b561 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 20 Apr 2020 08:25:40 -0400 Subject: [PATCH 010/113] support another centralite IAS signature (#343) --- zhaquirks/centralite/ias.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/zhaquirks/centralite/ias.py b/zhaquirks/centralite/ias.py index f109f5c4e0..ef70d107f7 100755 --- a/zhaquirks/centralite/ias.py +++ b/zhaquirks/centralite/ias.py @@ -132,3 +132,36 @@ class CentraLiteIASSensorV2(CustomDevice): } replacement = CentraLiteIASSensor.replacement + + +class CentraLiteIASSensorV3(CustomDevice): + """Custom device representing centralite ias sensors.""" + + signature = { + # + MODELS_INFO: CentraLiteIASSensor.signature[MODELS_INFO], + ENDPOINTS: { + 1: CentraLiteIASSensor.signature[ENDPOINTS][1], + # + 2: { + PROFILE_ID: MANUFACTURER_SPECIFIC_PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SIMPLE_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + BinaryInput.cluster_id, + PowerConfigurationCluster.cluster_id, + Identify.cluster_id, + DIAGNOSTICS_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id], + }, + }, + } + + replacement = CentraLiteIASSensor.replacement From b34e3f8142da89f29bdc0919c5ed9b13853ec32d Mon Sep 17 00:00:00 2001 From: Balazs Sandor Date: Fri, 8 May 2020 18:03:37 +0200 Subject: [PATCH 011/113] Extend Xiaomi wall switch support (#349) --- tests/test_ctrl_neutral1.py | 13 +- zhaquirks/xiaomi/__init__.py | 33 ++++- zhaquirks/xiaomi/aqara/ctrl_ln.py | 135 ++++++++++++++++++ .../{ctrl_neutral1.py => ctrl_neutral.py} | 67 ++++----- 4 files changed, 210 insertions(+), 38 deletions(-) create mode 100644 zhaquirks/xiaomi/aqara/ctrl_ln.py rename zhaquirks/xiaomi/aqara/{ctrl_neutral1.py => ctrl_neutral.py} (79%) diff --git a/tests/test_ctrl_neutral1.py b/tests/test_ctrl_neutral1.py index a6a60b6b09..96436bda43 100644 --- a/tests/test_ctrl_neutral1.py +++ b/tests/test_ctrl_neutral1.py @@ -5,7 +5,7 @@ import zigpy.application from zigpy.device import Device -from zhaquirks.xiaomi.aqara.ctrl_neutral1 import CtrlNeutral1 +from zhaquirks.xiaomi.aqara.ctrl_neutral import CtrlNeutral # zigbee-herdsman:controller:endpoint Command 0x00158d00024be541/2 genOnOff.on({}, @@ -24,7 +24,7 @@ # zigbee-herdsman:adapter:zStack:unpi:writer --> frame [254,13,36,1,31,255,2,1,6,0,16,0,30,3,1,7,0,198] -def test_ctrl_neutral1(): +def test_ctrl_neutral(): """Test ctrl neutral 1 sends correct request.""" sec = 8 ieee = 0 @@ -41,10 +41,13 @@ def test_ctrl_neutral1(): rep = Device(app, ieee, nwk) rep.add_endpoint(1) rep.add_endpoint(2) + rep.add_endpoint(3) + + dev = CtrlNeutral(app, ieee, nwk, rep) + dev.request = mock.MagicMock() - dev = CtrlNeutral1(app, ieee, nwk, rep) dev[2].in_clusters[cluster].command(1) - assert app.request.call_args == call( - dev, 260, cluster, src_ep, dst_ep, sec, data, expect_reply=True + assert dev.request.call_args == call( + 260, cluster, src_ep, dst_ep, sec, data, expect_reply=True ) diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index d47eeae869..9149b91537 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -3,11 +3,13 @@ import binascii import logging import math +from typing import Optional, Union import zigpy.zcl.foundation as foundation from zigpy import types as t +from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice -from zigpy.zcl.clusters.general import AnalogInput, Basic, PowerConfiguration +from zigpy.zcl.clusters.general import AnalogInput, Basic, OnOff, PowerConfiguration from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.measurement import ( IlluminanceMeasurement, @@ -490,3 +492,32 @@ def _update_attribute(self, attrid, value): def illuminance_reported(self, value): """Illuminance reported.""" self._update_attribute(self.ATTR_ID, value) + + +class OnOffCluster(OnOff, CustomCluster): + """Aqara wall switch cluster.""" + + def command( + self, + command_id: Union[foundation.Command, int, t.uint8_t], + *args, + manufacturer: Optional[Union[int, t.uint16_t]] = None, + expect_reply: bool = True, + tsn: Optional[Union[int, t.uint8_t]] = None + ): + """Command handler.""" + src_ep = 1 + dst_ep = self.endpoint.endpoint_id + device = self.endpoint.device + if tsn is None: + tsn = self._endpoint.device.application.get_sequence() + return device.request( + # device, + zha.PROFILE_ID, + OnOff.cluster_id, + src_ep, + dst_ep, + tsn, + bytes([src_ep, tsn, command_id]), + expect_reply=expect_reply, + ) diff --git a/zhaquirks/xiaomi/aqara/ctrl_ln.py b/zhaquirks/xiaomi/aqara/ctrl_ln.py new file mode 100644 index 0000000000..ed92cf4119 --- /dev/null +++ b/zhaquirks/xiaomi/aqara/ctrl_ln.py @@ -0,0 +1,135 @@ +"""Xiaomi aqara single key wall switch devices.""" +from zigpy.profiles import zha +from zigpy.zcl.clusters.general import ( + AnalogInput, + Basic, + Groups, + Identify, + MultistateInput, + OnOff, + Ota, + Scenes, + DeviceTemperature, + Time, + BinaryOutput, +) + +from zhaquirks import Bus +from .. import ( + LUMI, + AnalogInputCluster, + BasicCluster, + OnOffCluster, + PowerConfigurationCluster, + XiaomiCustomDevice, +) +from ...const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, + SKIP_CONFIGURATION, +) + + +class CtrlLn(XiaomiCustomDevice): + """Aqara double key switch device.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.power_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + MODELS_INFO: [(LUMI, "lumi.ctrl_ln1.aq1"), (LUMI, "lumi.ctrl_ln2.aq1")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfigurationCluster.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Time.cluster_id, + BinaryOutput.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [OnOff.cluster_id, BinaryOutput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [Groups.cluster_id, AnalogInput.cluster_id], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id], + }, + 5: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [MultistateInput.cluster_id, BinaryOutput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 6: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [MultistateInput.cluster_id, BinaryOutput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 7: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [MultistateInput.cluster_id, BinaryOutput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + }, + } + + replacement = { + SKIP_CONFIGURATION: True, + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + BasicCluster, + PowerConfigurationCluster, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOffCluster, + Time.cluster_id, + BinaryOutput.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [OnOffCluster, BinaryOutput.cluster_id], + OUTPUT_CLUSTERS: [], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInputCluster], + OUTPUT_CLUSTERS: [Groups.cluster_id, AnalogInput.cluster_id], + }, + }, + } diff --git a/zhaquirks/xiaomi/aqara/ctrl_neutral1.py b/zhaquirks/xiaomi/aqara/ctrl_neutral.py similarity index 79% rename from zhaquirks/xiaomi/aqara/ctrl_neutral1.py rename to zhaquirks/xiaomi/aqara/ctrl_neutral.py index c9ca1c0bdc..f53ff409e7 100644 --- a/zhaquirks/xiaomi/aqara/ctrl_neutral1.py +++ b/zhaquirks/xiaomi/aqara/ctrl_neutral.py @@ -2,7 +2,6 @@ import logging from zigpy.profiles import zha -from zigpy.quirks import CustomCluster from zigpy.zcl.clusters.general import ( AnalogInput, Basic, @@ -17,7 +16,13 @@ BinaryOutput, ) -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice +from .. import ( + LUMI, + BasicCluster, + OnOffCluster, + PowerConfigurationCluster, + XiaomiCustomDevice, +) from ...const import ( DEVICE_TYPE, ENDPOINTS, @@ -45,35 +50,11 @@ # double click 0xCFF1F00 -class XiaomiOnOffCluster(OnOff, CustomCluster): - """Aqara wall switch cluster.""" - - server_commands = {0x0000: ("off", (), False), 0x0001: ("on", (), False)} - - def command( - self, command_id, *args, manufacturer=None, expect_reply=True, tsn=None - ): - """Command handler.""" - src_ep = 1 - dst_ep = 2 - seq = self._endpoint.device.application.get_sequence() - return self._endpoint.device.application.request( - self._endpoint.device, - zha.PROFILE_ID, - OnOff.cluster_id, - src_ep, - dst_ep, - seq, - bytes([src_ep, seq, command_id]), - expect_reply=expect_reply, - ) - - -class CtrlNeutral1(XiaomiCustomDevice): - """Aqara single key switch device.""" +class CtrlNeutral(XiaomiCustomDevice): + """Aqara single and double key switch device.""" signature = { - MODELS_INFO: [(LUMI, "lumi.ctrl_neutral1")], + MODELS_INFO: [(LUMI, "lumi.ctrl_neutral1"), (LUMI, "lumi.ctrl_neutral2")], ENDPOINTS: { # Date: Fri, 8 May 2020 12:11:11 -0400 Subject: [PATCH 012/113] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1c5e45900e..e691f3d45b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.38" +VERSION = "0.0.39" def readme(): From e98c8185cc113a5758e2efdab149a1cea3ceb3cf Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Thu, 21 May 2020 17:38:52 +0300 Subject: [PATCH 013/113] LocalDataCluster bind (#359) * Prevent LocalDataCluster to send bind, unbind, and configure_reporting requests * Apply comments from code review and fixed docstring --- zhaquirks/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 2013e59271..44d3a34700 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -33,6 +33,18 @@ def __init__(self, *args, **kwargs): class LocalDataCluster(CustomCluster): """Cluster meant to prevent remote calls.""" + async def bind(self): + """Prevent bind.""" + return (foundation.Status.SUCCESS,) + + async def unbind(self): + """Prevent unbind.""" + return (foundation.Status.SUCCESS,) + + async def _configure_reporting(self, *args, **kwargs): + """Prevent remote configure reporting.""" + return foundation.ConfigureReportingResponse.deserialize(b"\x00")[0] + async def read_attributes_raw(self, attributes, manufacturer=None): """Prevent remote reads.""" records = [ From 829bf0afdd1385da5eb492b6c636acc5325ce9a8 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 21 May 2020 10:39:17 -0400 Subject: [PATCH 014/113] IKEA 5 button remove dev version 2 quirk (#358) --- zhaquirks/ikea/fivebtnremotezha.py | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/zhaquirks/ikea/fivebtnremotezha.py b/zhaquirks/ikea/fivebtnremotezha.py index e08c44ff79..f95678f351 100644 --- a/zhaquirks/ikea/fivebtnremotezha.py +++ b/zhaquirks/ikea/fivebtnremotezha.py @@ -2,6 +2,7 @@ from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( + Alarms, Basic, Groups, Identify, @@ -11,6 +12,7 @@ PollControl, PowerConfiguration, ) +from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.lightlink import LightLink from .. import DoublingPowerConfigurationCluster @@ -166,3 +168,62 @@ class IkeaTradfriRemote(CustomDevice): ARGS: [3328, 0], }, } + + +class IkeaTradfriRemote2(IkeaTradfriRemote): + """Custom device representing IKEA of Sweden TRADFRI 5 button remote control.""" + + signature = { + # Date: Sun, 24 May 2020 00:16:12 +0200 Subject: [PATCH 015/113] Add third variant of Opple 6 button (#360) --- zhaquirks/xiaomi/aqara/opple_remote.py | 128 +++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/zhaquirks/xiaomi/aqara/opple_remote.py b/zhaquirks/xiaomi/aqara/opple_remote.py index 380a1f0218..1a0eab2810 100644 --- a/zhaquirks/xiaomi/aqara/opple_remote.py +++ b/zhaquirks/xiaomi/aqara/opple_remote.py @@ -792,3 +792,131 @@ class RemoteB686OPCN01V2(XiaomiCustomDevice): } device_automation_triggers = RemoteB686OPCN01.device_automation_triggers + + +class RemoteB686OPCN01V3(XiaomiCustomDevice): + """Aqara Opple 6 button remote device.""" + + signature = { + # + MODELS_INFO: [(LUMI, "lumi.remote.b686opcn01")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + PowerConfigurationCluster.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: [OnOff.cluster_id, Identify.cluster_id], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + MultistateInputCluster.cluster_id, + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + MultistateInputCluster.cluster_id, + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 5: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + MultistateInputCluster.cluster_id, + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 6: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + MultistateInputCluster.cluster_id, + Identify.cluster_id, + ], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + }, + } + + replacement = { + NODE_DESCRIPTOR: NodeDescriptor( + 0x02, 0x40, 0x80, 0x115F, 0x7F, 0x0064, 0x2C00, 0x0064, 0x00 + ), + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH, + INPUT_CLUSTERS: [ + BasicCluster, + Identify.cluster_id, + PowerConfigurationCluster, + OppleCluster, + MultistateInputCluster, + ], + 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: [OnOff.cluster_id, Identify.cluster_id], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [MultistateInputCluster, Identify.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 4: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [MultistateInputCluster, Identify.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 5: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [MultistateInputCluster, Identify.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 6: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [MultistateInputCluster, Identify.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + }, + } + + device_automation_triggers = RemoteB686OPCN01.device_automation_triggers From 38569bde11dce22eabcaf3f18f2180b37cec8be9 Mon Sep 17 00:00:00 2001 From: tube0013 Date: Thu, 11 Jun 2020 20:40:13 -0400 Subject: [PATCH 016/113] Add Quirk For Konke Contact Sensor - Battery (#361) * Add Quirk For Konke Contact Sensor for Battery Added a new quirk for the Konke 3AFE270104020015 contact sensor so it can report battery percentage in HA. Tested and working with sensor I just received recently. * Update magnet.py fix lint * Fix Lint * Run black * flake8 Co-authored-by: Alexei Chetroi --- zhaquirks/konke/magnet.py | 62 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 zhaquirks/konke/magnet.py diff --git a/zhaquirks/konke/magnet.py b/zhaquirks/konke/magnet.py new file mode 100644 index 0000000000..e96134e6f6 --- /dev/null +++ b/zhaquirks/konke/magnet.py @@ -0,0 +1,62 @@ +"""Konke magnet sensor.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.security import IasZone +from zigpy.zcl.clusters.general import Basic, Identify, PowerConfiguration + +from . import KONKE +from .. import PowerConfigurationCluster +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +KONKE_CLUSTER_ID = 0xFCC0 + + +class KonkeMagnet(CustomDevice): + """Custom device representing konke magnet sensors.""" + + signature = { + # + MODELS_INFO: [(KONKE, "3AFE270104020015")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + IasZone.cluster_id, + KONKE_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, KONKE_CLUSTER_ID], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfigurationCluster, + Identify.cluster_id, + IasZone.cluster_id, + KONKE_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, KONKE_CLUSTER_ID], + } + } + } From 994007cda43bcbdfd9969777d0c9672f5dbb4dd3 Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Fri, 12 Jun 2020 02:40:50 +0200 Subject: [PATCH 017/113] Add quirk for Eurotronic Spirit Zigbee (#373) * Add SPZB0001 quirk (initial) * Add SPZB0001 quirk (2) * SPZB001 cleanup * Fix style * Fix new line errors * Update zhaquirks/eurotronic/__init__.py Co-authored-by: Alexei Chetroi Co-authored-by: Alexei Chetroi --- zhaquirks/eurotronic/__init__.py | 148 +++++++++++++++++++++++++++++++ zhaquirks/eurotronic/spzb0001.py | 85 ++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 zhaquirks/eurotronic/__init__.py create mode 100644 zhaquirks/eurotronic/spzb0001.py diff --git a/zhaquirks/eurotronic/__init__.py b/zhaquirks/eurotronic/__init__.py new file mode 100644 index 0000000000..76c3211ca7 --- /dev/null +++ b/zhaquirks/eurotronic/__init__.py @@ -0,0 +1,148 @@ +"""Eurotronic devices.""" + +import logging + +import zigpy.types as types +from zigpy.quirks import CustomCluster +from zigpy.zcl import foundation +from zigpy.zcl.clusters.hvac import Thermostat + + +EUROTRONIC = "Eurotronic" + +THERMOSTAT_CHANNEL = "thermostat" + +MANUFACTURER = 0x1037 # 4151 + +OCCUPIED_HEATING_SETPOINT_ATTR = 0x0012 +CTRL_SEQ_OF_OPER_ATTR = 0x001B +SYSTEM_MODE_ATTR = 0x001C + +TRV_MODE_ATTR = 0x4000 +SET_VALVE_POS_ATTR = 0x4001 +ERRORS_ATTR = 0x4002 +CURRENT_TEMP_SETPOINT_ATTR = 0x4003 +HOST_FLAGS_ATTR = 0x4008 + + +# Host Flags +# unknown (defaults to 1) = 0b00000001 # 1 +MIRROR_SCREEN_FLAG = 0b00000010 # 2 +BOOST_FLAG = 0b00000100 # 4 +# unknown = 0b00001000 # 8 +CLR_OFF_MODE_FLAG = 0b00010000 # 16 +SET_OFF_MODE_FLAG = 0b00100000 # 32, reported back as 16 +# unknown = 0b01000000 # 64 +CHILD_LOCK_FLAG = 0b10000000 # 128 + + +_LOGGER = logging.getLogger(__name__) + + +class ThermostatCluster(CustomCluster, Thermostat): + """Thermostat cluster.""" + + cluster_id = Thermostat.cluster_id + + attributes = { + TRV_MODE_ATTR: ("trv_mode", types.enum8), + SET_VALVE_POS_ATTR: ("set_valve_position", types.uint8_t), + ERRORS_ATTR: ("errors", types.uint8_t), + CURRENT_TEMP_SETPOINT_ATTR: ("current_temperature_setpoint", types.int16s), + HOST_FLAGS_ATTR: ("host_flags", types.uint24_t), + } + attributes.update(Thermostat.attributes) + + def _update_attribute(self, attrid, value): + _LOGGER.debug("update attribute %04x to %s... ", attrid, value) + + if attrid == CURRENT_TEMP_SETPOINT_ATTR: + super()._update_attribute(OCCUPIED_HEATING_SETPOINT_ATTR, value) + elif attrid == HOST_FLAGS_ATTR: + if value & CLR_OFF_MODE_FLAG == CLR_OFF_MODE_FLAG: + super()._update_attribute(SYSTEM_MODE_ATTR, 0x0) + _LOGGER.debug("set system_mode to [off ]") + else: + super()._update_attribute(SYSTEM_MODE_ATTR, 0x4) + _LOGGER.debug("set system_mode to [heat]") + + _LOGGER.debug("update attribute %04x to %s... [ ok ]", attrid, value) + super()._update_attribute(attrid, value) + + async def read_attributes_raw(self, attributes, manufacturer=None): + """Override wrong attribute reports from the thermostat.""" + success = [] + error = [] + + if CTRL_SEQ_OF_OPER_ATTR in attributes: + rar = foundation.ReadAttributeRecord( + CTRL_SEQ_OF_OPER_ATTR, foundation.Status.SUCCESS, foundation.TypeValue() + ) + rar.value.value = 0x2 + success.append(rar) + + if SYSTEM_MODE_ATTR in attributes: + rar = foundation.ReadAttributeRecord( + SYSTEM_MODE_ATTR, foundation.Status.SUCCESS, foundation.TypeValue() + ) + rar.value.value = 0x4 + success.append(rar) + + if OCCUPIED_HEATING_SETPOINT_ATTR in attributes: + + _LOGGER.debug("intercepting OCC_HS") + + values = await super().read_attributes_raw( + [CURRENT_TEMP_SETPOINT_ATTR], manufacturer=MANUFACTURER + ) + + if len(values) == 2: + current_temp_setpoint = values[1][0] + current_temp_setpoint.attrid = OCCUPIED_HEATING_SETPOINT_ATTR + + error.extend(values[1]) + else: + current_temp_setpoint = values[0][0] + current_temp_setpoint.attrid = OCCUPIED_HEATING_SETPOINT_ATTR + + success.extend(values[0]) + + attributes = list( + filter( + lambda x: x + not in ( + CTRL_SEQ_OF_OPER_ATTR, + SYSTEM_MODE_ATTR, + OCCUPIED_HEATING_SETPOINT_ATTR, + ), + attributes, + ) + ) + + if attributes: + values = await super().read_attributes_raw(attributes, manufacturer) + + success.extend(values[0]) + + if len(values) == 2: + error.extend(values[1]) + + return success, error + + def write_attributes(self, attributes, manufacturer=None): + """Override wrong writes to thermostat attributes.""" + if "system_mode" in attributes: + + host_flags = self._attr_cache.get(HOST_FLAGS_ATTR, 1) + _LOGGER.debug("current host_flags: %s", host_flags) + + if attributes.get("system_mode") == 0x0: + return super().write_attributes( + {"host_flags": host_flags | SET_OFF_MODE_FLAG}, MANUFACTURER + ) + if attributes.get("system_mode") == 0x4: + return super().write_attributes( + {"host_flags": host_flags | CLR_OFF_MODE_FLAG}, MANUFACTURER + ) + + return super().write_attributes(attributes, manufacturer) diff --git a/zhaquirks/eurotronic/spzb0001.py b/zhaquirks/eurotronic/spzb0001.py new file mode 100644 index 0000000000..ef6441eb8e --- /dev/null +++ b/zhaquirks/eurotronic/spzb0001.py @@ -0,0 +1,85 @@ +"""Eurotronic Spirit Zigbee quirk.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + Ota, + PowerConfiguration, + Time, +) +from zigpy.zcl.clusters.hvac import Thermostat + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +from . import EUROTRONIC, ThermostatCluster + + +class SPZB0001(CustomDevice): + """Eurotronic Spirit Zigbee device.""" + + signature = { + # + MODELS_INFO: [(EUROTRONIC, "SPZB0001")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.THERMOSTAT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Thermostat.cluster_id, + Ota.cluster_id, + Time.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Thermostat.cluster_id, + Ota.cluster_id, + Time.cluster_id, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.THERMOSTAT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + ThermostatCluster, + Ota.cluster_id, + Time.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + ThermostatCluster, + Ota.cluster_id, + Time.cluster_id, + ], + } + } + } From 2bb13d46a70acd1441ba606090703c29ecea9890 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 12 Jun 2020 16:57:57 -0400 Subject: [PATCH 018/113] Fix plaid battery sensor. (#375) --- zhaquirks/plaid/soil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/plaid/soil.py b/zhaquirks/plaid/soil.py index a85fc142bd..635dda529a 100644 --- a/zhaquirks/plaid/soil.py +++ b/zhaquirks/plaid/soil.py @@ -25,7 +25,7 @@ class PowerConfigurationClusterMains(PowerConfigurationCluster): def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) if attrid == self.MAINS_VOLTAGE_ATTR: - super()._update_attribute(self.self.BATTERY_VOLTAGE_ATTR, value) + super()._update_attribute(self.BATTERY_VOLTAGE_ATTR, round(value / 100)) class SoilMoisture(CustomDevice): From f84a2e23cdbc9ba58a3f989fe513e7e6b9b6224c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 13 Jun 2020 15:47:36 -0400 Subject: [PATCH 019/113] Fix PowerConfig cluster reporting 0V (#376) --- zhaquirks/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 44d3a34700..08c4694421 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -154,15 +154,13 @@ class PowerConfigurationCluster(CustomCluster, PowerConfiguration): def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) - if attrid == self.BATTERY_VOLTAGE_ATTR: + if attrid == self.BATTERY_VOLTAGE_ATTR and value not in (0, 255): super()._update_attribute( self.BATTERY_PERCENTAGE_REMAINING, self._calculate_battery_percentage(value), ) def _calculate_battery_percentage(self, raw_value): - if raw_value in (0, 255): - return -1 volts = raw_value / 10 volts = max(volts, self.MIN_VOLTS) volts = min(volts, self.MAX_VOLTS) From b723eb3a18a6616e66718ac74037ee70d003f758 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 14 Jun 2020 10:49:25 -0400 Subject: [PATCH 020/113] Fix Plaid sensor battery quirk (#377) * Fix Plaid sensor battery quirk When reading or configuring `battery_voltage` attribute, configure `mains_voltage` attribute instead. * Pylint --- zhaquirks/plaid/soil.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/zhaquirks/plaid/soil.py b/zhaquirks/plaid/soil.py index 635dda529a..65a6720aff 100644 --- a/zhaquirks/plaid/soil.py +++ b/zhaquirks/plaid/soil.py @@ -27,6 +27,22 @@ def _update_attribute(self, attrid, value): if attrid == self.MAINS_VOLTAGE_ATTR: super()._update_attribute(self.BATTERY_VOLTAGE_ATTR, round(value / 100)) + def _remap(self, attr): + """Replace battery voltage attribute name/id with mains_voltage.""" + if attr in (self.BATTERY_VOLTAGE_ATTR, "battery_voltage"): + return self.MAINS_VOLTAGE_ATTR + return attr + + def read_attributes(self, attributes, *args, **kwargs): # pylint: disable=W0221 + """Replace battery voltage with mains voltage.""" + return super().read_attributes( + [self._remap(attr) for attr in attributes], *args, **kwargs + ) + + def configure_reporting(self, attribute, *args, **kwargs): # pylint: disable=W0221 + """Replace battery voltage with mains voltage.""" + return super().configure_reporting(self._remap(attribute), *args, **kwargs) + class SoilMoisture(CustomDevice): """Custom device representing plaid systems soil sensors.""" From b363b7946a0e4f654659096ab07a5b42f0002d29 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Sun, 14 Jun 2020 12:50:29 -0400 Subject: [PATCH 021/113] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e691f3d45b..0c38453727 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.39" +VERSION = "0.0.40" def readme(): From eb7bce2ebfb0c3c462bf191524ccdd0b79a95bab Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 18 Jun 2020 12:52:55 -0400 Subject: [PATCH 022/113] Add constant attributes to plaid soil sensor battery (#381) --- zhaquirks/plaid/soil.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zhaquirks/plaid/soil.py b/zhaquirks/plaid/soil.py index 65a6720aff..ae0a960d99 100644 --- a/zhaquirks/plaid/soil.py +++ b/zhaquirks/plaid/soil.py @@ -21,6 +21,9 @@ class PowerConfigurationClusterMains(PowerConfigurationCluster): """Common use power configuration cluster.""" MAINS_VOLTAGE_ATTR = 0x0000 + ATTR_ID_BATT_SIZE = 0x0031 + ATTR_ID_BATT_QTY = 0x0033 + _CONSTANT_ATTRIBUTES = {ATTR_ID_BATT_SIZE: 0x08, ATTR_ID_BATT_QTY: 1} def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) From b786861b1ae987aa66f022a85491257f0f0bd583 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 18 Jun 2020 12:54:08 -0400 Subject: [PATCH 023/113] Quirk for Xiaomi Aqara Smart LED lamp ZNLDP12LM (#382) --- zhaquirks/xiaomi/aqara/light_aqcn2.py | 109 ++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 zhaquirks/xiaomi/aqara/light_aqcn2.py diff --git a/zhaquirks/xiaomi/aqara/light_aqcn2.py b/zhaquirks/xiaomi/aqara/light_aqcn2.py new file mode 100644 index 0000000000..9f0bfff93c --- /dev/null +++ b/zhaquirks/xiaomi/aqara/light_aqcn2.py @@ -0,0 +1,109 @@ +"""Quirk for Xiaomi Aqara Smart LED bulb ZNLDP12LM.""" +import logging + +from zigpy.profiles import zha +from zigpy.zcl.clusters.closures import WindowCovering +from zigpy.zcl.clusters.general import ( + AnalogOutput, + Groups, + Identify, + LevelControl, + MultistateOutput, + OnOff, + Ota, + PowerConfiguration, + Scenes, + Time, +) +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.measurement import ( + OccupancySensing, + PressureMeasurement, + RelativeHumidity, + TemperatureMeasurement, +) + +from .. import LUMI, BasicCluster, XiaomiCustomDevice +from ... import CustomCluster +from ...const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +_LOGGER = logging.getLogger(__name__) + + +class LightAqcn02(XiaomiCustomDevice): + """Custom device for ZNLDP12LM.""" + + class ColorCluster(CustomCluster, Color): + """Color Cluster.""" + + _CONSTANT_ATTRIBUTES = {0x400A: 0x0010, 0x400B: 153, 0x400C: 370} + + signature = { + # endpoint=1 profile=260 device_type=258 device_version=1 input_clusters=[0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026] output_clusters=[25, 10, 13, 258, 19, 6, 1, 1030, 8, 768]> + MODELS_INFO: [(LUMI, "lumi.light.aqcn02")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + BasicCluster.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Time.cluster_id, + AnalogOutput.cluster_id, + MultistateOutput.cluster_id, + WindowCovering.cluster_id, + Color.cluster_id, + TemperatureMeasurement.cluster_id, + PressureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + OccupancySensing.cluster_id, + ], + OUTPUT_CLUSTERS: [ + PowerConfiguration.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Time.cluster_id, + AnalogOutput.cluster_id, + MultistateOutput.cluster_id, + Ota.cluster_id, + WindowCovering.cluster_id, + ColorCluster.cluster_id, + OccupancySensing.cluster_id, + ], + } + }, + } + replacement = { + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + BasicCluster, # 0 + Groups.cluster_id, # 4 + Identify.cluster_id, # 3 + Scenes.cluster_id, # 5 + OnOff.cluster_id, # 6 + PowerConfiguration.cluster_id, # 1 + LevelControl.cluster_id, # 8 + ColorCluster, # 768 + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, # 10 + Ota.cluster_id, # 19 + OccupancySensing.cluster_id, # 1030 + ], + } + } + } From 25f060a24a1205875bb4e0b4836dcb213598ac9a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 26 Jun 2020 15:08:51 -0400 Subject: [PATCH 024/113] New signature for Aqara temp/humid/pressure sensor (#386) --- zhaquirks/xiaomi/aqara/weather.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/zhaquirks/xiaomi/aqara/weather.py b/zhaquirks/xiaomi/aqara/weather.py index b6e58e39b2..1551d7e93d 100644 --- a/zhaquirks/xiaomi/aqara/weather.py +++ b/zhaquirks/xiaomi/aqara/weather.py @@ -89,3 +89,34 @@ def __init__(self, *args, **kwargs): } }, } + + +class Weather2(Weather): + """New Xiaomi weather sensor device.""" + + signature = { + # + MODELS_INFO: [(LUMI, "lumi.weather")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + BasicCluster.cluster_id, + Identify.cluster_id, + TemperatureMeasurementCluster.cluster_id, + PressureMeasurement.cluster_id, + RelativeHumidityCluster.cluster_id, + XIAOMI_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + BasicCluster.cluster_id, + Groups.cluster_id, + XIAOMI_CLUSTER_ID, + ], + } + }, + } From cb820ebbcf0d5f044bde8997aed09091486dbce8 Mon Sep 17 00:00:00 2001 From: Hedda Date: Sun, 28 Jun 2020 15:08:38 +0200 Subject: [PATCH 025/113] Updated README.md with info on how-to test new releases. (#338) Updated README.md with info on how-to test new releases. Copied this text from zigpy-cc so credits go to @sanyatuning --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 71ce3c5a52..65a40049f0 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,28 @@ Please refer to [xbee.md](xbee.md) for details on configuration and usage exampl - All supported devices report battery level +# Testing new releases + +Testing a new release of the zha-quirks package before it is released in Home Assistant. + +If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io distro): +- Add https://github.com/home-assistant/hassio-addons-development as "add-on" repository +- Install "Custom deps deployment" addon +- Update config like: + ``` + pypi: + - zha-quirks==0.0.38 + apk: [] + ``` + where 0.0.38 is the new version +- Start the addon + +If you are instead using some custom python installation of Home Assistant then do this: +- Activate your python virtual env +- Update package with ``pip`` + ``` + pip install zha-quirks==0.0.38 + # Thanks - Special thanks to damarco for the majority of the device tracker code From 368dcb058c40ec651269595e88c6ae6be7a372c0 Mon Sep 17 00:00:00 2001 From: Christopher Masto Date: Mon, 29 Jun 2020 09:15:24 -0400 Subject: [PATCH 026/113] Support older SmartThings sensors (multi and motion) (#390) * SmartThings SmartSense Multi Sensor (PGC313) quirk * Refactor SmartThings to support broken IasZone implementation This seems to be common on some of their earliest sensors. Thanks to @puddly on the Discord for this code. * SmartThings SmartSense Motion Sensor (PGC314) quirk * Fix lint errors --- zhaquirks/smartthings/__init__.py | 18 ++++++++++ zhaquirks/smartthings/pgc313.py | 59 +++++++++++++++++++++++++++++++ zhaquirks/smartthings/pgc314.py | 59 +++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100755 zhaquirks/smartthings/pgc313.py create mode 100644 zhaquirks/smartthings/pgc314.py diff --git a/zhaquirks/smartthings/__init__.py b/zhaquirks/smartthings/__init__.py index 66bdbeef64..65cf6893c0 100644 --- a/zhaquirks/smartthings/__init__.py +++ b/zhaquirks/smartthings/__init__.py @@ -2,6 +2,7 @@ import zigpy.types as t from zigpy.quirks import CustomCluster +from zigpy.zcl.clusters.security import IasZone SMART_THINGS = "SmartThings" MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFC02 # decimal = 64514 @@ -24,3 +25,20 @@ class SmartThingsAccelCluster(CustomCluster): client_commands = {} server_commands = {} + + +class SmartThingsIasZone(CustomCluster, IasZone): + """IasZone cluster patched to support SmartThings spec violations.""" + + client_commands = IasZone.client_commands.copy() + client_commands[0x0000] = ( + "status_change_notification", + ( + IasZone.ZoneStatus, + t.bitmap8, + # SmartThings never sends these two + t.Optional(t.uint8_t), + t.Optional(t.uint16_t), + ), + False, + ) diff --git a/zhaquirks/smartthings/pgc313.py b/zhaquirks/smartthings/pgc313.py new file mode 100755 index 0000000000..863a085c8c --- /dev/null +++ b/zhaquirks/smartthings/pgc313.py @@ -0,0 +1,59 @@ +"""SmartThings SmartSense Multi Sensor quirk.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Ota + +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from . import SMART_THINGS, SmartThingsIasZone + +SMARTSENSE_MULTI_DEVICE_TYPE = 0x0139 # decimal = 313 + + +class IasZoneContactSwitchCluster(SmartThingsIasZone): + """Custom IasZone cluster.""" + + ZONE_TYPE = 0x0001 + CONTACT_SWITCH_TYPE = 0x0015 + _CONSTANT_ATTRIBUTES = {ZONE_TYPE: CONTACT_SWITCH_TYPE} + + +class SmartthingsSmartSenseMultiSensor(CustomDevice): + """Multipurpose sensor.""" + + signature = { + # + MODELS_INFO: [(SMART_THINGS, "PGC313")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: SMARTSENSE_MULTI_DEVICE_TYPE, + INPUT_CLUSTERS: [Basic.cluster_id], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + # + 2: { + PROFILE_ID: 0xFC01, # decimal = 64513 + DEVICE_TYPE: SMARTSENSE_MULTI_DEVICE_TYPE, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [Basic.cluster_id, IasZoneContactSwitchCluster], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + } + } diff --git a/zhaquirks/smartthings/pgc314.py b/zhaquirks/smartthings/pgc314.py new file mode 100644 index 0000000000..7c1f194e50 --- /dev/null +++ b/zhaquirks/smartthings/pgc314.py @@ -0,0 +1,59 @@ +"""SmartThings SmartSense Motion quirk.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Ota + +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from . import SMART_THINGS, SmartThingsIasZone + +SMARTSENSE_MOTION_DEVICE_TYPE = 0x013A # decimal = 314 + + +class IasZoneMotionCluster(SmartThingsIasZone): + """Custom IasZone cluster.""" + + ZONE_TYPE = 0x0001 + MOTION_TYPE = 0x000D + _CONSTANT_ATTRIBUTES = {ZONE_TYPE: MOTION_TYPE} + + +class SmartthingsSmartSenseMotionSensor(CustomDevice): + """SmartSense Motion Sensor.""" + + signature = { + # + MODELS_INFO: [(SMART_THINGS, "PGC314")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: SMARTSENSE_MOTION_DEVICE_TYPE, + INPUT_CLUSTERS: [Basic.cluster_id], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + # + 2: { + PROFILE_ID: 0xFC01, # decimal = 64513 + DEVICE_TYPE: SMARTSENSE_MOTION_DEVICE_TYPE, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [Basic.cluster_id, IasZoneMotionCluster], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + } + } From dfa710d9f934c09b36ad9b3f8c2a0e67b20b5bf3 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Jun 2020 11:46:01 -0400 Subject: [PATCH 027/113] Add quirk for Phillips SML002 (#391) * add quirk for phillips sml002 * fix scenes cluster --- zhaquirks/philips/__init__.py | 1 + zhaquirks/philips/sml002.py | 115 ++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 zhaquirks/philips/sml002.py diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 3210b3d36a..556f8b4cc3 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -1 +1,2 @@ """Module for Phillips quirks implementations.""" +PHILLIPS = "Philips" diff --git a/zhaquirks/philips/sml002.py b/zhaquirks/philips/sml002.py new file mode 100644 index 0000000000..a24b8d4184 --- /dev/null +++ b/zhaquirks/philips/sml002.py @@ -0,0 +1,115 @@ +"""Quirk for Phillips SML002.""" +from zigpy.profiles import zha, zll +from zigpy.quirks import CustomCluster, CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + PowerConfiguration, + Scenes, +) +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.measurement import ( + IlluminanceMeasurement, + OccupancySensing, + TemperatureMeasurement, +) + +from . import PHILLIPS +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class OccupancyCluster(CustomCluster, OccupancySensing): + """Phillips occupancy cluster.""" + + attributes = OccupancySensing.attributes.copy() + attributes.update({0x0030: ("sensitivity", t.uint8_t)}) + attributes.update({0x0031: ("sensitivity_max", t.uint8_t)}) + + +class PhilipsSML002(CustomDevice): + """Phillips SML002 device.""" + + signature = { + MODELS_INFO: [(PHILLIPS, "SML002")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.ON_OFF_SENSOR, + INPUT_CLUSTERS: [Basic.cluster_id], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + }, + # + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + IlluminanceMeasurement.cluster_id, + TemperatureMeasurement.cluster_id, + OccupancySensing.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.ON_OFF_SENSOR, + INPUT_CLUSTERS: [Basic.cluster_id], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + IlluminanceMeasurement.cluster_id, + TemperatureMeasurement.cluster_id, + OccupancyCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + } + } From 21e40c90c228515d3f39aee14a915e332f80ff0e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Jun 2020 11:57:34 -0400 Subject: [PATCH 028/113] alternate legrand dimmer implementation (#392) --- zhaquirks/legrand/dimmer.py | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/zhaquirks/legrand/dimmer.py b/zhaquirks/legrand/dimmer.py index 5ac196eeed..426839932f 100644 --- a/zhaquirks/legrand/dimmer.py +++ b/zhaquirks/legrand/dimmer.py @@ -93,3 +93,42 @@ class DimmerWithoutNeutral(CustomDevice): } } } + + +class DimmerWithoutNeutral2(DimmerWithoutNeutral): + """Dimmer switch w/o neutral 2.""" + + signature = { + # + MODELS_INFO: [(f" {LEGRAND}", " Dimmer switch w/o neutral")], + 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, + OnOff.cluster_id, + LevelControl.cluster_id, + Scenes.cluster_id, + BinaryInput.cluster_id, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + Ota.cluster_id, + ], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 0x0061, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [0x0021], + }, + }, + } From 531b7d448c179e8e7abe5b164fae24038f72234b Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Mon, 29 Jun 2020 16:29:51 -0400 Subject: [PATCH 029/113] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0c38453727..aed13448ba 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.40" +VERSION = "0.0.41" def readme(): From 8e1647a3be5eba81a58c75e9898e22b15be5b8cb Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Jun 2020 17:09:26 -0400 Subject: [PATCH 030/113] Fix spelling of Philips (#393) * fix spelling * more spelling * more spelling * more spelling --- zhaquirks/philips/__init__.py | 4 ++-- zhaquirks/philips/rom001.py | 4 ++-- zhaquirks/philips/sml002.py | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 556f8b4cc3..57f9b57dd0 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -1,2 +1,2 @@ -"""Module for Phillips quirks implementations.""" -PHILLIPS = "Philips" +"""Module for Philips quirks implementations.""" +PHILIPS = "Philips" diff --git a/zhaquirks/philips/rom001.py b/zhaquirks/philips/rom001.py index d517c0c86b..ad34786796 100644 --- a/zhaquirks/philips/rom001.py +++ b/zhaquirks/philips/rom001.py @@ -1,4 +1,4 @@ -"""Phillips ROM001 device.""" +"""Philips ROM001 device.""" from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( @@ -31,7 +31,7 @@ class PhilipsROM001(CustomDevice): - """Phillips ROM001 device.""" + """Philips ROM001 device.""" signature = { # Date: Fri, 3 Jul 2020 07:33:59 -0500 Subject: [PATCH 031/113] Added Philips LCA003 and LCT016 for PowerOnState (#395) * added Philips LCT016 * simpler enum name * Fix missing blank lines after class docstring * Fix missing global docstring * Reformatted with black * actually lca003 * fix crazy mistakes; add actual lct016 as well * Remove unused imports * moved common classes to __init__.py --- zhaquirks/philips/__init__.py | 19 +++++++ zhaquirks/philips/lca003.py | 95 +++++++++++++++++++++++++++++++++++ zhaquirks/philips/lct016.py | 94 ++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 zhaquirks/philips/lca003.py create mode 100644 zhaquirks/philips/lct016.py diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 57f9b57dd0..16156f484b 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -1,2 +1,21 @@ """Module for Philips quirks implementations.""" +from zigpy.quirks import CustomCluster +from zigpy.zcl.clusters.general import OnOff +import zigpy.types as t + PHILIPS = "Philips" + + +class PowerOnState(t.enum8): + """Philips power on state enum.""" + + Off = 0x00 + On = 0x01 + LastState = 0xFF + + +class PhilipsOnOffCluster(CustomCluster, OnOff): + """Philips OnOff cluster.""" + + attributes = OnOff.attributes.copy() + attributes.update({0x4003: ("power_on_state", PowerOnState)}) diff --git a/zhaquirks/philips/lca003.py b/zhaquirks/philips/lca003.py new file mode 100644 index 0000000000..f027c1af3d --- /dev/null +++ b/zhaquirks/philips/lca003.py @@ -0,0 +1,95 @@ +"""Quirk for Phillips LCA003.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + OnOff, + Basic, + Identify, + LevelControl, + Scenes, + Groups, + Ota, + GreenPowerProxy, +) + +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from zhaquirks.const import ( + ENDPOINTS, + OUTPUT_CLUSTERS, + INPUT_CLUSTERS, + DEVICE_TYPE, + PROFILE_ID, + MODELS_INFO, +) +from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster + + +class PhilipsLCA003(CustomDevice): + """Philips LCA003 device.""" + + signature = { + MODELS_INFO: [(PHILIPS, "LCA003")], + ENDPOINTS: { + 11: { + # + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.EXTENDED_COLOR_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + 64514, + Color.cluster_id, + 64513, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + 242: { + # + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 11: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.EXTENDED_COLOR_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + PhilipsOnOffCluster, + LevelControl.cluster_id, + LightLink.cluster_id, + 64514, + Color.cluster_id, + 64513, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + } + } diff --git a/zhaquirks/philips/lct016.py b/zhaquirks/philips/lct016.py new file mode 100644 index 0000000000..8905e9438e --- /dev/null +++ b/zhaquirks/philips/lct016.py @@ -0,0 +1,94 @@ +"""Quirk for Phillips LCT016.""" +from zigpy.profiles import zll +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + OnOff, + Basic, + Identify, + LevelControl, + Scenes, + Groups, + Ota, + GreenPowerProxy, +) + +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from zhaquirks.const import ( + ENDPOINTS, + OUTPUT_CLUSTERS, + INPUT_CLUSTERS, + DEVICE_TYPE, + PROFILE_ID, + MODELS_INFO, +) +from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster + + +class PhilipsLCT016(CustomDevice): + """Philips LCT016 device.""" + + signature = { + MODELS_INFO: [(PHILIPS, "LCT016")], + ENDPOINTS: { + 11: { + # Date: Fri, 3 Jul 2020 08:49:41 -0400 Subject: [PATCH 032/113] [BREAKING CHANGE] Phillips Hue remote manufacturer specific cluster handling (#388) * hack on phillips cluster * add manufacturer support to phillips remotes * clean up code duplication * remove logger * update device triggers * fix spelling --- zhaquirks/philips/__init__.py | 96 ++++++++++++++++++++++++++++++++++- zhaquirks/philips/rwl020.py | 70 ++++--------------------- zhaquirks/philips/rwl021.py | 70 ++++--------------------- 3 files changed, 113 insertions(+), 123 deletions(-) diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 16156f484b..7f364f4747 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -1,9 +1,49 @@ """Module for Philips quirks implementations.""" +import logging + from zigpy.quirks import CustomCluster -from zigpy.zcl.clusters.general import OnOff import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, OnOff + +from ..const import ( + ARGS, + BUTTON, + COMMAND, + COMMAND_ID, + DIM_DOWN, + DIM_UP, + LONG_PRESS, + LONG_RELEASE, + PRESS_TYPE, + SHORT_PRESS, + SHORT_RELEASE, + TURN_OFF, + TURN_ON, + ZHA_SEND_EVENT, +) +DIAGNOSTICS_CLUSTER_ID = 0x0B05 # decimal = 2821 PHILIPS = "Philips" +_LOGGER = logging.getLogger(__name__) + +HUE_REMOTE_DEVICE_TRIGGERS = { + (SHORT_PRESS, TURN_ON): {COMMAND: "on_press"}, + (SHORT_PRESS, TURN_OFF): {COMMAND: "off_press"}, + (SHORT_PRESS, DIM_UP): {COMMAND: "up_press"}, + (SHORT_PRESS, DIM_DOWN): {COMMAND: "down_press"}, + (LONG_PRESS, TURN_ON): {COMMAND: "on_hold"}, + (LONG_PRESS, TURN_OFF): {COMMAND: "off_hold"}, + (LONG_PRESS, DIM_UP): {COMMAND: "up_hold"}, + (LONG_PRESS, DIM_DOWN): {COMMAND: "down_hold"}, + (SHORT_RELEASE, TURN_ON): {COMMAND: "on_short_release"}, + (SHORT_RELEASE, TURN_OFF): {COMMAND: "off_short_release"}, + (SHORT_RELEASE, DIM_UP): {COMMAND: "up_short_release"}, + (SHORT_RELEASE, DIM_DOWN): {COMMAND: "down_short_release"}, + (LONG_RELEASE, TURN_ON): {COMMAND: "on_long_release"}, + (LONG_RELEASE, TURN_OFF): {COMMAND: "off_long_release"}, + (LONG_RELEASE, DIM_UP): {COMMAND: "up_long_release"}, + (LONG_RELEASE, DIM_DOWN): {COMMAND: "down_long_release"}, +} class PowerOnState(t.enum8): @@ -19,3 +59,57 @@ class PhilipsOnOffCluster(CustomCluster, OnOff): attributes = OnOff.attributes.copy() attributes.update({0x4003: ("power_on_state", PowerOnState)}) + + +class PhilipsBasicCluster(CustomCluster, Basic): + """Philips Basic cluster.""" + + attributes = Basic.attributes.copy() + attributes.update({0x0031: ("philips", t.bitmap16)}) + + attr_config = {0x0031: 0x000B} + + async def bind(self): + """Bind cluster.""" + result = await super().bind() + await self.write_attributes(self.attr_config, manufacturer=0x100B) + return result + + +class PhilipsRemoteCluster(CustomCluster): + """Philips remote cluster.""" + + cluster_id = 64512 + name = "PhilipsRemoteCluster" + ep_attribute = "philips_remote_cluster" + attributes = {} + server_commands = {} + client_commands = { + 0x0000: ( + "notification", + (t.uint8_t, t.uint24_t, t.uint8_t, t.uint8_t, t.uint8_t, t.uint8_t), + False, + ) + } + BUTTONS = {1: "on", 2: "up", 3: "down", 4: "off"} + PRESS_TYPES = {0: "press", 1: "hold", 2: "short_release", 3: "long_release"} + + def handle_cluster_request(self, tsn, command_id, args): + """Handle the cluster command.""" + _LOGGER.debug( + "PhilipsRemoteCluster - handle_cluster_request tsn: [%s] command id: %s - args: [%s]", + tsn, + command_id, + args, + ) + button = self.BUTTONS.get(args[0], args[0]) + press_type = self.PRESS_TYPES.get(args[2], args[2]) + + event_args = { + BUTTON: button, + PRESS_TYPE: press_type, + COMMAND_ID: command_id, + ARGS: args, + } + action = "{}_{}".format(button, press_type) + self.listener_event(ZHA_SEND_EVENT, action, event_args) diff --git a/zhaquirks/philips/rwl020.py b/zhaquirks/philips/rwl020.py index c0dbc537e0..9af7aecdcb 100644 --- a/zhaquirks/philips/rwl020.py +++ b/zhaquirks/philips/rwl020.py @@ -1,7 +1,7 @@ -"""Phillips RWL020 device.""" +"""Philips RWL020 device.""" + from zigpy.profiles import zha, zll -from zigpy.quirks import CustomCluster, CustomDevice -import zigpy.types as t +from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, BinaryInput, @@ -13,39 +13,14 @@ PowerConfiguration, ) -from ..const import ( - ARGS, - CLUSTER_ID, - COMMAND, - COMMAND_OFF_WITH_EFFECT, - COMMAND_ON, - COMMAND_STEP, - DEVICE_TYPE, - DIM_DOWN, - DIM_UP, - ENDPOINT_ID, - ENDPOINTS, - INPUT_CLUSTERS, - LONG_PRESS, - OUTPUT_CLUSTERS, - PROFILE_ID, - SHORT_PRESS, - TURN_OFF, - TURN_ON, -) +from . import HUE_REMOTE_DEVICE_TRIGGERS, PhilipsBasicCluster, PhilipsRemoteCluster +from ..const import DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, OUTPUT_CLUSTERS, PROFILE_ID DIAGNOSTICS_CLUSTER_ID = 0x0B05 # decimal = 2821 -class BasicCluster(CustomCluster, Basic): - """Centralite acceleration cluster.""" - - attributes = Basic.attributes.copy() - attributes.update({0x0031: ("phillips", t.bitmap16)}) - - class PhilipsRWL020(CustomDevice): - """Phillips RWL020 device.""" + """Philips RWL020 device.""" signature = { # Date: Fri, 3 Jul 2020 09:00:11 -0400 Subject: [PATCH 033/113] initial tradfri blind quirks (#269) * initial tradfri blind quirks * sort imports and run black Co-authored-by: David Mulcahey --- zhaquirks/ikea/blinds.py | 79 +++++++++++++++++++++++++++ zhaquirks/ikea/opencloseremote.py | 89 +++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 zhaquirks/ikea/blinds.py create mode 100644 zhaquirks/ikea/opencloseremote.py diff --git a/zhaquirks/ikea/blinds.py b/zhaquirks/ikea/blinds.py new file mode 100644 index 0000000000..3ef5786dde --- /dev/null +++ b/zhaquirks/ikea/blinds.py @@ -0,0 +1,79 @@ +"""Device handler for IKEA of Sweden TRADFRI Fyrtur blinds.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.closures import WindowCovering +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + Ota, + PollControl, + PowerConfiguration, + Scenes, +) +from zigpy.zcl.clusters.lightlink import LightLink + +from . import IKEA +from .. import DoublingPowerConfigurationCluster +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 + + +class IkeaTradfriRollerBlinds(CustomDevice): + """Custom device representing IKEA of Sweden TRADFRI Fyrtur blinds.""" + + signature = { + # + MODELS_INFO: [ + (IKEA, "FYRTUR block-out roller blind"), + (IKEA, "KADRILJ roller blind"), + ], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + PollControl.cluster_id, + WindowCovering.cluster_id, + LightLink.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, LightLink.cluster_id], + } + }, + } + + replacement = { + "endpoints": { + 1: { + "profile_id": zha.PROFILE_ID, + "device_type": zha.DeviceType.WINDOW_COVERING_DEVICE, + "input_clusters": [ + Basic.cluster_id, + DoublingPowerConfigurationCluster, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + PollControl.cluster_id, + WindowCovering.cluster_id, + LightLink.cluster_id, + ], + "output_clusters": [Ota.cluster_id, LightLink.cluster_id], + } + } + } diff --git a/zhaquirks/ikea/opencloseremote.py b/zhaquirks/ikea/opencloseremote.py new file mode 100644 index 0000000000..9a6495c5ea --- /dev/null +++ b/zhaquirks/ikea/opencloseremote.py @@ -0,0 +1,89 @@ +"""Device handler for IKEA of Sweden TRADFRI remote control.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.closures import WindowCovering +from zigpy.zcl.clusters.general import ( + Alarms, + Basic, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + PollControl, + PowerConfiguration, +) +from zigpy.zcl.clusters.lightlink import LightLink + +from . import IKEA +from .. import DoublingPowerConfigurationCluster +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 + + +class IkeaTradfriOpenCloseRemote(CustomDevice): + """Custom device representing IKEA of Sweden TRADFRI remote control.""" + + signature = { + MODELS_INFO: [("\x02KE", "TRADFRI open/close remote")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_CONTROLLER, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Alarms.cluster_id, + PollControl.cluster_id, + LightLink.cluster_id, + IKEA_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Ota.cluster_id, + WindowCovering.cluster_id, + LightLink.cluster_id, + ], + } + }, + } + + replacement = { + MODELS_INFO: [(IKEA, "TRADFRI open/close remote")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_CONTROLLER, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DoublingPowerConfigurationCluster, + Identify.cluster_id, + Alarms.cluster_id, + PollControl.cluster_id, + LightLink.cluster_id, + IKEA_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Ota.cluster_id, + WindowCovering.cluster_id, + LightLink.cluster_id, + ], + } + }, + } From 50f9b4af284567c9627c86c32872c0ac375bc097 Mon Sep 17 00:00:00 2001 From: Jeremy Marzka Date: Mon, 6 Jul 2020 06:45:16 -0500 Subject: [PATCH 034/113] Added power on state for Philips Hue LCB001, LCT011, LCT024, LST002 (#396) * Remove extra cluster from LCT016 * Added power on state for LCB001, LCT011, LCT024, LST002 --- zhaquirks/philips/lcb001.py | 95 +++++++++++++++++++++++++++++++++++++ zhaquirks/philips/lct011.py | 93 ++++++++++++++++++++++++++++++++++++ zhaquirks/philips/lct016.py | 1 - zhaquirks/philips/lct024.py | 93 ++++++++++++++++++++++++++++++++++++ zhaquirks/philips/lst002.py | 93 ++++++++++++++++++++++++++++++++++++ 5 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 zhaquirks/philips/lcb001.py create mode 100644 zhaquirks/philips/lct011.py create mode 100644 zhaquirks/philips/lct024.py create mode 100644 zhaquirks/philips/lst002.py diff --git a/zhaquirks/philips/lcb001.py b/zhaquirks/philips/lcb001.py new file mode 100644 index 0000000000..4783d1a674 --- /dev/null +++ b/zhaquirks/philips/lcb001.py @@ -0,0 +1,95 @@ +"""Quirk for Phillips LCB001.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + OnOff, + Basic, + Identify, + LevelControl, + Scenes, + Groups, + Ota, + GreenPowerProxy, +) + +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from zhaquirks.const import ( + ENDPOINTS, + OUTPUT_CLUSTERS, + INPUT_CLUSTERS, + DEVICE_TYPE, + PROFILE_ID, + MODELS_INFO, +) +from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster + + +class PhilipsLCB001(CustomDevice): + """Philips LCB001 device.""" + + signature = { + MODELS_INFO: [(PHILIPS, "LCB001")], + ENDPOINTS: { + 11: { + # + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.EXTENDED_COLOR_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + 64514, + Color.cluster_id, + 64513, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + 242: { + # + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 11: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.EXTENDED_COLOR_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + PhilipsOnOffCluster, + LevelControl.cluster_id, + LightLink.cluster_id, + 64514, + Color.cluster_id, + 64513, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + } + } diff --git a/zhaquirks/philips/lct011.py b/zhaquirks/philips/lct011.py new file mode 100644 index 0000000000..48e44de95f --- /dev/null +++ b/zhaquirks/philips/lct011.py @@ -0,0 +1,93 @@ +"""Quirk for Phillips LCT011.""" +from zigpy.profiles import zll +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + OnOff, + Basic, + Identify, + LevelControl, + Scenes, + Groups, + Ota, + GreenPowerProxy, +) + +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from zhaquirks.const import ( + ENDPOINTS, + OUTPUT_CLUSTERS, + INPUT_CLUSTERS, + DEVICE_TYPE, + PROFILE_ID, + MODELS_INFO, +) +from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster + + +class PhilipsLCT011(CustomDevice): + """Philips LCT011 device.""" + + signature = { + MODELS_INFO: [(PHILIPS, "LCT011")], + ENDPOINTS: { + 11: { + # Date: Wed, 8 Jul 2020 10:51:22 -0400 Subject: [PATCH 035/113] Refactor "manufacturer specific" attributes and commands declaration (#400) * Syncup with zigpy manufacturer cluster changes * Use manufacturer_* attributes for CustomClusters * Syncup with upstream. * Update minimum zigpy version requirement * Manufacturer id override for xiaomi vibration * Pylint --- setup.py | 2 +- zhaquirks/__init__.py | 2 +- zhaquirks/centralite/3310S.py | 4 +--- zhaquirks/centralite/__init__.py | 5 +---- zhaquirks/eurotronic/__init__.py | 5 +---- zhaquirks/ikea/__init__.py | 16 +++++-------- zhaquirks/ledvance/__init__.py | 4 +--- zhaquirks/legrand/dimmer.py | 4 +--- zhaquirks/osram/__init__.py | 4 +--- zhaquirks/osram/lightifyx4.py | 4 +--- zhaquirks/philips/__init__.py | 10 +++------ zhaquirks/philips/sml002.py | 7 +++--- zhaquirks/sinope/thermostat.py | 7 ++---- zhaquirks/smartthings/__init__.py | 30 ++++++++++++------------- zhaquirks/waxman/leaksmart.py | 8 +++---- zhaquirks/xiaomi/aqara/opple_remote.py | 2 +- zhaquirks/xiaomi/aqara/vibration_aq1.py | 8 +++---- zhaquirks/xiaomi/mija/smoke.py | 9 ++++---- 18 files changed, 50 insertions(+), 81 deletions(-) diff --git a/setup.py b/setup.py index aed13448ba..ead217a9e4 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,6 @@ def readme(): keywords="zha quirks homeassistant hass", packages=find_packages(exclude=["*.tests"]), python_requires=">=3", - install_requires=["zigpy>=0.20.0"], + install_requires=["zigpy>=0.22.0"], tests_require=["pytest"], ) diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 08c4694421..2536ff0fff 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -41,7 +41,7 @@ async def unbind(self): """Prevent unbind.""" return (foundation.Status.SUCCESS,) - async def _configure_reporting(self, *args, **kwargs): + async def _configure_reporting(self, *args, **kwargs): # pylint: disable=W0221 """Prevent remote configure reporting.""" return foundation.ConfigureReportingResponse.deserialize(b"\x00")[0] diff --git a/zhaquirks/centralite/3310S.py b/zhaquirks/centralite/3310S.py index cb291a85c2..198002c50a 100755 --- a/zhaquirks/centralite/3310S.py +++ b/zhaquirks/centralite/3310S.py @@ -26,12 +26,10 @@ class SmartthingsRelativeHumidityCluster(CustomCluster): cluster_id = SMRT_THINGS_REL_HUM_CLSTR name = "Smartthings Relative Humidity Measurement" ep_attribute = "humidity" - attributes = { + manufacturer_attributes = { # Relative Humidity Measurement Information 0x0000: ("measured_value", t.int16s) } - server_commands = {} - client_commands = {} class CentraLite3310S(CustomDevice): diff --git a/zhaquirks/centralite/__init__.py b/zhaquirks/centralite/__init__.py index a1eab73f7c..aad61e221a 100755 --- a/zhaquirks/centralite/__init__.py +++ b/zhaquirks/centralite/__init__.py @@ -14,7 +14,7 @@ class CentraLiteAccelCluster(CustomCluster): cluster_id = 0xFC02 name = "CentraLite Accelerometer" ep_attribute = "accelerometer" - attributes = { + manufacturer_attributes = { 0x0000: ("motion_threshold_multiplier", t.uint8_t), 0x0002: ("motion_threshold", t.uint16_t), 0x0010: ("acceleration", t.bitmap8), # acceleration detected @@ -22,6 +22,3 @@ class CentraLiteAccelCluster(CustomCluster): 0x0013: ("y_axis", t.int16s), 0x0014: ("z_axis", t.int16s), } - - client_commands = {} - server_commands = {} diff --git a/zhaquirks/eurotronic/__init__.py b/zhaquirks/eurotronic/__init__.py index 76c3211ca7..2cdc2b19d9 100644 --- a/zhaquirks/eurotronic/__init__.py +++ b/zhaquirks/eurotronic/__init__.py @@ -42,16 +42,13 @@ class ThermostatCluster(CustomCluster, Thermostat): """Thermostat cluster.""" - cluster_id = Thermostat.cluster_id - - attributes = { + manufacturer_attributes = { TRV_MODE_ATTR: ("trv_mode", types.enum8), SET_VALVE_POS_ATTR: ("set_valve_position", types.uint8_t), ERRORS_ATTR: ("errors", types.uint8_t), CURRENT_TEMP_SETPOINT_ATTR: ("current_temperature_setpoint", types.int16s), HOST_FLAGS_ATTR: ("host_flags", types.uint24_t), } - attributes.update(Thermostat.attributes) def _update_attribute(self, attrid, value): _LOGGER.debug("update attribute %04x to %s... ", attrid, value) diff --git a/zhaquirks/ikea/__init__.py b/zhaquirks/ikea/__init__.py index d02ba520c5..181219975a 100644 --- a/zhaquirks/ikea/__init__.py +++ b/zhaquirks/ikea/__init__.py @@ -37,14 +37,8 @@ async def bind(self): class ScenesCluster(CustomCluster, Scenes): """Ikea Scenes cluster.""" - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self.server_commands = Scenes.attributes.copy() - self.server_commands.update( - { - 0x0007: ("press", (t.int16s, t.int8s, t.int8s), False), - 0x0008: ("hold", (t.int16s, t.int8s), False), - 0x0009: ("release", (t.int16s,), False), - } - ) + manufacturer_server_commands = { + 0x0007: ("press", (t.int16s, t.int8s, t.int8s), False), + 0x0008: ("hold", (t.int16s, t.int8s), False), + 0x0009: ("release", (t.int16s,), False), + } diff --git a/zhaquirks/ledvance/__init__.py b/zhaquirks/ledvance/__init__.py index d24813947f..b95059dabd 100644 --- a/zhaquirks/ledvance/__init__.py +++ b/zhaquirks/ledvance/__init__.py @@ -7,9 +7,7 @@ class LedvanceLightCluster(CustomCluster): """LedvanceLightCluster.""" - attributes = {} - client_commands = {} cluster_id = 0xFC01 ep_attribute = "ledvance_light" name = "LedvanceLight" - server_commands = {0x0001: ("save_defaults", (), False)} + manufacturer_server_commands = {0x0001: ("save_defaults", (), False)} diff --git a/zhaquirks/legrand/dimmer.py b/zhaquirks/legrand/dimmer.py index 426839932f..9648929c26 100644 --- a/zhaquirks/legrand/dimmer.py +++ b/zhaquirks/legrand/dimmer.py @@ -33,13 +33,11 @@ class LegrandCluster(CustomCluster, ManufacturerSpecificCluster): cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID name = "LegrandCluster" ep_attribute = "legrand_cluster" - attributes = { + manufacturer_attributes = { 0x0000: ("dimmer", t.data16), 0x0001: ("led_dark", t.Bool), 0x0002: ("led_on", t.Bool), } - server_commands = {} - client_commands = {} class DimmerWithoutNeutral(CustomDevice): diff --git a/zhaquirks/osram/__init__.py b/zhaquirks/osram/__init__.py index 9f8875e1cd..f4f91b7525 100644 --- a/zhaquirks/osram/__init__.py +++ b/zhaquirks/osram/__init__.py @@ -7,9 +7,7 @@ class OsramLightCluster(CustomCluster): """OsramLightCluster.""" - attributes = {} - client_commands = {} cluster_id = 0xFC0F ep_attribute = "osram_light" name = "OsramLight" - server_commands = {0x0001: ("save_defaults", (), False)} + manufacturer_server_commands = {0x0001: ("save_defaults", (), False)} diff --git a/zhaquirks/osram/lightifyx4.py b/zhaquirks/osram/lightifyx4.py index dcec905f53..b02674d7a3 100644 --- a/zhaquirks/osram/lightifyx4.py +++ b/zhaquirks/osram/lightifyx4.py @@ -55,7 +55,7 @@ class OsramButtonCluster(CustomCluster): cluster_id = OSRAM_CLUSTER name = "OsramCluster" ep_attribute = "osram_cluster" - attributes = { + manufacturer_attributes = { 0x000A: ("osram_1", t.uint8_t), 0x000B: ("osram_2", t.uint8_t), 0x000C: ("osram_3", t.uint16_t), @@ -71,8 +71,6 @@ class OsramButtonCluster(CustomCluster): 0x002E: ("osram_13", t.uint16_t), 0x002F: ("osram_14", t.uint16_t), } - server_commands = {} - client_commands = {} attr_config = { 0x000A: 0x01, 0x000B: 0x00, diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 7f364f4747..0e366f516f 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -57,15 +57,13 @@ class PowerOnState(t.enum8): class PhilipsOnOffCluster(CustomCluster, OnOff): """Philips OnOff cluster.""" - attributes = OnOff.attributes.copy() - attributes.update({0x4003: ("power_on_state", PowerOnState)}) + manufacturer_attributes = {0x4003: ("power_on_state", PowerOnState)} class PhilipsBasicCluster(CustomCluster, Basic): """Philips Basic cluster.""" - attributes = Basic.attributes.copy() - attributes.update({0x0031: ("philips", t.bitmap16)}) + manufacturer_attributes = {0x0031: ("philips", t.bitmap16)} attr_config = {0x0031: 0x000B} @@ -82,9 +80,7 @@ class PhilipsRemoteCluster(CustomCluster): cluster_id = 64512 name = "PhilipsRemoteCluster" ep_attribute = "philips_remote_cluster" - attributes = {} - server_commands = {} - client_commands = { + manufacturer_client_commands = { 0x0000: ( "notification", (t.uint8_t, t.uint24_t, t.uint8_t, t.uint8_t, t.uint8_t, t.uint8_t), diff --git a/zhaquirks/philips/sml002.py b/zhaquirks/philips/sml002.py index 01d7245033..39ec317b62 100644 --- a/zhaquirks/philips/sml002.py +++ b/zhaquirks/philips/sml002.py @@ -33,9 +33,10 @@ class OccupancyCluster(CustomCluster, OccupancySensing): """philips occupancy cluster.""" - attributes = OccupancySensing.attributes.copy() - attributes.update({0x0030: ("sensitivity", t.uint8_t)}) - attributes.update({0x0031: ("sensitivity_max", t.uint8_t)}) + manufacturer_attributes = { + 0x0030: ("sensitivity", t.uint8_t), + 0x0031: ("sensitivity_max", t.uint8_t), + } class PhilipsSML002(CustomDevice): diff --git a/zhaquirks/sinope/thermostat.py b/zhaquirks/sinope/thermostat.py index 73bad382fd..9f724d1034 100644 --- a/zhaquirks/sinope/thermostat.py +++ b/zhaquirks/sinope/thermostat.py @@ -41,19 +41,16 @@ class SinopeTechnologiesManufacturerCluster(CustomCluster): cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID name = "Sinopé Technologies Manufacturer specific" ep_attribute = "sinope_manufacturer_specific" - attributes = { + manufacturer_attributes = { 0x0010: ("outdoor_temp", t.int16s), 0x0020: ("secs_since_2k", t.uint32_t), } - client_commands = {} - server_commands = {} class SinopeTechnologiesThermostatCluster(CustomCluster, Thermostat): """SinopeTechnologiesThermostatCluster custom cluster.""" - attributes = Thermostat.attributes.copy() - attributes[0x0400] = ("set_occupancy", t.enum8) + manufacturer_attributes = {0x0400: ("set_occupancy", t.enum8)} class SinopeTechnologiesThermostat(CustomDevice): diff --git a/zhaquirks/smartthings/__init__.py b/zhaquirks/smartthings/__init__.py index 65cf6893c0..f0e702ce81 100644 --- a/zhaquirks/smartthings/__init__.py +++ b/zhaquirks/smartthings/__init__.py @@ -14,7 +14,7 @@ class SmartThingsAccelCluster(CustomCluster): cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID name = "Smartthings Accelerometer" ep_attribute = "accelerometer" - attributes = { + manufacturer_attributes = { 0x0000: ("motion_threshold_multiplier", t.uint8_t), 0x0002: ("motion_threshold", t.uint16_t), 0x0010: ("acceleration", t.bitmap8), # acceleration detected @@ -23,22 +23,20 @@ class SmartThingsAccelCluster(CustomCluster): 0x0014: ("z_axis", t.int16s), } - client_commands = {} - server_commands = {} - class SmartThingsIasZone(CustomCluster, IasZone): """IasZone cluster patched to support SmartThings spec violations.""" - client_commands = IasZone.client_commands.copy() - client_commands[0x0000] = ( - "status_change_notification", - ( - IasZone.ZoneStatus, - t.bitmap8, - # SmartThings never sends these two - t.Optional(t.uint8_t), - t.Optional(t.uint16_t), - ), - False, - ) + manufacturer_client_commands = { + 0x0000: ( + "status_change_notification", + ( + IasZone.ZoneStatus, + t.bitmap8, + # SmartThings never sends these two + t.Optional(t.uint8_t), + t.Optional(t.uint16_t), + ), + False, + ) + } diff --git a/zhaquirks/waxman/leaksmart.py b/zhaquirks/waxman/leaksmart.py index 4b40589abf..7442c221ae 100644 --- a/zhaquirks/waxman/leaksmart.py +++ b/zhaquirks/waxman/leaksmart.py @@ -59,13 +59,13 @@ def update_state(self, value): class WAXMANApplianceEventAlerts(CustomCluster, ApplianceEventAlerts): """WAXMAN specific ApplianceEventAlert cluster.""" + manufacturer_client_commands = { + WAXMAN_CMDID: ("alerts_notification", (t.uint8_t, t.bitmap24), False) + } + def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) - self.client_commands = super().client_commands.copy() - self.client_commands.update( - {WAXMAN_CMDID: ("alerts_notification", (t.uint8_t, t.bitmap24), False)} - ) self.endpoint.device.app_cluster = self def handle_cluster_request(self, tsn, command_id, args): diff --git a/zhaquirks/xiaomi/aqara/opple_remote.py b/zhaquirks/xiaomi/aqara/opple_remote.py index 1a0eab2810..e68db4895c 100644 --- a/zhaquirks/xiaomi/aqara/opple_remote.py +++ b/zhaquirks/xiaomi/aqara/opple_remote.py @@ -142,7 +142,7 @@ class OppleCluster(CustomCluster): ep_attribute = "opple_cluster" cluster_id = OPPLE_CLUSTER_ID - attributes = {0x0009: ("mode", types.uint8_t)} + manufacturer_attributes = {0x0009: ("mode", types.uint8_t)} attr_config = {0x0009: 0x01} def __init__(self, *args, **kwargs): diff --git a/zhaquirks/xiaomi/aqara/vibration_aq1.py b/zhaquirks/xiaomi/aqara/vibration_aq1.py index 333dc89239..234b60e68f 100644 --- a/zhaquirks/xiaomi/aqara/vibration_aq1.py +++ b/zhaquirks/xiaomi/aqara/vibration_aq1.py @@ -61,6 +61,8 @@ class VibrationAQ1(XiaomiCustomDevice): """Xiaomi aqara smart motion sensor device.""" + manufacturer_id_override = 0x115F + def __init__(self, *args, **kwargs): """Init.""" self.motion_bus = Bus() @@ -70,15 +72,13 @@ class VibrationBasicCluster(BasicCluster): """Vibration cluster.""" cluster_id = BasicCluster.cluster_id - attributes = Basic.attributes.copy() - attributes.update({0xFF0D: ("sensitivity", types.uint8_t)}) + manufacturer_attributes = {0xFF0D: ("sensitivity", types.uint8_t)} class MultistateInputCluster(CustomCluster, MultistateInput): """Multistate input cluster.""" cluster_id = DoorLock.cluster_id - attributes = MultistateInput.attributes.copy() - attributes.update({0x0000: ("lock_state", types.uint8_t)}) + manufacturer_attributes = {0x0000: ("lock_state", types.uint8_t)} def __init__(self, *args, **kwargs): """Init.""" diff --git a/zhaquirks/xiaomi/mija/smoke.py b/zhaquirks/xiaomi/mija/smoke.py index 81f9d67a8b..f98d03bd7e 100644 --- a/zhaquirks/xiaomi/mija/smoke.py +++ b/zhaquirks/xiaomi/mija/smoke.py @@ -44,11 +44,10 @@ class XiaomiSmokeIASCluster(CustomCluster, IasZone): """Xiaomi smoke IAS cluster implementation.""" - cluster_id = IasZone.cluster_id - attributes = IasZone.attributes.copy() - attributes.update( - {0xFFF1: ("set_options", t.uint32_t), 0xFFF0: ("get_status", t.uint32_t)} - ) + manufacturer_attributes = { + 0xFFF1: ("set_options", t.uint32_t), + 0xFFF0: ("get_status", t.uint32_t), + } class MijiaHoneywellSmokeDetectorSensor(XiaomiCustomDevice): From 4a88899da254f8441686d9bbe305a2d27adac93b Mon Sep 17 00:00:00 2001 From: Greg Thornton Date: Fri, 10 Jul 2020 14:17:59 -0500 Subject: [PATCH 036/113] Add sercomm flood sensor quirk (#402) --- zhaquirks/sercomm/__init__.py | 2 + zhaquirks/sercomm/szwtd02n.py | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 zhaquirks/sercomm/__init__.py create mode 100644 zhaquirks/sercomm/szwtd02n.py diff --git a/zhaquirks/sercomm/__init__.py b/zhaquirks/sercomm/__init__.py new file mode 100644 index 0000000000..d953f0bd4c --- /dev/null +++ b/zhaquirks/sercomm/__init__.py @@ -0,0 +1,2 @@ +"""Module for sercomm quirks.""" +SERCOMM = "Sercomm Corp." diff --git a/zhaquirks/sercomm/szwtd02n.py b/zhaquirks/sercomm/szwtd02n.py new file mode 100644 index 0000000000..1962e37a0a --- /dev/null +++ b/zhaquirks/sercomm/szwtd02n.py @@ -0,0 +1,69 @@ +"""Device handler for Sercomm SZ-WTD02N flood sensor.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Identify, Ota, PollControl +from zigpy.zcl.clusters.security import IasZone +from zigpy.zcl.clusters.homeautomation import Diagnostic + +from zhaquirks import PowerConfigurationCluster +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +from . import SERCOMM + + +class SercommPowerConfiguration(PowerConfigurationCluster): + """Sercomm power configuration cluster for flood sensor.""" + + MAX_VOLTS = 3.2 + MIN_VOLTS = 2.1 + + +class SZWTD02N(CustomDevice): + """Custom device representing Sercomm SZ-WTD02N flood sensor.""" + + signature = { + # + MODELS_INFO: [(SERCOMM, "SZ-WTD02N_SF")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + SercommPowerConfiguration.cluster_id, + Identify.cluster_id, + PollControl.cluster_id, + IasZone.cluster_id, + Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic.cluster_id, + SercommPowerConfiguration, + Identify.cluster_id, + PollControl.cluster_id, + IasZone.cluster_id, + Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + } + } From 7bcff6fafb9d3668ca0522676d6963818767ab3b Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 10 Jul 2020 15:18:52 -0400 Subject: [PATCH 037/113] Support for Develco WISZB-120 sensor (#404) * Support for Develco WISZB-120 sensor. * Lint --- zhaquirks/develco/__init_.py | 11 +++ zhaquirks/develco/open_close.py | 116 ++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 zhaquirks/develco/__init_.py create mode 100644 zhaquirks/develco/open_close.py diff --git a/zhaquirks/develco/__init_.py b/zhaquirks/develco/__init_.py new file mode 100644 index 0000000000..c13fae8603 --- /dev/null +++ b/zhaquirks/develco/__init_.py @@ -0,0 +1,11 @@ +"""Quirks for Develco Products A/S.""" +from .. import PowerConfigurationCluster + +DEVELCO = "Develco Products A/S" + + +class DevelcoPowerConfiguration(PowerConfigurationCluster): + """Common use power configuration cluster.""" + + MIN_VOLTS = 2.6 # old 2.1 + MAX_VOLTS = 3.0 # old 3.2 diff --git a/zhaquirks/develco/open_close.py b/zhaquirks/develco/open_close.py new file mode 100644 index 0000000000..bd11840ffa --- /dev/null +++ b/zhaquirks/develco/open_close.py @@ -0,0 +1,116 @@ +"""Door/Windows sensors.""" + +from zigpy.profiles import zha +import zigpy.types as t +from zigpy.quirks import CustomDevice, CustomCluster +from zigpy.zcl.clusters import general, measurement, security + +from . import DEVELCO, DevelcoPowerConfiguration +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class DevelcoIASZone(CustomCluster, security.IasZone): + """IAS Zone.""" + + manufacturer_client_commands = { + 0x0000: ( + "status_change_notification", + ( + security.IasZone.ZoneStatus, + t.bitmap8, + t.Optional(t.uint8_t), + t.Optional(t.uint16_t), + ), + False, + ) + } + + +class WISZB120(CustomDevice): + """Custom device representing door/windows sensors.""" + + signature = { + # + # + # + MODELS_INFO: [(DEVELCO, "WISZB-120")], + ENDPOINTS: { + 1: { + PROFILE_ID: 0xC0C9, + DEVICE_TYPE: 1, + INPUT_CLUSTERS: [ + general.Identify.cluster_id, + general.Scenes.cluster_id, + general.OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 35: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.BinaryInput.cluster_id, + general.PollControl.cluster_id, + security.IasZone.cluster_id, + ], + OUTPUT_CLUSTERS: [general.Time, general.Ota.cluster_id], + }, + 38: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + general.Basic.cluster_id, + general.Identify.cluster_id, + measurement.TemperatureMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [general.Identify.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + general.Identify.cluster_id, + general.Scenes.cluster_id, + general.OnOff.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + 35: { + INPUT_CLUSTERS: [ + general.Basic.cluster_id, + DevelcoPowerConfiguration, + general.Identify.cluster_id, + general.BinaryInput.cluster_id, + general.PollControl.cluster_id, + DevelcoIASZone, + ], + OUTPUT_CLUSTERS: [general.Time, general.Ota.cluster_id], + }, + 38: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, + INPUT_CLUSTERS: [ + general.Basic.cluster_id, + general.Identify.cluster_id, + measurement.TemperatureMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [general.Identify.cluster_id], + }, + } + } From db4b58f1ae4df1214e61fd32d05cdec66eb00c98 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 12 Jul 2020 08:20:28 -0400 Subject: [PATCH 038/113] Add save defaults to Ledvance A19 RGBW (#406) --- zhaquirks/ledvance/a19rgbw.py | 74 +++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 zhaquirks/ledvance/a19rgbw.py diff --git a/zhaquirks/ledvance/a19rgbw.py b/zhaquirks/ledvance/a19rgbw.py new file mode 100644 index 0000000000..0c76ba919e --- /dev/null +++ b/zhaquirks/ledvance/a19rgbw.py @@ -0,0 +1,74 @@ +"""Ledvance A19 RGBW device.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + Scenes, +) +from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.zcl.clusters.lighting import Color + +from . import LEDVANCE, LedvanceLightCluster +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class LedvanceA19RGBW(CustomDevice): + """Ledvance A19 RGBW device.""" + + signature = { + # + MODELS_INFO: [(LEDVANCE, "A19 RGBW")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + Diagnostic.cluster_id, + LedvanceLightCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + Diagnostic.cluster_id, + LedvanceLightCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + } + } From 5c56d1a5310a5cdb1a12fc1ebecd02ce976be8e0 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 13 Jul 2020 06:58:55 -0400 Subject: [PATCH 039/113] update philips bulb support (#407) --- zhaquirks/philips/{lct011.py => lct.py} | 21 ++++- zhaquirks/philips/lct024.py | 93 ---------------------- zhaquirks/philips/{lct016.py => lwb010.py} | 19 ++--- 3 files changed, 24 insertions(+), 109 deletions(-) rename zhaquirks/philips/{lct011.py => lct.py} (83%) delete mode 100644 zhaquirks/philips/lct024.py rename zhaquirks/philips/{lct016.py => lwb010.py} (81%) diff --git a/zhaquirks/philips/lct011.py b/zhaquirks/philips/lct.py similarity index 83% rename from zhaquirks/philips/lct011.py rename to zhaquirks/philips/lct.py index 48e44de95f..2032b89d91 100644 --- a/zhaquirks/philips/lct011.py +++ b/zhaquirks/philips/lct.py @@ -1,4 +1,4 @@ -"""Quirk for Phillips LCT011.""" +"""Quirk for Phillips LCT bulbs.""" from zigpy.profiles import zll from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( @@ -26,11 +26,24 @@ from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster -class PhilipsLCT011(CustomDevice): - """Philips LCT011 device.""" +class PhilipsLCT(CustomDevice): + """Philips LCT bulb device.""" signature = { - MODELS_INFO: [(PHILIPS, "LCT011")], + MODELS_INFO: [ + (PHILIPS, "LCT001"), + (PHILIPS, "LCT002"), + (PHILIPS, "LCT003"), + (PHILIPS, "LCT007"), + (PHILIPS, "LCT010"), + (PHILIPS, "LCT011"), + (PHILIPS, "LCT012"), + (PHILIPS, "LCT014"), + (PHILIPS, "LCT015"), + (PHILIPS, "LCT016"), + (PHILIPS, "LCT021"), + (PHILIPS, "LCT024"), + ], ENDPOINTS: { 11: { # Date: Tue, 14 Jul 2020 01:35:31 +0100 Subject: [PATCH 040/113] Add EDP WithUs SmartPlug quirk (#410) * Add EDP WithUs SmartPlug quirk. This device wasn't providing any values for divisor and multiplier attributes, leading to incorrect values. * Remove unused imports --- zhaquirks/edpwithus/__init__.py | 15 ++++++++ zhaquirks/edpwithus/redy_plug.py | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 zhaquirks/edpwithus/__init__.py create mode 100644 zhaquirks/edpwithus/redy_plug.py diff --git a/zhaquirks/edpwithus/__init__.py b/zhaquirks/edpwithus/__init__.py new file mode 100644 index 0000000000..c546c01427 --- /dev/null +++ b/zhaquirks/edpwithus/__init__.py @@ -0,0 +1,15 @@ +"""EDP WithUs module.""" +import logging + +from zigpy.quirks import CustomCluster +from zigpy.zcl.clusters.smartenergy import Metering + +_LOGGER = logging.getLogger(__name__) + + +class MeteringCluster(CustomCluster, Metering): + """EDP WithUs Metering cluster.""" + + MULTIPLIER = 0x0301 + DIVISOR = 0x0302 + _CONSTANT_ATTRIBUTES = {MULTIPLIER: 1, DIVISOR: 1000} diff --git a/zhaquirks/edpwithus/redy_plug.py b/zhaquirks/edpwithus/redy_plug.py new file mode 100644 index 0000000000..fb27bcf709 --- /dev/null +++ b/zhaquirks/edpwithus/redy_plug.py @@ -0,0 +1,64 @@ +"""EDP WithUs SmartPlug Quirk.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Alarms, + Basic, + Identify, + Groups, + Scenes, + OnOff, + Ota, + Time, +) +from zigpy.zcl.clusters.smartenergy import Metering + +from . import MeteringCluster + + +class EdpWithUsSmartPlug(CustomDevice): + """Tradfri Plug.""" + + signature = { + "endpoints": { + # + 85: { + "profile_id": zha.PROFILE_ID, + "device_type": zha.DeviceType.MAIN_POWER_OUTLET, + "input_clusters": [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Alarms.cluster_id, + Time.cluster_id, + Metering.cluster_id, + ], + "output_clusters": [Ota.cluster_id], + } + }, + "manufacturer": "EDP-WITHUS", + } + + replacement = { + "endpoints": { + 85: { + "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, + Alarms.cluster_id, + Time.cluster_id, + MeteringCluster, + ], + "output_clusters": [Ota.cluster_id], + } + } + } From e92d217a1553d283507a9d09f3e046c44161d4f6 Mon Sep 17 00:00:00 2001 From: Piotr Majkrzak Date: Tue, 14 Jul 2020 21:46:14 +0200 Subject: [PATCH 041/113] Add quirk for Osram Smart+ Switch Mini (#409) * Add quirk for Osram Smart+ Switch Mini Signed-off-by: Piotr Majkrzak * Use COLOR_SCENE_CONTROLLER instead of custom const Signed-off-by: Piotr Majkrzak --- zhaquirks/osram/switchmini.py | 128 ++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 zhaquirks/osram/switchmini.py diff --git a/zhaquirks/osram/switchmini.py b/zhaquirks/osram/switchmini.py new file mode 100644 index 0000000000..f1a0131d17 --- /dev/null +++ b/zhaquirks/osram/switchmini.py @@ -0,0 +1,128 @@ +"""Osram Smart+ Switch Mini device.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + PowerConfiguration, + Scenes, + PollControl, +) +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from . import OSRAM +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + OUTPUT_CLUSTERS, + PROFILE_ID, + SHORT_PRESS, + COMMAND, + COMMAND_ON, + MODELS_INFO, + BUTTON_1, + ENDPOINT_ID, + COMMAND_STEP_ON_OFF, + COMMAND_STOP, + BUTTON_2, + BUTTON_3, + LONG_RELEASE, + LONG_PRESS, + COMMAND_MOVE_TO_LEVEL_ON_OFF, + COMMAND_OFF, + COMMAND_MOVE, +) + +OSRAM_CLUSTER = 0xFD00 + + +class OsramSwitchMini(CustomDevice): + """Osram Smart+ Switch Mini device.""" + + signature = { + MODELS_INFO: [(OSRAM, "Lightify Switch Mini")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_SCENE_CONTROLLER, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + PollControl.cluster_id, + LightLink.cluster_id, + OSRAM_CLUSTER, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Ota.cluster_id, + Color.cluster_id, + LightLink.cluster_id, + ], + }, + # + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_SCENE_CONTROLLER, + INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + LightLink.cluster_id, + ], + }, + # + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_SCENE_CONTROLLER, + INPUT_CLUSTERS: [Basic.cluster_id, LightLink.cluster_id, OSRAM_CLUSTER], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + LightLink.cluster_id, + ], + }, + }, + } + + device_automation_triggers = { + (SHORT_PRESS, BUTTON_1): {COMMAND: COMMAND_ON, ENDPOINT_ID: 1}, + (LONG_PRESS, BUTTON_1): {COMMAND: COMMAND_STEP_ON_OFF, ENDPOINT_ID: 1}, + (LONG_RELEASE, BUTTON_1): {COMMAND: COMMAND_STOP, ENDPOINT_ID: 1}, + (SHORT_PRESS, BUTTON_2): { + COMMAND: COMMAND_MOVE_TO_LEVEL_ON_OFF, + ENDPOINT_ID: 3, + }, + (LONG_PRESS, BUTTON_2): {COMMAND: "move_to_saturation", ENDPOINT_ID: 3}, + (LONG_RELEASE, BUTTON_2): {COMMAND: "move_hue", ENDPOINT_ID: 3}, + (SHORT_PRESS, BUTTON_3): {COMMAND: COMMAND_OFF, ENDPOINT_ID: 2}, + (LONG_PRESS, BUTTON_3): {COMMAND: COMMAND_MOVE, ENDPOINT_ID: 2}, + (LONG_RELEASE, BUTTON_3): {COMMAND: COMMAND_STOP, ENDPOINT_ID: 2}, + } From 5228d10029a5143708e4888a220712cf2731ba7d Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 14 Jul 2020 17:39:57 -0400 Subject: [PATCH 042/113] bump version and zigpy requirement --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ead217a9e4..6a4bf53871 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.41" +VERSION = "0.0.42" def readme(): @@ -24,6 +24,6 @@ def readme(): keywords="zha quirks homeassistant hass", packages=find_packages(exclude=["*.tests"]), python_requires=">=3", - install_requires=["zigpy>=0.22.0"], + install_requires=["zigpy>=0.22.1"], tests_require=["pytest"], ) From 76006f985222f749177e93ef4e4867aa4c05fe4e Mon Sep 17 00:00:00 2001 From: tube0013 Date: Wed, 29 Jul 2020 11:46:56 -0400 Subject: [PATCH 043/113] Update README.md (#427) Add instructions to test quirks in development in supervised installs. --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 65a40049f0..b488256987 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,23 @@ If you are instead using some custom python installation of Home Assistant then ``` pip install zha-quirks==0.0.38 +# Testing quirks in development in docker based install +If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io distro) you will need to get access to the home-assistant docker container. Directions below are given for using the portainer add-on to do this, there are other methods as well not covered here. +- Install the portainer add-on (https://github.com/hassio-addons/addon-portainer) from Home Assistant Community Add-ons. +- Follow the add-on documentation to un-hide the home-assistant container (https://github.com/hassio-addons/addon-portainer/blob/master/portainer/DOCS.md) +- Stage the update quirk in a directory within your config directory +- Use portainer to access a console in the home-assistant container: + + + +- Access the quirks directory + - on HA > 0.113: /usr/local/lib/python3.8/site-packages/zhaquirks/ + - on HA < 0.113: /usr/local/lib/python3.7/site-packages/zhaquirks/ +- Copy updated/new quirk to zhaquirks directory: ```cp -a /config/temp/NEW_QUIRK ./``` +- Remove the __py_cache__ folder so it is regenerated ```rm -rf ./__py_cache__/``` +- Close out the console and restart HA. +- Note: The added/update quirk will not survive a HA version update. + # Thanks - Special thanks to damarco for the majority of the device tracker code From 8ffc64f4dc89d740e098d10fa2d22fb8181c044d Mon Sep 17 00:00:00 2001 From: Adrien Chevrier Date: Wed, 29 Jul 2020 17:47:33 +0200 Subject: [PATCH 044/113] Support for WXKG07LM (#419) --- zhaquirks/xiaomi/aqara/remote_b286acn01.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zhaquirks/xiaomi/aqara/remote_b286acn01.py b/zhaquirks/xiaomi/aqara/remote_b286acn01.py index 6643ff9867..62722d062e 100644 --- a/zhaquirks/xiaomi/aqara/remote_b286acn01.py +++ b/zhaquirks/xiaomi/aqara/remote_b286acn01.py @@ -92,7 +92,11 @@ def _update_attribute(self, attrid, value): # device_version=1 # input_clusters=[0, 3, 25, 65535, 18] # output_clusters=[0, 4, 3, 5, 25, 65535, 18]> - MODELS_INFO: [(LUMI, "lumi.remote.b286acn01"), (LUMI, "lumi.sensor_86sw2")], + MODELS_INFO: [ + (LUMI, "lumi.remote.b286acn01"), + (LUMI, "lumi.remote.b286acn02"), + (LUMI, "lumi.sensor_86sw2"), + ], ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, From e09f7f81171de15c84825f5e0b3f3b987616f829 Mon Sep 17 00:00:00 2001 From: Claude Gelinas Date: Thu, 30 Jul 2020 21:10:29 -0400 Subject: [PATCH 045/113] Add quirks to support Sinope light and dimmer (#425) * Create dimmer.py * Update dimmer.py * first commit First code layout for lights switch and dimmer * Cleanup in import * Class name error * Changed device type for light and dimmeranged device type * code formating with black and typo fix * Updated cluster number for input and output to match the devices * Fix cluster_id for replacement * Fix PR error * code reformating * Typo fix * Formating * Remove unnessary code * Black reformat * Remove unused code * Fix cluster_id Put the new DeviceType in the replacement section instead of the signature * adjust coma * Fix formatting. Co-authored-by: Alexei Chetroi --- zhaquirks/sinope/light.py | 150 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 zhaquirks/sinope/light.py diff --git a/zhaquirks/sinope/light.py b/zhaquirks/sinope/light.py new file mode 100644 index 0000000000..8e008fdcb4 --- /dev/null +++ b/zhaquirks/sinope/light.py @@ -0,0 +1,150 @@ +""" +This module handles quirks of the Sinopé Technologies light SW2500ZB and dimmer DM2500ZB. + +Manufacturer specific cluster implements attributes to control displaying +setting occupancy on/off. +""" + +import zigpy.profiles.zha as zha_p +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + DeviceTemperature, + Groups, + Identify, + OnOff, + LevelControl, + Ota, + Scenes, +) +from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.zcl.clusters.smartenergy import Metering +from . import SINOPE +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +SINOPE_MANUFACTURER_CLUSTER_ID = 0xFF01 + + +class SinopeTechnologieslight(CustomDevice): + """SinopeTechnologiesLight custom device.""" + + signature = { + # + MODELS_INFO: [(SINOPE, "SW2500ZB")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.ON_OFF_LIGHT_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Metering.cluster_id, + Diagnostic.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Ota.cluster_id, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + Metering.cluster_id, + Diagnostic.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Ota.cluster_id, + ], + } + } + } + + +class SinopeDM2500ZB(SinopeTechnologieslight): + """DM2500ZB Dimmer.""" + + signature = { + # + MODELS_INFO: [(SINOPE, "DM2500ZB")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Metering.cluster_id, + Diagnostic.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Ota.cluster_id, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DeviceTemperature.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Metering.cluster_id, + Diagnostic.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Ota.cluster_id, + ], + } + } + } From 2faaf8391338f4f1a424f1ceced6a92286440b5a Mon Sep 17 00:00:00 2001 From: Michael Thingnes Date: Mon, 10 Aug 2020 00:31:50 +1200 Subject: [PATCH 046/113] Add power measurement quirk for Xiaomi CN/AU lumi.plug (#434) * Add power measurement for `lumi.plug` Addresses #397 * Formatted new plug.py module with black. Addresses #397 --- Contributors.md | 1 + README.md | 1 + zhaquirks/xiaomi/__init__.py | 2 +- zhaquirks/xiaomi/aqara/plug.py | 128 +++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 zhaquirks/xiaomi/aqara/plug.py diff --git a/Contributors.md b/Contributors.md index 732bc0d049..f89727b103 100644 --- a/Contributors.md +++ b/Contributors.md @@ -19,3 +19,4 @@ - [Andy Zickler](https://github.com/andyzickler) - [Piotr Majkrzak](https://github.com/majkrzak) - [Gleb Sinyavskiy](https://github.com/zhulik) +- [Michael Thingnes](https://github.com/thimic) diff --git a/README.md b/README.md index b488256987..39f56ea21a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ If you are looking to make your first code contribution to this project then we - [Water Leak](https://www.aqara.com/en/water_leak_sensor.html): lumi.sensor_wleak.aq1 - [US Plug](https://www.aqara.com/en/smart_plug.html): lumi.plug.maus01 - [EU Plug](https://zigbee.blakadder.com/Xiaomi_ZNCZ04LM.html): lumi.plug.mmeu01 +- [CN Plug](https://zigbee.blakadder.com/Xiaomi_ZNCZ02LM.html): lumi.plug ### Osram - [OSRAM LIGHTIFY Dimming Switch](https://assets.osram-americas.com/assets/Documents/LTFY012.06c0d6e6-17c7-4dcb-bd2c-1fca7feecfb4.pdf): diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index 9149b91537..2e348c1f0e 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -430,7 +430,7 @@ def pressure_reported(self, value): class AnalogInputCluster(CustomCluster, AnalogInput): - """Analog input cluster, only used to relay power consumtion information to ElectricalMeasurementCluster.""" + """Analog input cluster, only used to relay power consumption information to ElectricalMeasurementCluster.""" cluster_id = AnalogInput.cluster_id diff --git a/zhaquirks/xiaomi/aqara/plug.py b/zhaquirks/xiaomi/aqara/plug.py new file mode 100644 index 0000000000..5e10d79ee0 --- /dev/null +++ b/zhaquirks/xiaomi/aqara/plug.py @@ -0,0 +1,128 @@ +"""Xiaomi lumi.plug plug.""" +import logging + +from zigpy.profiles import zha +from zigpy.zcl.clusters.general import ( + AnalogInput, + Basic, + BinaryOutput, + DeviceTemperature, + Groups, + Identify, + OnOff, + Ota, + PowerConfiguration, + Scenes, + Time, +) + +from zhaquirks.xiaomi import ( + LUMI, + AnalogInputCluster, + BasicCluster, + ElectricalMeasurementCluster, + XiaomiCustomDevice, +) +from zhaquirks import Bus +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, + SKIP_CONFIGURATION, +) + +_LOGGER = logging.getLogger(__name__) + + +class Plug(XiaomiCustomDevice): + """lumi.plug plug.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.voltage_bus = Bus() + self.consumption_bus = Bus() + self.power_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + MODELS_INFO: [(LUMI, "lumi.plug")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + # + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], + }, + # + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id], + }, + }, + } + replacement = { + SKIP_CONFIGURATION: True, + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + BasicCluster, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurementCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInputCluster], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id], + }, + }, + } From c1074884eb213bebb4c54237e3f4eb407eec4dfd Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 9 Aug 2020 08:32:09 -0400 Subject: [PATCH 047/113] Update contributing guide (#431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * start hacking on a real guide * add more to guide * Reformatting, spelling fixes (#428) * add more to guide * fix error * Starting to flesh out the building example (#429) Starting to flesh out the building example and some more spelling fixes * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Abílio Costa * Update CONTRIBUTING.md Co-authored-by: Alexei Chetroi Co-authored-by: walthowd Co-authored-by: Alexei Chetroi Co-authored-by: Abílio Costa --- CONTRIBUTING.md | 388 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 378 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb700fb337..1f84d78be2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,382 @@ -# Initial Contribution Guidelines - WIP +# Primer + +ZHA device handlers and it's provided Quirks allow Zigpy, ZHA and Home Assistant to work with non standard Zigbee devices. If you are reading this you may have a device that isn't working as expected. This can be the case for a number of reasons but in this guide we will cover the cases where functionality is provided by a device in a non specification compliant manner by the device manufacturer. + +## What are these specifications? + +[Zigbee Specification](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf) + +[Zigbee Cluster Library](https://zigbeealliance.org/wp-content/uploads/2019/12/07-5123-06-zigbee-cluster-library-specification.pdf) + +[Zigbee Base Device Specification](https://zigbeealliance.org/wp-content/uploads/zip/zigbee-base-device-behavior-bdb-v1-0.zip) + +[Zigbee Primer](https://docs.smartthings.com/en/latest/device-type-developers-guide/zigbee-primer.html) + +## What is a device in human terms? + +A device is a physical object that you want to join to a Zigbee network: a light bulb, a switch, a sensor etc. The host application, in this case Zigpy, needs to understand how to interact with the device so there are standards that define how the application and devices can communicate. The device's functionality is described by several **descriptors** while the device itself contains **endpoints** and **endpoints** contain **clusters**. There are two types of clusters an endpoint contains: +- **in_clusters** - are "Server" clusters in ZCL terms. These clusters control the device, e.g. a smart plug or light bulb would have an `on_off` server cluster. **in_clusters** are also the ones which also send attribute reports and/or you can read an attribute from a **in_cluster**. +- **out_clusters** - are "Client" clusters. These clusters control some other device, as "Client" cluster sends commands to "Server" cluster. For example an On/Off remote would have an `on_off` client cluster and will generate cluster commands and send those to some other device. +Zigpy needs to understand all these elements in order to correctly work with the device. + +### Endpoints + +Endpoints are essentially groupings of functionality. For example, a typical Zigbee light bulb will have a single endpoint for the light. A multi-gang wall switch may have an endpoint for each individual switch so they can all be controlled separately. Each endpoint has several functions represented by clusters. + +### Clusters + +Clusters are objects that contain the information (attributes and commands) for individual functions. There is the ability to turn the switch on and off, maybe there is energy monitoring, maybe there is the ability to add each switch to an individual group or a scene, etc. Each of these functions belong to a cluster. + +### Descriptors + +For the purposes of Zigpy and Quirks we will focus on two descriptors: **Node Descriptor** and **Simple Descriptor**. + + +#### Node Descriptor + +A node descriptor explains some basic device attributes to Zigpy. The manufacturer code and the power type are the ones that we generally care about. In most cases you won't have to worry about this but it is good to know why it is there in case you come across it while looking at an existing quirk. Here is an example: +`` + +#### Simple Descriptor + +A simple descriptor is a description of a Zigbee device endpoint and is responsible for explaining the endpoint's functionality. It contains a profile id, the device type, and collections of clusters. The profile id tells the application what set of Zigbee rules to use. The most common profile will be 260 (0x0104) for the Home Automation profile. The device type tells the application what logical type of device this is ex: on off light, color light, etc. The clusters explain to the application what types of functionality exist on the endpoint. Here is an example: +`` + +## What the heck is a quirk? + +In human terms you can think of a quirk like google translate. I know it's a weird comparison but lets dig in a bit. You may only speak one language but there is an interesting article written in another language that you really want to read. Google translate takes the original article and displays it in a format (language) that you understand. A quirk is a file that translates device functionality from the format that the manufacturer chose to implement it in to a format that Zigpy and in turn ZHA understand. The main purpose of a quirk is to serve as a translator. A quirk is comprised of several parts: + +- Signature - To identify and apply the correct quirk +- Replacement - To allow Zigpy and ZHA to correctly work with the device +- device_automation_triggers - To let the Home Assistant Device Automation engine and users interact with the device + +### Signature + +The signature on a quirk identifies the device as the manufacturer implemented it. You can think of it as a fingerprint or the dna of the device. The signature is what we use to identify the device. If any part of the signature doesn't match what the device returns during discovery the quirk will not match and as a result it will not be applied. The signature is made up of several parts: + +- `models_info` +- `endpoints` + +Models info tells the application which devices should use this particular quirk. Endpoints are the simple descriptors that we spoke about earlier exactly as they are on the device. `endpoints` is a dict where the key is the id of the endpoint and the value is an object with the following properties: `profile_id`, `device_type`, `input_clusters` and `output_clusters`. Creating the signature element is generally just a job of transcribing what the device gives us. Here is an example: + +```python +signature = { + MODELS_INFO: [(LUMI, "lumi.plug.maus01")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + }, +} +``` + +### Replacement + +The replacement on a quirk is what we want the device to be. Remember, we said that quirks were like Google translate... you can think of the replacement like the output from Google translate. The replacement dict is what will actually be used by Zigpy and ZHA to interact with the device. The structure of `replacement` is the same as signature with 2 key differences: `models_info` is generally omitted and there is an extra element `skip_configuration` that instructs the application to skip configuration if necessary. Some manufacturers have not implemented the specifications correctly and the devices come pre-configured and therefore the configuration calls fail (non Zigbee 3.0 Xiaomi devices for instance). Usually, you should not add `skip_configuration`. + +Here is an example: + +```python +replacement = { + SKIP_CONFIGURATION: True, + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + BasicCluster, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurementCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + }, +} +``` + +### device_automation_triggers + +Device automation triggers are essentially representations of the events that the devices fire in HA. They allow users to use actions in the UI instead of using the raw events. + +# Building a quirk + +Now that we got that out of the way we can focus on the task at hand: make our devices work the way they should with Zigpy and ZHA. Because the device doesn't work correctly out of the box we have to write a quirk for it. First lets look at what the quirk looks like when complete: + +```python +class Plug(XiaomiCustomDevice): + """lumi.plug.maus01 plug.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.voltage_bus = Bus() + self.consumption_bus = Bus() + self.power_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + MODELS_INFO: [(LUMI, "lumi.plug.maus01")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + # + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], + }, + # + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id], + }, + # + 100: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [BinaryInput.cluster_id], + OUTPUT_CLUSTERS: [BinaryInput.cluster_id, Groups.cluster_id], + }, + }, + } + replacement = { + SKIP_CONFIGURATION: True, + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + BasicCluster, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurementCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInputCluster], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id], + }, + 100: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [BinaryInput.cluster_id], + OUTPUT_CLUSTERS: [BinaryInput.cluster_id, Groups.cluster_id], + }, + }, + } +``` + +This quirk is for the US version of the Xiaomi plug. Xiaomi is notorious for not following the Zigbee specifications and most of their non Zigbee 3.0 devices need a quirk to function correctly. In this case we are correcting the `ElectricalMeasurement` cluster readings. Xiaomi decided to report the values for this cluster on the `AnalogInput` cluster instead. To fix this we will create a custom cluster to replace the `AnalogInput` and `ElectricalMeasurement` clusters. We will take the values that are reported on the `AnalogInput` cluster and publish them to the `ElectricalMeasurement` cluster. Doing this allows the device to work as if Xiaomi had implemented this in the first place. This is the act of translating that was mentioned in the Google Translate analogy above. + +First things first. All device definitions in quirks must extend `CustomDevice` or a derivative of it and all clusters that you define must extend `CustomCluster` or a derivative of it. If you want to send messages between `CustomCluster` definitions as we do here you need to create channels for the communication to flow through. We do this by adding instances of `Bus` on our `CustomDevice` implementation. `Bus` is a utility class used specifically for this purpose and adding it to the device implementation ensures that all clusters that you define will have access to the `Bus` so that they can communicate with eachother. + +```python +class Plug(XiaomiCustomDevice): + """lumi.plug.maus01 plug.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.voltage_bus = Bus() + self.consumption_bus = Bus() + self.power_bus = Bus() + super().__init__(*args, **kwargs) +``` + +You can see that we have extended `XiaomiCustomDevice` which is a derivative of `CustomDevice` shared by Xiaomi devices. You can also see that we have added some instances of `Bus` so that we can pass messages between `CustomCluster` definitions. To be clear, this is not always necessary. Quirks can be used to change formats of data on an existing cluster, to add manufacturer specific attributes or commands to clusters etc. In these instances you just need to create a derivative of `CustomCluster` and add your logic. This is more of an advanced example to illustrate what is possible. + +Here are the custom cluster definitions: + +```python +class AnalogInputCluster(CustomCluster, AnalogInput): + """Analog input cluster, only used to relay power consumtion information to ElectricalMeasurementCluster.""" + + cluster_id = AnalogInput.cluster_id + + def __init__(self, *args, **kwargs): + """Init.""" + self._current_state = {} + super().__init__(*args, **kwargs) + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if value is not None and value >= 0: + self.endpoint.device.power_bus.listener_event(POWER_REPORTED, value) + + +class ElectricalMeasurementCluster(LocalDataCluster, ElectricalMeasurement): + """Electrical measurement cluster to receive reports that are sent to the basic cluster.""" + + cluster_id = ElectricalMeasurement.cluster_id + POWER_ID = 0x050B + VOLTAGE_ID = 0x0500 + CONSUMPTION_ID = 0x0304 + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.voltage_bus.add_listener(self) + self.endpoint.device.consumption_bus.add_listener(self) + self.endpoint.device.power_bus.add_listener(self) + + def power_reported(self, value): + """Power reported.""" + self._update_attribute(self.POWER_ID, value) + + def voltage_reported(self, value): + """Voltage reported.""" + self._update_attribute(self.VOLTAGE_ID, value) + + def consumption_reported(self, value): + """Consumption reported.""" + self._update_attribute(self.CONSUMPTION_ID, value) +``` + +In the `AnalogInput` cluster we override the `_update_attribute` method so that we can access the data that the cluster receives when the device sends a report and we send the data via an event on a bus to the `ElectricalMeasurement` cluster. This is the line that does the heavy lifting: + +`self.endpoint.device.power_bus.listener_event(POWER_REPORTED, value)` + +Then in the `ElectricalMeasurement` cluster we need to subscribe to these events and handle them. This is how we subscribe to our custom events: + +`self.endpoint.device.power_bus.add_listener(self)` + +and this method (the method name must match the event name that you publish EXACTLY): + +```python +def power_reported(self, value): + """Power reported.""" + self._update_attribute(self.POWER_ID, value) +``` + +receives the event and handles updating the attribute on the correct zigbee cluster. As you can see there really isn't much here that needs to be done to accomplish our goal. + +Once we have created our `CustomCluster` implementations we have to tell the `CustomDevice` implementation to use them. We do this in the `replacement` dict in the quirk definition. Start by copying the `signature` dict and remove the `models_info` from it. Then we replace the cluster ids that we want to override with the names of our `CustomCluster` implementations that we have created. The result looks like this: + +```python +replacement = { + SKIP_CONFIGURATION: True, + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + BasicCluster, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurementCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInputCluster], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id], + }, + 100: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [BinaryInput.cluster_id], + OUTPUT_CLUSTERS: [BinaryInput.cluster_id, Groups.cluster_id], + }, +``` + +You can see that we have replaced `ElectricalMeasurement.cluster_id` from endpoint 1 in the `signature` dict with the name of our cluster that we created: `ElectricalMeasurementCluster` and on endpoint 2 we replaced `AnalogInput.cluster_id` with the implementation we created for that: `AnalogInputCluster`. This instructs Zigpy to use these `CustomCluster` derivatives instead of the normal cluster definitions for these clusters and this is why this part of the quirk is called `replacement`. + +Now lets put this all together. If you examine the device definition above you will see that we have defined our custom device, we defined the `signature` dict where we transcribed the `SimpleDescriptor` output we obtained when the device joined the network and we defined the `replacement` dict where we swapped the cluster ids for the culsters that we wanted to replace with the `CustomCluster` implementations that we created. + +# Contribution Guidelines - All code is formatted with black. The check format script that runs in CI will ensure that code meets this requirement and that it is correctly formatted with black. Instructions for installing black in many editors can be found here: -- Capture the SimpleDescriptor log entries for each endpoint on the device. These can be found in the HA logs after joining a device and they look like this: ``. This information can also be obtained from the zigbee.db if you want to take the time to query the tables and reconstitute the log entry. I find it easier to just remove and rejoin the device. ZHA entity ids are stable for the most part so it _shouldn't_ disrupt anything you have configured. These need to match what the device reports EXACTLY or zigpy will not match them when a device joins and the handler will not be used for the device. +- Capture the SimpleDescriptor log entries for each endpoint on the device. These can be found in the HA logs after joining a device and they look like this: ``. This information can also be obtained from the zigbee.db if you want to take the time to query the tables and reconstitute the log entry. I find it easier to just remove and rejoin the device. ZHA entity ids are stable for the most part so it _shouldn't_ disrupt anything you have configured. These need to match what the device reports EXACTLY or zigpy will not match them when a device joins and the handler will not be used for the device. You can also obtain this information from the device screen in HA for the device. The `Zigbee Device Signature` button will launch a dialog that contains all of the information necessary to create quirks. -- Create a device class extending CustomDevice or a derivitave of it +- All custom device definitions must extend `CustomDevice` or a derivative of it -- All custom cluster definitions must extend CustomCluster +- All custom cluster definitions must extend `CustomCluster` or a derivative of it + +- Use constants for all attribute values referencing the appropriate labels from Zigpy / HA as necessary - Use an existing handler as a guide. signature and replacement dicts are required. Include the SimpleDescriptor entry for each endpoint in the signature dict above the definition of the endpoint in this format: @@ -16,10 +386,8 @@ # input_clusters=[0, 1, 3, 32, 1026, 1280, 2821] # output_clusters=[25]> ``` - -- Use constants for all attribute values referencing the appropriate labels from Zigpy / HA as necessary - -- how `device_automation_triggers` work: + +### How `device_automation_triggers` work: Device automation triggers are essentially representations of the events that the devices fire in HA. They allow users to use actions in the UI instead of using the raw events. Ex: For the Hue remote - the on button fires this event: @@ -31,7 +399,7 @@ The first part `(SHORT_PRESS, TURN_ON)` corresponds to the txt the user will see in the UI: - image +image The second part is the event data. You only need to supply enough of the event data to uniquely match the event which in this case is just the command for this event fired by this device: `{COMMAND: COMMAND_ON}` @@ -39,4 +407,4 @@ `(SHORT_PRESS, DIM_UP): {COMMAND: COMMAND_STEP, CLUSTER_ID: 8, ENDPOINT_ID: 1, ARGS: [0, 30, 9],}` - you can see a pattern that illustrates how to match a more complex event. In this case the step command is used for the dim up and dim down buttons so we need to match more of the event data to uniquely match the event. + You can see a pattern that illustrates how to match a more complex event. In this case the step command is used for the dim up and dim down buttons so we need to match more of the event data to uniquely match the event. From b87b5961b0a1edc2b7c88f2b6979a0ef40f2ed69 Mon Sep 17 00:00:00 2001 From: Andrew Delikat Date: Sun, 9 Aug 2020 21:21:20 -0400 Subject: [PATCH 048/113] Add quirk for Xiaomi Aqara plug_mitw01 (#432) * Add quirk for Aqara plug_mitw01 * Revert "Add quirk for Aqara plug_mitw01" This reverts commit a1015c39630253659a80f3199336c7221bef74a3. * Add plug_mitw01 identifier to plug_maus01 * black --- zhaquirks/xiaomi/aqara/plug_maus01.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/xiaomi/aqara/plug_maus01.py b/zhaquirks/xiaomi/aqara/plug_maus01.py index 1373d43f37..51bb9b500e 100644 --- a/zhaquirks/xiaomi/aqara/plug_maus01.py +++ b/zhaquirks/xiaomi/aqara/plug_maus01.py @@ -50,7 +50,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) signature = { - MODELS_INFO: [(LUMI, "lumi.plug.maus01")], + MODELS_INFO: [(LUMI, "lumi.plug.maus01"), (LUMI, "lumi.plug.mitw01")], ENDPOINTS: { # Date: Sun, 9 Aug 2020 22:11:35 -0400 Subject: [PATCH 049/113] Sinope floor thermostat quirks (#430) * Added floor thermostat TH11300ZB * Typo fix * Added Sinope TH1500ZB thermostat support * Changes requested --- zhaquirks/sinope/thermostat.py | 62 +++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/zhaquirks/sinope/thermostat.py b/zhaquirks/sinope/thermostat.py index 9f724d1034..3861af1cf3 100644 --- a/zhaquirks/sinope/thermostat.py +++ b/zhaquirks/sinope/thermostat.py @@ -60,7 +60,7 @@ class SinopeTechnologiesThermostat(CustomDevice): # - MODELS_INFO: [(SINOPE, "TH1123ZB"), (SINOPE, "TH1124ZB")], + MODELS_INFO: [(SINOPE, "TH1123ZB"), (SINOPE, "TH1124ZB"), (SINOPE, "TH1500ZB")], ENDPOINTS: { 1: { PROFILE_ID: zha_p.PROFILE_ID, @@ -164,3 +164,63 @@ class SinopeTH1400ZB(SinopeTechnologiesThermostat): } } } + + +class SinopeTH1300ZB(SinopeTechnologiesThermostat): + """TH1300ZB thermostat.""" + + signature = { + # + MODELS_INFO: [(SINOPE, "TH11300ZB")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.THERMOSTAT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + Thermostat.cluster_id, + UserInterface.cluster_id, + TemperatureMeasurement.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + Diagnostic.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + UserInterface.cluster_id, + TemperatureMeasurement.cluster_id, + Metering.cluster_id, + ElectricalMeasurement.cluster_id, + Diagnostic.cluster_id, + SinopeTechnologiesThermostatCluster, + SinopeTechnologiesManufacturerCluster, + ], + OUTPUT_CLUSTERS: [ + Time.cluster_id, + Ota.cluster_id, + SINOPE_MANUFACTURER_CLUSTER_ID, + ], + } + } + } From 18de4c5a2d5f8fbc9b9fd6067c812a59f546b531 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 11 Aug 2020 09:56:55 -0400 Subject: [PATCH 050/113] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6a4bf53871..3686077d41 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.42" +VERSION = "0.0.43" def readme(): From 9982631af919c71a91a36383a2f68c4e19bd2a47 Mon Sep 17 00:00:00 2001 From: Adrien Chevrier Date: Fri, 14 Aug 2020 19:21:59 +0200 Subject: [PATCH 051/113] Support for WXKG06LM (#441) --- zhaquirks/xiaomi/aqara/remote_b186acn01.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zhaquirks/xiaomi/aqara/remote_b186acn01.py b/zhaquirks/xiaomi/aqara/remote_b186acn01.py index d548d9dfeb..c82ea927ed 100644 --- a/zhaquirks/xiaomi/aqara/remote_b186acn01.py +++ b/zhaquirks/xiaomi/aqara/remote_b186acn01.py @@ -77,7 +77,11 @@ def _update_attribute(self, attrid, value): # device_version=1 # input_clusters=[0, 3, 25, 65535, 18] # output_clusters=[0, 4, 3, 5, 25, 65535, 18]> - MODELS_INFO: [(LUMI, "lumi.remote.b186acn01"), (LUMI, "lumi.sensor_86sw1")], + MODELS_INFO: [ + (LUMI, "lumi.remote.b186acn01"), + (LUMI, "lumi.remote.b186acn02"), + (LUMI, "lumi.sensor_86sw1"), + ], ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, From 2866f72e68488e8f11faf2089ef03a25bca87dfa Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sat, 22 Aug 2020 02:37:34 +0200 Subject: [PATCH 052/113] Fixed pycache typo (#456) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 39f56ea21a..76af4991df 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io - on HA > 0.113: /usr/local/lib/python3.8/site-packages/zhaquirks/ - on HA < 0.113: /usr/local/lib/python3.7/site-packages/zhaquirks/ - Copy updated/new quirk to zhaquirks directory: ```cp -a /config/temp/NEW_QUIRK ./``` -- Remove the __py_cache__ folder so it is regenerated ```rm -rf ./__py_cache__/``` +- Remove the __pycache__ folder so it is regenerated ```rm -rf ./__pycache__/``` - Close out the console and restart HA. - Note: The added/update quirk will not survive a HA version update. From 1c6fb592aa89db09a65e794efe6c167ce1f71348 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sat, 22 Aug 2020 02:39:24 +0200 Subject: [PATCH 053/113] OSRAM/LEDVANCE Gardenpole Integration (save_defaults) (#459) * OSRAM/LEDVANCE Gardenpole Integration (save_defaults) * Fixed wrong profile/device type, renamed to proper name * Fixed device type * Remove ZHA import --- zhaquirks/osram/gardenpolesrgbw.py | 74 ++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 zhaquirks/osram/gardenpolesrgbw.py diff --git a/zhaquirks/osram/gardenpolesrgbw.py b/zhaquirks/osram/gardenpolesrgbw.py new file mode 100644 index 0000000000..1597961b20 --- /dev/null +++ b/zhaquirks/osram/gardenpolesrgbw.py @@ -0,0 +1,74 @@ +"""Osram RGBW Gardenpoles.""" +from zigpy.profiles import zll +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + Scenes, +) +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from . import OSRAM, OsramLightCluster +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class GardenpoleRGBW(CustomDevice): + """Osram Gardenpole RGBW-Lightify.""" + + signature = { + # + MODELS_INFO: [(OSRAM, "Gardenpole RGBW-Lightify")], + ENDPOINTS: { + 3: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.EXTENDED_COLOR_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + LightLink.cluster_id, + OsramLightCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 3: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.EXTENDED_COLOR_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + LightLink.cluster_id, + OsramLightCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + } + } From a32a94f7dc71e2c75bd290bdcec101d506f6818e Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sat, 22 Aug 2020 18:17:01 +0200 Subject: [PATCH 054/113] Fix Philips power_on_state by not sending the manufacturer code (#462) --- zhaquirks/philips/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 0e366f516f..dbe714e50f 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -57,7 +57,8 @@ class PowerOnState(t.enum8): class PhilipsOnOffCluster(CustomCluster, OnOff): """Philips OnOff cluster.""" - manufacturer_attributes = {0x4003: ("power_on_state", PowerOnState)} + attributes = OnOff.attributes.copy() + attributes.update({0x4003: ("power_on_state", PowerOnState)}) class PhilipsBasicCluster(CustomCluster, Basic): From 1235e8434a68db70590cdb9ee77dbd0477f8e819 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sat, 22 Aug 2020 23:32:11 +0200 Subject: [PATCH 055/113] Philips Hue Go/Bloom support (power_on_state) (#460) * Philips Hue LivingColors Bloom support * Philips Hue Go support * Fix SimpleDescriptor comment for device type of Hue Bloom * Renamed PhilipsLCT to ZLLExtendedColorLight, moved LLC020 to ZLLExtendedColorLight --- zhaquirks/philips/llc011.py | 93 +++++++++++++++++++ .../{lct.py => zllextendedcolorlight.py} | 7 +- 2 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 zhaquirks/philips/llc011.py rename zhaquirks/philips/{lct.py => zllextendedcolorlight.py} (94%) diff --git a/zhaquirks/philips/llc011.py b/zhaquirks/philips/llc011.py new file mode 100644 index 0000000000..29835bd84b --- /dev/null +++ b/zhaquirks/philips/llc011.py @@ -0,0 +1,93 @@ +"""Quirk for Phillips Hue LivingColors Bloom.""" +from zigpy.profiles import zll +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + OnOff, + Basic, + Identify, + LevelControl, + Scenes, + Groups, + Ota, + GreenPowerProxy, +) + +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from zhaquirks.const import ( + ENDPOINTS, + OUTPUT_CLUSTERS, + INPUT_CLUSTERS, + DEVICE_TYPE, + PROFILE_ID, + MODELS_INFO, +) +from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster + + +class PhilipsLLC011(CustomDevice): + """Philips LLC device.""" + + signature = { + MODELS_INFO: [(PHILIPS, "LLC011")], + ENDPOINTS: { + 11: { + # Date: Wed, 2 Sep 2020 13:14:48 +0200 Subject: [PATCH 056/113] Refactored Philips ZHA lights to ZHAExtendedColorLight, new bulb support (#467) * Refactored Philips ZHA lights to ZHAExtendedColorLight, added LCA001 + Bloom support * Added Philips Hue Go (2nd Gen) Support --- zhaquirks/philips/lcb001.py | 95 ------------------- .../{lca003.py => zhaextendedcolorlight.py} | 79 ++++++++++++++- 2 files changed, 75 insertions(+), 99 deletions(-) delete mode 100644 zhaquirks/philips/lcb001.py rename zhaquirks/philips/{lca003.py => zhaextendedcolorlight.py} (50%) diff --git a/zhaquirks/philips/lcb001.py b/zhaquirks/philips/lcb001.py deleted file mode 100644 index 4783d1a674..0000000000 --- a/zhaquirks/philips/lcb001.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Quirk for Phillips LCB001.""" -from zigpy.profiles import zha -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import ( - OnOff, - Basic, - Identify, - LevelControl, - Scenes, - Groups, - Ota, - GreenPowerProxy, -) - -from zigpy.zcl.clusters.lighting import Color -from zigpy.zcl.clusters.lightlink import LightLink - -from zhaquirks.const import ( - ENDPOINTS, - OUTPUT_CLUSTERS, - INPUT_CLUSTERS, - DEVICE_TYPE, - PROFILE_ID, - MODELS_INFO, -) -from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster - - -class PhilipsLCB001(CustomDevice): - """Philips LCB001 device.""" - - signature = { - MODELS_INFO: [(PHILIPS, "LCB001")], - ENDPOINTS: { - 11: { - # - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.EXTENDED_COLOR_LIGHT, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - LightLink.cluster_id, - 64514, - Color.cluster_id, - 64513, - ], - OUTPUT_CLUSTERS: [Ota.cluster_id], - }, - 242: { - # - PROFILE_ID: 41440, - DEVICE_TYPE: 97, - INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 11: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.EXTENDED_COLOR_LIGHT, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - PhilipsOnOffCluster, - LevelControl.cluster_id, - LightLink.cluster_id, - 64514, - Color.cluster_id, - 64513, - ], - OUTPUT_CLUSTERS: [Ota.cluster_id], - }, - 242: { - PROFILE_ID: 41440, - DEVICE_TYPE: 97, - INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - }, - } - } diff --git a/zhaquirks/philips/lca003.py b/zhaquirks/philips/zhaextendedcolorlight.py similarity index 50% rename from zhaquirks/philips/lca003.py rename to zhaquirks/philips/zhaextendedcolorlight.py index f027c1af3d..4d6cd5bceb 100644 --- a/zhaquirks/philips/lca003.py +++ b/zhaquirks/philips/zhaextendedcolorlight.py @@ -1,4 +1,4 @@ -"""Quirk for Phillips LCA003.""" +"""Quirk for Phillips extended color bulbs.""" from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( @@ -26,11 +26,11 @@ from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster -class PhilipsLCA003(CustomDevice): - """Philips LCA003 device.""" +class ZHAExtendedColorLight(CustomDevice): + """Philips ZigBee HomeAutomation extended color bulb device.""" signature = { - MODELS_INFO: [(PHILIPS, "LCA003")], + MODELS_INFO: [(PHILIPS, "LCA001"), (PHILIPS, "LCA003"), (PHILIPS, "LCB001")], ENDPOINTS: { 11: { # + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.EXTENDED_COLOR_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + LightLink.cluster_id, + Color.cluster_id, + 64513, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + 242: { + # + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 11: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.EXTENDED_COLOR_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + PhilipsOnOffCluster, + LevelControl.cluster_id, + LightLink.cluster_id, + Color.cluster_id, + 64513, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + 242: { + PROFILE_ID: 41440, + DEVICE_TYPE: 97, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], + }, + } + } From 408b14accfcb4dd4c447fcf24f63cf9847b790a5 Mon Sep 17 00:00:00 2001 From: Ulrich Doewich Date: Wed, 2 Sep 2020 07:35:11 -0400 Subject: [PATCH 057/113] ORVIBO switch, dimmer and motion sensor support (#463) * ORVIBO switch, dimmer and motion sensor support * quirk not needed * comment style change * adds missing period * update format Co-authored-by: David Mulcahey --- zhaquirks/orvibo/__init__.py | 72 +++++++++++++++++++++++++++++ zhaquirks/orvibo/dimmer.py | 59 ++++++++++++++++++++++++ zhaquirks/orvibo/motion.py | 87 ++++++++++++++++++++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 zhaquirks/orvibo/__init__.py create mode 100644 zhaquirks/orvibo/dimmer.py create mode 100644 zhaquirks/orvibo/motion.py diff --git a/zhaquirks/orvibo/__init__.py b/zhaquirks/orvibo/__init__.py new file mode 100644 index 0000000000..7e89558f16 --- /dev/null +++ b/zhaquirks/orvibo/__init__.py @@ -0,0 +1,72 @@ +"""Module for ORVIBO quirks implementations.""" + +import asyncio + +from zigpy.quirks import CustomCluster +from zigpy.zcl.clusters.measurement import OccupancySensing +from zigpy.zcl.clusters.security import IasZone + +from .. import LocalDataCluster +from ..const import CLUSTER_COMMAND, OFF, ON, ZONE_STATE + +ORVIBO = "欧瑞博" +ORVIBO_LATIN = "ORVIBO" + +OCCUPANCY_STATE = 0 +OCCUPANCY_EVENT = "occupancy_event" +MOTION_TYPE = 0x000D +ZONE_TYPE = 0x0001 + +MOTION_TIME = 30 +OCCUPANCY_TIME = 600 + + +class OccupancyCluster(LocalDataCluster, OccupancySensing): + """Occupancy cluster.""" + + cluster_id = OccupancySensing.cluster_id + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self._timer_handle = None + self.endpoint.device.occupancy_bus.add_listener(self) + + def occupancy_event(self): + """Occupancy event.""" + self._update_attribute(OCCUPANCY_STATE, ON) + + if self._timer_handle: + self._timer_handle.cancel() + + loop = asyncio.get_event_loop() + self._timer_handle = loop.call_later(OCCUPANCY_TIME, self._turn_off) + + def _turn_off(self): + self._timer_handle = None + self._update_attribute(OCCUPANCY_STATE, OFF) + + +class MotionCluster(CustomCluster, IasZone): + """Motion cluster.""" + + cluster_id = IasZone.cluster_id + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self._timer_handle = None + + def handle_cluster_request(self, tsn, command_id, args): + """Handle the cluster command.""" + if command_id == 0: + if self._timer_handle: + self._timer_handle.cancel() + loop = asyncio.get_event_loop() + self._timer_handle = loop.call_later(MOTION_TIME, self._turn_off) + self.endpoint.device.occupancy_bus.listener_event(OCCUPANCY_EVENT) + + def _turn_off(self): + self._timer_handle = None + self.listener_event(CLUSTER_COMMAND, 999, 0, [0, 0, 0, 0]) + self._update_attribute(ZONE_STATE, OFF) diff --git a/zhaquirks/orvibo/dimmer.py b/zhaquirks/orvibo/dimmer.py new file mode 100644 index 0000000000..b65f1062ab --- /dev/null +++ b/zhaquirks/orvibo/dimmer.py @@ -0,0 +1,59 @@ +"""ORVIBO dimmers.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Groups, LevelControl, OnOff, Scenes +from zigpy.zcl.clusters.lighting import Color + +from . import ORVIBO +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class T10D1ZW(CustomDevice): + """T10D1ZW in-wall dimmer.""" + + signature = { + # + MODELS_INFO: [(ORVIBO, "abb71ca5fe1846f185cfbda554046cce")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + OUTPUT_CLUSTERS: [Basic.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + ], + OUTPUT_CLUSTERS: [Basic.cluster_id], + } + } + } diff --git a/zhaquirks/orvibo/motion.py b/zhaquirks/orvibo/motion.py new file mode 100644 index 0000000000..3816caf9c4 --- /dev/null +++ b/zhaquirks/orvibo/motion.py @@ -0,0 +1,87 @@ +""" +ORVIBO motion sensors. + +Based on Konke motion sensor code. +""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Identify, + PowerConfiguration, + Groups, + Scenes, +) +from zigpy.zcl.clusters.security import IasZone + +from . import ORVIBO_LATIN, OccupancyCluster, MotionCluster +from .. import Bus, PowerConfigurationCluster +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +ORVIBO_CLUSTER_ID = 0xFFFF + + +class SN10ZW(CustomDevice): + """SN10ZW motion sensor.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.occupancy_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + # + MODELS_INFO: [(ORVIBO_LATIN, "895a2d80097f4ae2b2d40500d5e03dcc")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + IasZone.cluster_id, + ORVIBO_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfigurationCluster, + Identify.cluster_id, + OccupancyCluster, + MotionCluster, + ], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + ], + } + } + } From 9d0a38632cc2a6d53f782dc82becf156d2352760 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 4 Sep 2020 19:17:13 -0400 Subject: [PATCH 058/113] Enable tox and tests (#471) * Fix tests. * Fixture to create device from quirk. * Switch lumi neutral to use new fixture. * Pydocstyle D404 * Pydocstyle adjustments * Switch to py37 * If lint fails on format, show what's causing it * Disable check_dirty script --- .travis.yml | 11 +++- requirements_test_all.txt | 3 - script/check_format | 1 + setup.cfg | 8 ++- tests/conftest.py | 105 +++++++++++++++++++++++++++++++++ tests/test_ctrl_neutral1.py | 30 +++------- tests/test_kof.py | 5 +- tests/test_xiaomi.py | 4 +- tox.ini | 3 +- zhaquirks/__init__.py | 3 +- zhaquirks/kof/kof_mr101z.py | 9 +-- zhaquirks/orvibo/motion.py | 3 +- zhaquirks/sinope/light.py | 3 +- zhaquirks/sinope/thermostat.py | 3 +- zhaquirks/zen/thermostat.py | 2 +- 15 files changed, 144 insertions(+), 49 deletions(-) create mode 100644 tests/conftest.py diff --git a/.travis.yml b/.travis.yml index 5fc5866351..9d106de634 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,17 @@ dist: xenial matrix: fast_finish: true include: - - python: "3.6.1" + - python: "3.7" env: TOXENV=lint - - python: "3.6.1" + - python: "3.7" env: TOXENV=pylint + - python: "3.7" + env: TOXENV=py37 + sudo: true + - python: "3.8" + env: TOXENV=py38 cache: pip install: pip install -U tox language: python -script: travis_wait 40 tox --develop \ No newline at end of file +script: travis_wait 40 tox --develop diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e717df40aa..f1814082b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,6 +19,3 @@ pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.1.2 requests_mock==1.6.0 - -zigpy-homeassistant==0.18.1 -homeassistant diff --git a/script/check_format b/script/check_format index cc38a2c279..0b925b6d98 100755 --- a/script/check_format +++ b/script/check_format @@ -6,4 +6,5 @@ cd "$(dirname "$0")/.." black \ --check \ --fast \ + --diff \ zhaquirks tests script *.py diff --git a/setup.cfg b/setup.cfg index c7a790ef5c..4623728436 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ forced_separate = tests combine_as_imports = true [mypy] -python_version = 3.6 +python_version = 3.7 check_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_calls = true @@ -52,3 +52,9 @@ warn_return_any = true warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true + +[pydocstyle] +ignore = + D202, + D203, + D213 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..20c962cd95 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,105 @@ +"""Fixtures for all tests.""" + +from asynctest import CoroutineMock +import pytest +import zigpy.device +import zigpy.application +import zigpy.types + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class MockApp(zigpy.application.ControllerApplication): + """App Controller.""" + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self._ieee = zigpy.types.EUI64(b"Zigbee78") + self._nwk = zigpy.types.NWK(0x0000) + + async def probe(self, *args): + """Probe method.""" + return True + + async def shutdown(self): + """Mock shutdown.""" + + async def startup(self, *args): + """Mock startup.""" + + async def permit_ncp(self, *args): + """Mock permit ncp.""" + + mrequest = CoroutineMock() + request = CoroutineMock() + + +@pytest.fixture(name="MockAppController") +def app_controller_mock(): + """App controller mock.""" + config = {"device": {"path": "/dev/ttyUSB0"}, "database": None} + config = MockApp.SCHEMA(config) + app = MockApp(config) + return app + + +@pytest.fixture +def ieee_mock(): + """Return a static ieee.""" + return zigpy.types.EUI64([1, 2, 3, 4, 5, 6, 7, 8]) + + +@pytest.fixture +def zigpy_device_mock(MockAppController, ieee_mock): + """Zigpy device mock.""" + + def _dev(ieee=None, nwk=zigpy.types.NWK(0x1234)): + if ieee is None: + ieee = ieee_mock + device = MockAppController.add_device(ieee, nwk) + return device + + return _dev + + +@pytest.fixture +def zigpy_device_from_quirk(MockAppController, ieee_mock): + """Create zigpy device from Quirk's signature.""" + + def _dev(quirk, ieee=None, nwk=zigpy.types.NWK(0x1234)): + if ieee is None: + ieee = ieee_mock + models_info = quirk.signature.get( + MODELS_INFO, (("Mock Manufacturer", "Mock Model"),) + ) + manufacturer, model = models_info[0] + + raw_device = zigpy.device.Device(MockAppController, ieee, nwk) + raw_device.manufacturer = manufacturer + raw_device.model = model + + endpoints = quirk.signature.get(ENDPOINTS, {}) + for ep_id, ep_data in endpoints.items(): + ep = raw_device.add_endpoint(ep_id) + ep.profile_id = ep_data.get(PROFILE_ID, 0x0260) + ep.device_type = ep_data.get(DEVICE_TYPE, 0xFEDB) + in_clusters = ep_data.get(INPUT_CLUSTERS, []) + for cluster_id in in_clusters: + ep.add_input_cluster(cluster_id) + out_clusters = ep_data.get(OUTPUT_CLUSTERS, []) + for cluster_id in out_clusters: + ep.add_output_cluster(cluster_id) + device = quirk(MockAppController, ieee, nwk, raw_device) + MockAppController.devices[ieee] = device + + return device + + return _dev diff --git a/tests/test_ctrl_neutral1.py b/tests/test_ctrl_neutral1.py index 96436bda43..4ca080e1a6 100644 --- a/tests/test_ctrl_neutral1.py +++ b/tests/test_ctrl_neutral1.py @@ -1,9 +1,5 @@ """Tests for xiaomi.""" from unittest import mock -from unittest.mock import call - -import zigpy.application -from zigpy.device import Device from zhaquirks.xiaomi.aqara.ctrl_neutral import CtrlNeutral @@ -24,30 +20,20 @@ # zigbee-herdsman:adapter:zStack:unpi:writer --> frame [254,13,36,1,31,255,2,1,6,0,16,0,30,3,1,7,0,198] -def test_ctrl_neutral(): +def test_ctrl_neutral(zigpy_device_from_quirk): """Test ctrl neutral 1 sends correct request.""" - sec = 8 - ieee = 0 - nwk = 1234 - data = b"\x01\x08\x01" + data = b"\x01\x01\x01" cluster = 6 src_ep = 1 dst_ep = 2 - app = zigpy.application.ControllerApplication() - app.request = mock.MagicMock() - app.get_sequence = mock.MagicMock(return_value=sec) - - rep = Device(app, ieee, nwk) - rep.add_endpoint(1) - rep.add_endpoint(2) - rep.add_endpoint(3) - - dev = CtrlNeutral(app, ieee, nwk, rep) + dev = zigpy_device_from_quirk(CtrlNeutral) dev.request = mock.MagicMock() dev[2].in_clusters[cluster].command(1) - assert dev.request.call_args == call( - 260, cluster, src_ep, dst_ep, sec, data, expect_reply=True - ) + assert dev.request.call_args[0][0] == 260 + assert dev.request.call_args[0][1] == cluster + assert dev.request.call_args[0][2] == src_ep + assert dev.request.call_args[0][3] == dst_ep + assert dev.request.call_args[0][5] == data diff --git a/tests/test_kof.py b/tests/test_kof.py index 716ac0946e..16cb65053b 100644 --- a/tests/test_kof.py +++ b/tests/test_kof.py @@ -4,12 +4,15 @@ import zigpy.device import zigpy.endpoint import zigpy.quirks +import zhaquirks.kof.kof_mr101z def test_kof_no_reply(): """Test KOF No reply.""" - class TestCluster(zigpy.quirks.kof.NoReplyMixin, zigpy.quirks.CustomCluster): + class TestCluster( + zhaquirks.kof.kof_mr101z.NoReplyMixin, zigpy.quirks.CustomCluster + ): """Test Cluster Class.""" cluster_id = 0x1234 diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 61a5498157..792d8547f5 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -14,7 +14,7 @@ def test_basic_cluster_deserialize_wrong_len(): data += b"\x02\n!\x00\x00d\x10\x01" deserialized = cluster.deserialize(data) - assert deserialized[3] + assert deserialized[1] def test_basic_cluster_deserialize_wrong_len_2(): @@ -26,4 +26,4 @@ def test_basic_cluster_deserialize_wrong_len_2(): data += b"\x00\x14\x00\x00\x08!\x04\x02\n!\x00\x00d\x10\x01" deserialized = cluster.deserialize(data) - assert deserialized[3] + assert deserialized[1] diff --git a/tox.ini b/tox.ini index 0c3fc7ba8d..69e8982d5b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,11 @@ [tox] -envlist = py36, lint, pylint +envlist = py37, py38, lint, pylint skip_missing_interpreters = True [testenv] basepython = {env:PYTHON3_PATH:python3} commands = pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar {posargs} - {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 2536ff0fff..a631be4b0f 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -98,8 +98,7 @@ def _update_attribute(self, attrid, value): class GroupBoundCluster(CustomCluster): - """ - Cluster that can only bind to a group instead of direct to hub. + """Cluster that can only bind to a group instead of direct to hub. Binding this cluster results in binding to a group that the coordinator is a member of. diff --git a/zhaquirks/kof/kof_mr101z.py b/zhaquirks/kof/kof_mr101z.py index d728f3a4c0..0ad29d3ae8 100644 --- a/zhaquirks/kof/kof_mr101z.py +++ b/zhaquirks/kof/kof_mr101z.py @@ -1,5 +1,4 @@ -""" -This module handles quirks of the King of Fans MR101Z ceiling fan receiver. +"""Module to handle quirks of the King of Fans MR101Z ceiling fan receiver. The King of Fans ceiling fan receiver does not generate default replies. This module overrides all server commands that do not have a mandatory reply to not @@ -21,8 +20,7 @@ class NoReplyMixin: - """ - A simple mixin. + """A simple mixin. Allows a cluster to have configureable list of command ids that do not generate an explicit reply. @@ -31,8 +29,7 @@ class NoReplyMixin: void_input_commands = [] def command(self, command, *args, manufacturer=None, expect_reply=None): - """ - Override the default Cluster command. + """Override the default Cluster command. expect_reply behavior is based on void_input_commands. Note that this method changes the default value of diff --git a/zhaquirks/orvibo/motion.py b/zhaquirks/orvibo/motion.py index 3816caf9c4..4870ec5da2 100644 --- a/zhaquirks/orvibo/motion.py +++ b/zhaquirks/orvibo/motion.py @@ -1,5 +1,4 @@ -""" -ORVIBO motion sensors. +"""ORVIBO motion sensors. Based on Konke motion sensor code. """ diff --git a/zhaquirks/sinope/light.py b/zhaquirks/sinope/light.py index 8e008fdcb4..67aae6987b 100644 --- a/zhaquirks/sinope/light.py +++ b/zhaquirks/sinope/light.py @@ -1,5 +1,4 @@ -""" -This module handles quirks of the Sinopé Technologies light SW2500ZB and dimmer DM2500ZB. +"""Module to handle quirks of the Sinopé Technologies light SW2500ZB and dimmer DM2500ZB. Manufacturer specific cluster implements attributes to control displaying setting occupancy on/off. diff --git a/zhaquirks/sinope/thermostat.py b/zhaquirks/sinope/thermostat.py index 3861af1cf3..b70364fe36 100644 --- a/zhaquirks/sinope/thermostat.py +++ b/zhaquirks/sinope/thermostat.py @@ -1,5 +1,4 @@ -""" -This module handles quirks of the Sinopé Technologies thermostat. +"""Module to handle quirks of the Sinopé Technologies thermostat. manufacturer specific cluster implements attributes to control displaying of outdoor temperature, setting occupancy on/off and setting device time. diff --git a/zhaquirks/zen/thermostat.py b/zhaquirks/zen/thermostat.py index c5fc508b78..8ada1cde87 100644 --- a/zhaquirks/zen/thermostat.py +++ b/zhaquirks/zen/thermostat.py @@ -1,4 +1,4 @@ -"""This module handles quirks of the Zen Within thermostat.""" +"""Module to handle quirks of the Zen Within thermostat.""" import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomDevice From 44a61caa3621624af2aed64e364837475c33a6f0 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 4 Sep 2020 20:40:11 -0400 Subject: [PATCH 059/113] Refactor self reset motion/occupancy quirk clusters (#473) * Refactor self reset Occupancy and Motion clusters. Implement common self reset Occupancy and Motion custom clusters. MotionWithReset class -- self reset motion cluster, optionally send event on device occupancy_bus. MotionOnEvent class -- self reset motion cluster, based on event received on device motion_bus. OccupancyOnEvent class -- self reset occupancy cluster, based on event received on device occupancy_bus. OccupancyWithReset class -- self reset occupancy cluster, sends an event to device motion_bus. * Update tests. * Explicitly set the rest times --- tests/__init__.py | 1 + tests/common.py | 23 +++++++ tests/test_konke.py | 55 ++++++++++++++++ tests/test_orvibo.py | 53 +++++++++++++++ tests/test_xiaomi.py | 58 +++++++++++++++++ zhaquirks/__init__.py | 115 +++++++++++++++++++++++++++++++++ zhaquirks/hivehome/__init__.py | 28 +------- zhaquirks/konke/__init__.py | 64 ++---------------- zhaquirks/orvibo/__init__.py | 65 ++----------------- zhaquirks/trust/__init__.py | 29 +-------- zhaquirks/xiaomi/__init__.py | 62 ++---------------- 11 files changed, 327 insertions(+), 226 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/common.py create mode 100644 tests/test_konke.py create mode 100644 tests/test_orvibo.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..8418ddb1fd --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for zha quirks.""" diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 0000000000..319cd19dff --- /dev/null +++ b/tests/common.py @@ -0,0 +1,23 @@ +"""Quirks common helpers.""" + + +ZCL_IAS_MOTION_COMMAND = b"\t!\x00\x01\x00\x00\x00\x00\x00" +ZCL_OCC_ATTR_RPT_OCC = b"\x18d\n\x00\x00\x18\x01" + + +class ClusterListener: + """Generic cluster listener.""" + + def __init__(self, cluster): + """Init instance.""" + self.cluster_commands = [] + self.attribute_updates = [] + cluster.add_listener(self) + + def attribute_updated(self, attr_id, value): + """Attribute updated listener.""" + self.attribute_updates.append((attr_id, value)) + + def cluster_command(self, tsn, commdand_id, args): + """Command received listener.""" + self.cluster_commands.append((tsn, commdand_id, args)) diff --git a/tests/test_konke.py b/tests/test_konke.py new file mode 100644 index 0000000000..e1a2d2e2d7 --- /dev/null +++ b/tests/test_konke.py @@ -0,0 +1,55 @@ +"""Tests for konke quirks.""" + +import asyncio +from unittest import mock + +import pytest + +from zhaquirks.const import OFF, ON, ZONE_STATE +import zhaquirks.konke.motion + +from tests.common import ZCL_IAS_MOTION_COMMAND, ClusterListener + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "quirk", (zhaquirks.konke.motion.KonkeMotion, zhaquirks.konke.motion.KonkeMotionB) +) +async def test_konke_motion(zigpy_device_from_quirk, quirk): + """Test konke motion sensor.""" + + motion_dev = zigpy_device_from_quirk(quirk) + + motion_cluster = motion_dev.endpoints[1].ias_zone + motion_listener = ClusterListener(motion_cluster) + + occupancy_cluster = motion_dev.endpoints[1].occupancy + occupancy_listener = ClusterListener(occupancy_cluster) + + p1 = mock.patch.object(motion_cluster, "reset_s", 0) + p2 = mock.patch.object(occupancy_cluster, "reset_s", 0) + # send motion on IAS zone command + hdr, args = motion_cluster.deserialize(ZCL_IAS_MOTION_COMMAND) + with p1, p2: + motion_cluster.handle_message(hdr, args) + + assert len(motion_listener.cluster_commands) == 1 + assert len(motion_listener.attribute_updates) == 0 + assert motion_listener.cluster_commands[0][1] == ZONE_STATE + assert motion_listener.cluster_commands[0][2][0] == ON + + assert len(occupancy_listener.cluster_commands) == 0 + assert len(occupancy_listener.attribute_updates) == 1 + assert occupancy_listener.attribute_updates[0][0] == 0x0000 + assert occupancy_listener.attribute_updates[0][1] == 1 + + await asyncio.sleep(0.1) + + assert len(motion_listener.cluster_commands) == 2 + assert motion_listener.cluster_commands[1][1] == ZONE_STATE + assert motion_listener.cluster_commands[1][2][0] == OFF + + assert len(occupancy_listener.cluster_commands) == 0 + assert len(occupancy_listener.attribute_updates) == 2 + assert occupancy_listener.attribute_updates[1][0] == 0x0000 + assert occupancy_listener.attribute_updates[1][1] == 0 diff --git a/tests/test_orvibo.py b/tests/test_orvibo.py new file mode 100644 index 0000000000..e2a74ddd7e --- /dev/null +++ b/tests/test_orvibo.py @@ -0,0 +1,53 @@ +"""Tests for Orvibo quirks.""" + +import asyncio +from unittest import mock + +import pytest + +from zhaquirks.const import OFF, ON, ZONE_STATE +import zhaquirks.orvibo.motion + +from tests.common import ZCL_IAS_MOTION_COMMAND, ClusterListener + + +@pytest.mark.asyncio +@pytest.mark.parametrize("quirk", (zhaquirks.orvibo.motion.SN10ZW,)) +async def test_konke_motion(zigpy_device_from_quirk, quirk): + """Test Orvibo motion sensor.""" + + motion_dev = zigpy_device_from_quirk(quirk) + + motion_cluster = motion_dev.endpoints[1].ias_zone + motion_listener = ClusterListener(motion_cluster) + + occupancy_cluster = motion_dev.endpoints[1].occupancy + occupancy_listener = ClusterListener(occupancy_cluster) + + p1 = mock.patch.object(motion_cluster, "reset_s", 0) + p2 = mock.patch.object(occupancy_cluster, "reset_s", 0) + # send motion on IAS zone command + hdr, args = motion_cluster.deserialize(ZCL_IAS_MOTION_COMMAND) + with p1, p2: + motion_cluster.handle_message(hdr, args) + + assert len(motion_listener.cluster_commands) == 1 + assert len(motion_listener.attribute_updates) == 0 + assert motion_listener.cluster_commands[0][1] == ZONE_STATE + assert motion_listener.cluster_commands[0][2][0] == ON + + assert len(occupancy_listener.cluster_commands) == 0 + assert len(occupancy_listener.attribute_updates) == 1 + assert occupancy_listener.attribute_updates[0][0] == 0x0000 + assert occupancy_listener.attribute_updates[0][1] == 1 + + await asyncio.sleep(0.1) + + assert len(motion_listener.cluster_commands) == 2 + assert motion_listener.cluster_commands[1][1] == ZONE_STATE + assert motion_listener.cluster_commands[1][2][0] == OFF + + assert len(occupancy_listener.cluster_commands) == 0 + assert len(occupancy_listener.attribute_updates) == 2 + assert occupancy_listener.attribute_updates[1][0] == 0x0000 + assert occupancy_listener.attribute_updates[1][1] == 0 diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 792d8547f5..6e9a18fa83 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -1,7 +1,16 @@ """Tests for xiaomi.""" +import asyncio from unittest import mock +import pytest + +from zhaquirks.const import OFF, ON, ZONE_STATE from zhaquirks.xiaomi import BasicCluster +import zhaquirks.xiaomi.aqara.motion_aq2 +import zhaquirks.xiaomi.aqara.motion_aq2b +import zhaquirks.xiaomi.mija.motion + +from tests.common import ZCL_OCC_ATTR_RPT_OCC, ClusterListener def test_basic_cluster_deserialize_wrong_len(): @@ -27,3 +36,52 @@ def test_basic_cluster_deserialize_wrong_len_2(): deserialized = cluster.deserialize(data) assert deserialized[1] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "quirk", + ( + zhaquirks.xiaomi.aqara.motion_aq2.MotionAQ2, + zhaquirks.xiaomi.aqara.motion_aq2b.MotionAQ2, + zhaquirks.xiaomi.mija.motion.Motion, + ), +) +async def test_konke_motion(zigpy_device_from_quirk, quirk): + """Test Orvibo motion sensor.""" + + motion_dev = zigpy_device_from_quirk(quirk) + + motion_cluster = motion_dev.endpoints[1].ias_zone + motion_listener = ClusterListener(motion_cluster) + + occupancy_cluster = motion_dev.endpoints[1].occupancy + occupancy_listener = ClusterListener(occupancy_cluster) + + p1 = mock.patch.object(motion_cluster, "reset_s", 0) + p2 = mock.patch.object(occupancy_cluster, "reset_s", 0) + # send motion on IAS zone command + hdr, args = occupancy_cluster.deserialize(ZCL_OCC_ATTR_RPT_OCC) + with p1, p2: + occupancy_cluster.handle_message(hdr, args) + + assert len(motion_listener.cluster_commands) == 1 + assert len(motion_listener.attribute_updates) == 0 + assert motion_listener.cluster_commands[0][1] == ZONE_STATE + assert motion_listener.cluster_commands[0][2][0] == ON + + assert len(occupancy_listener.cluster_commands) == 0 + assert len(occupancy_listener.attribute_updates) == 1 + assert occupancy_listener.attribute_updates[0][0] == 0x0000 + assert occupancy_listener.attribute_updates[0][1] == 1 + + await asyncio.sleep(0.1) + + assert len(motion_listener.cluster_commands) == 2 + assert motion_listener.cluster_commands[1][1] == ZONE_STATE + assert motion_listener.cluster_commands[1][2][0] == OFF + + assert len(occupancy_listener.cluster_commands) == 0 + assert len(occupancy_listener.attribute_updates) == 2 + assert occupancy_listener.attribute_updates[1][0] == 0x0000 + assert occupancy_listener.attribute_updates[1][1] == 0 diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index a631be4b0f..679c81f5a7 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -1,4 +1,5 @@ """Quirks implementations for the ZHA component of Homeassistant.""" +import asyncio import logging import importlib import pkgutil @@ -7,18 +8,27 @@ from zigpy.util import ListenableMixin from zigpy.zcl import foundation from zigpy.zcl.clusters.general import PowerConfiguration +from zigpy.zcl.clusters.security import IasZone +from zigpy.zcl.clusters.measurement import OccupancySensing from zigpy.zdo import types as zdotypes from .const import ( ATTRIBUTE_ID, ATTRIBUTE_NAME, + CLUSTER_COMMAND, COMMAND_ATTRIBUTE_UPDATED, UNKNOWN, VALUE, ZHA_SEND_EVENT, + ZONE_STATE, + OFF, + ON, + MOTION_EVENT, ) _LOGGER = logging.getLogger(__name__) +OCCUPANCY_STATE = 0 +OCCUPANCY_EVENT = "occupancy_event" class Bus(ListenableMixin): @@ -181,6 +191,111 @@ def _calculate_battery_percentage(self, raw_value): return percent +class _Motion(CustomCluster, IasZone): + """Self reset Motion cluster.""" + + reset_s: int = 30 + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self._loop = asyncio.get_running_loop() + self._timer_handle = None + + def _turn_off(self): + self._timer_handle = None + _LOGGER.debug("%s - Resetting motion sensor", self.endpoint.device.ieee) + self.listener_event(CLUSTER_COMMAND, 253, ZONE_STATE, [OFF, 0, 0, 0]) + self._update_attribute(ZONE_STATE, OFF) + + +class MotionWithReset(_Motion): + """Self reset Motion cluster. + + Optionally send event over device bus. + """ + + send_occupancy_event: bool = False + + def handle_cluster_request(self, tsn, command_id, args): + """Handle the cluster command.""" + if command_id == ZONE_STATE: + if self._timer_handle: + self._timer_handle.cancel() + self._timer_handle = self._loop.call_later(self.reset_s, self._turn_off) + if self.send_occupancy_event: + self.endpoint.device.occupancy_bus.listener_event(OCCUPANCY_EVENT) + + +class MotionOnEvent(_Motion): + """Motion based on received events from occupancy.""" + + reset_s: int = 120 + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.motion_bus.add_listener(self) + + def motion_event(self): + """Motion event.""" + super().listener_event(CLUSTER_COMMAND, 254, ZONE_STATE, [ON, 0, 0, 0]) + + _LOGGER.debug("%s - Received motion event message", self.endpoint.device.ieee) + + if self._timer_handle: + self._timer_handle.cancel() + + self._timer_handle = self._loop.call_later(self.reset_s, self._turn_off) + + +class _Occupancy(CustomCluster, OccupancySensing): + """Self reset Occupancy cluster.""" + + reset_s: int = 600 + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self._timer_handle = None + self._loop = asyncio.get_running_loop() + + def _turn_off(self): + self._timer_handle = None + self._update_attribute(OCCUPANCY_STATE, OFF) + + +class OccupancyOnEvent(_Occupancy): + """Self reset occupancy from bus.""" + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.occupancy_bus.add_listener(self) + + def occupancy_event(self): + """Occupancy event.""" + self._update_attribute(OCCUPANCY_STATE, ON) + + if self._timer_handle: + self._timer_handle.cancel() + + self._timer_handle = self._loop.call_later(self.reset_s, self._turn_off) + + +class OccupancyWithReset(_Occupancy): + """Self reset Occupancy cluster and send event on motion bus.""" + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + + if attrid == OCCUPANCY_STATE and value == ON: + if self._timer_handle: + self._timer_handle.cancel() + self.endpoint.device.motion_bus.listener_event(MOTION_EVENT) + self._timer_handle = self._loop.call_later(self.reset_s, self._turn_off) + + NAME = __name__ PATH = __path__ for importer, modname, ispkg in pkgutil.walk_packages(path=PATH, prefix=NAME + "."): diff --git a/zhaquirks/hivehome/__init__.py b/zhaquirks/hivehome/__init__.py index 68a13be23a..61d0d2b6a9 100644 --- a/zhaquirks/hivehome/__init__.py +++ b/zhaquirks/hivehome/__init__.py @@ -1,33 +1,11 @@ """Hive Home.""" -import asyncio -from zigpy.quirks import CustomCluster -from zigpy.zcl.clusters.security import IasZone - -from ..const import CLUSTER_COMMAND, OFF, ZONE_STATE +from .. import MotionWithReset HIVEHOME = "HiveHome.com" -class MotionCluster(CustomCluster, IasZone): +class MotionCluster(MotionWithReset): """Motion cluster.""" - cluster_id = IasZone.cluster_id - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self._timer_handle = None - - def handle_cluster_request(self, tsn, command_id, args): - """Handle the cluster command.""" - if command_id == 0: - if self._timer_handle: - self._timer_handle.cancel() - loop = asyncio.get_event_loop() - self._timer_handle = loop.call_later(30, self._turn_off) - - def _turn_off(self): - self._timer_handle = None - self.listener_event(CLUSTER_COMMAND, 999, 0, [0, 0, 0, 0]) - self._update_attribute(ZONE_STATE, OFF) + reset_s: int = 30 diff --git a/zhaquirks/konke/__init__.py b/zhaquirks/konke/__init__.py index 8e71e1dbc6..ca32a62cb0 100644 --- a/zhaquirks/konke/__init__.py +++ b/zhaquirks/konke/__init__.py @@ -1,70 +1,18 @@ """Konke sensors.""" -import asyncio - -from zigpy.quirks import CustomCluster -from zigpy.zcl.clusters.measurement import OccupancySensing -from zigpy.zcl.clusters.security import IasZone - -from .. import LocalDataCluster -from ..const import CLUSTER_COMMAND, OFF, ON, ZONE_STATE +from .. import MotionWithReset, OccupancyOnEvent, LocalDataCluster KONKE = "Konke" -OCCUPANCY_STATE = 0 -OCCUPANCY_EVENT = "occupancy_event" -MOTION_TYPE = 0x000D -ZONE_TYPE = 0x0001 - -MOTION_TIME = 60 -OCCUPANCY_TIME = 600 -class OccupancyCluster(LocalDataCluster, OccupancySensing): +class OccupancyCluster(LocalDataCluster, OccupancyOnEvent): """Occupancy cluster.""" - cluster_id = OccupancySensing.cluster_id - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self._timer_handle = None - self.endpoint.device.occupancy_bus.add_listener(self) - - def occupancy_event(self): - """Occupancy event.""" - self._update_attribute(OCCUPANCY_STATE, ON) - - if self._timer_handle: - self._timer_handle.cancel() + reset_s: int = 600 - loop = asyncio.get_event_loop() - self._timer_handle = loop.call_later(OCCUPANCY_TIME, self._turn_off) - def _turn_off(self): - self._timer_handle = None - self._update_attribute(OCCUPANCY_STATE, OFF) - - -class MotionCluster(CustomCluster, IasZone): +class MotionCluster(MotionWithReset): """Motion cluster.""" - cluster_id = IasZone.cluster_id - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self._timer_handle = None - - def handle_cluster_request(self, tsn, command_id, args): - """Handle the cluster command.""" - if command_id == 0: - if self._timer_handle: - self._timer_handle.cancel() - loop = asyncio.get_event_loop() - self._timer_handle = loop.call_later(MOTION_TIME, self._turn_off) - self.endpoint.device.occupancy_bus.listener_event(OCCUPANCY_EVENT) - - def _turn_off(self): - self._timer_handle = None - self.listener_event(CLUSTER_COMMAND, 999, 0, [0, 0, 0, 0]) - self._update_attribute(ZONE_STATE, OFF) + reset_s: int = 60 + send_occupancy_event: bool = True diff --git a/zhaquirks/orvibo/__init__.py b/zhaquirks/orvibo/__init__.py index 7e89558f16..fa81a30f07 100644 --- a/zhaquirks/orvibo/__init__.py +++ b/zhaquirks/orvibo/__init__.py @@ -1,72 +1,17 @@ """Module for ORVIBO quirks implementations.""" -import asyncio - -from zigpy.quirks import CustomCluster -from zigpy.zcl.clusters.measurement import OccupancySensing -from zigpy.zcl.clusters.security import IasZone - -from .. import LocalDataCluster -from ..const import CLUSTER_COMMAND, OFF, ON, ZONE_STATE +from .. import MotionWithReset, OccupancyOnEvent ORVIBO = "欧瑞博" ORVIBO_LATIN = "ORVIBO" -OCCUPANCY_STATE = 0 -OCCUPANCY_EVENT = "occupancy_event" -MOTION_TYPE = 0x000D -ZONE_TYPE = 0x0001 - -MOTION_TIME = 30 -OCCUPANCY_TIME = 600 - -class OccupancyCluster(LocalDataCluster, OccupancySensing): +class OccupancyCluster(OccupancyOnEvent): """Occupancy cluster.""" - cluster_id = OccupancySensing.cluster_id - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self._timer_handle = None - self.endpoint.device.occupancy_bus.add_listener(self) - - def occupancy_event(self): - """Occupancy event.""" - self._update_attribute(OCCUPANCY_STATE, ON) - - if self._timer_handle: - self._timer_handle.cancel() - loop = asyncio.get_event_loop() - self._timer_handle = loop.call_later(OCCUPANCY_TIME, self._turn_off) - - def _turn_off(self): - self._timer_handle = None - self._update_attribute(OCCUPANCY_STATE, OFF) - - -class MotionCluster(CustomCluster, IasZone): +class MotionCluster(MotionWithReset): """Motion cluster.""" - cluster_id = IasZone.cluster_id - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self._timer_handle = None - - def handle_cluster_request(self, tsn, command_id, args): - """Handle the cluster command.""" - if command_id == 0: - if self._timer_handle: - self._timer_handle.cancel() - loop = asyncio.get_event_loop() - self._timer_handle = loop.call_later(MOTION_TIME, self._turn_off) - self.endpoint.device.occupancy_bus.listener_event(OCCUPANCY_EVENT) - - def _turn_off(self): - self._timer_handle = None - self.listener_event(CLUSTER_COMMAND, 999, 0, [0, 0, 0, 0]) - self._update_attribute(ZONE_STATE, OFF) + reset_s: int = 30 + send_occupancy_event: bool = True diff --git a/zhaquirks/trust/__init__.py b/zhaquirks/trust/__init__.py index 1606d48b00..335325d519 100644 --- a/zhaquirks/trust/__init__.py +++ b/zhaquirks/trust/__init__.py @@ -1,33 +1,10 @@ """Trust.""" -import asyncio - -from zigpy.quirks import CustomCluster -from zigpy.zcl.clusters.security import IasZone - -from ..const import CLUSTER_COMMAND, OFF, ZONE_STATE +from .. import MotionWithReset TRUST = "Trust" -class MotionCluster(CustomCluster, IasZone): +class MotionCluster(MotionWithReset): """Motion cluster.""" - cluster_id = IasZone.cluster_id - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self._timer_handle = None - - def handle_cluster_request(self, tsn, command_id, args): - """Handle the cluster command.""" - if command_id == 0: - if self._timer_handle: - self._timer_handle.cancel() - loop = asyncio.get_event_loop() - self._timer_handle = loop.call_later(30, self._turn_off) - - def _turn_off(self): - self._timer_handle = None - self.listener_event(CLUSTER_COMMAND, 999, 0, [0, 0, 0, 0]) - self._update_attribute(ZONE_STATE, OFF) + reset_s: int = 30 diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index 2e348c1f0e..aa517ca334 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -1,5 +1,4 @@ """Xiaomi common components for custom device handlers.""" -import asyncio import binascii import logging import math @@ -13,27 +12,20 @@ from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.measurement import ( IlluminanceMeasurement, - OccupancySensing, PressureMeasurement, RelativeHumidity, TemperatureMeasurement, ) -from zigpy.zcl.clusters.security import IasZone -from .. import Bus, LocalDataCluster +from .. import Bus, LocalDataCluster, MotionOnEvent, OccupancyWithReset from ..const import ( ATTRIBUTE_ID, ATTRIBUTE_NAME, - CLUSTER_COMMAND, COMMAND_ATTRIBUTE_UPDATED, COMMAND_TRIPLE, - MOTION_EVENT, - OFF, - ON, UNKNOWN, VALUE, ZHA_SEND_EVENT, - ZONE_STATE, ) BATTERY_LEVEL = "battery_level" @@ -309,59 +301,15 @@ def battery_reported(self, voltage, raw_voltage): self._update_attribute(self.BATTERY_VOLTAGE_ATTR, int(raw_voltage / 100)) -class OccupancyCluster(CustomCluster, OccupancySensing): +class OccupancyCluster(OccupancyWithReset): """Occupancy cluster.""" - cluster_id = OccupancySensing.cluster_id - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self._timer_handle = None - - def _update_attribute(self, attrid, value): - super()._update_attribute(attrid, value) - - if attrid == OCCUPANCY_STATE and value == ON: - if self._timer_handle: - self._timer_handle.cancel() - self.endpoint.device.motion_bus.listener_event(MOTION_EVENT) - loop = asyncio.get_event_loop() - self._timer_handle = loop.call_later(600, self._turn_off) - - def _turn_off(self): - self._timer_handle = None - self._update_attribute(OCCUPANCY_STATE, OFF) - - -class MotionCluster(LocalDataCluster, IasZone): +class MotionCluster(LocalDataCluster, MotionOnEvent): """Motion cluster.""" - cluster_id = IasZone.cluster_id - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self._timer_handle = None - self.endpoint.device.motion_bus.add_listener(self) - super()._update_attribute(ZONE_TYPE, MOTION_TYPE) - - def motion_event(self): - """Motion event.""" - super().listener_event(CLUSTER_COMMAND, None, ZONE_STATE, [ON]) - - _LOGGER.debug("%s - Received motion event message", self.endpoint.device.ieee) - - if self._timer_handle: - self._timer_handle.cancel() - - loop = asyncio.get_event_loop() - self._timer_handle = loop.call_later(120, self._turn_off) - - def _turn_off(self): - _LOGGER.debug("%s - Resetting motion sensor", self.endpoint.device.ieee) - self._timer_handle = None - super().listener_event(CLUSTER_COMMAND, None, ZONE_STATE, [OFF]) + _CONSTANT_ATTRIBUTES = {ZONE_TYPE: MOTION_TYPE} + reset_s: int = 120 class TemperatureMeasurementCluster(CustomCluster, TemperatureMeasurement): From 838980faebd86a9d7b5ef2ba45b5eb2f96ba6bb9 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 4 Sep 2020 21:22:06 -0400 Subject: [PATCH 060/113] LocalDataCluster support for _CONSTANT_ATTRIBUTES (#474) * LocalDataCluster support for _CONSTANT_ATTRIBUTES * Make pylint happy. --- zhaquirks/__init__.py | 7 ++++++- zhaquirks/orvibo/__init__.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 679c81f5a7..081f5b853a 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -43,6 +43,8 @@ def __init__(self, *args, **kwargs): class LocalDataCluster(CustomCluster): """Cluster meant to prevent remote calls.""" + _CONSTANT_ATTRIBUTES = {} + async def bind(self): """Prevent bind.""" return (foundation.Status.SUCCESS,) @@ -64,7 +66,10 @@ async def read_attributes_raw(self, attributes, manufacturer=None): for attr in attributes ] for record in records: - record.value.value = self._attr_cache.get(record.attrid) + if record.attrid in self._CONSTANT_ATTRIBUTES: + record.value.value = self._CONSTANT_ATTRIBUTES[record.attrid] + else: + record.value.value = self._attr_cache.get(record.attrid) if record.value.value is not None: record.status = foundation.Status.SUCCESS return (records,) diff --git a/zhaquirks/orvibo/__init__.py b/zhaquirks/orvibo/__init__.py index fa81a30f07..653059474d 100644 --- a/zhaquirks/orvibo/__init__.py +++ b/zhaquirks/orvibo/__init__.py @@ -1,12 +1,12 @@ """Module for ORVIBO quirks implementations.""" -from .. import MotionWithReset, OccupancyOnEvent +from .. import LocalDataCluster, MotionWithReset, OccupancyOnEvent ORVIBO = "欧瑞博" ORVIBO_LATIN = "ORVIBO" -class OccupancyCluster(OccupancyOnEvent): +class OccupancyCluster(LocalDataCluster, OccupancyOnEvent): """Occupancy cluster.""" From 664676d03e75740a227044132974ba33bdfa98d6 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 5 Sep 2020 11:42:35 -0400 Subject: [PATCH 061/113] Quirk for Tuya PIR motion detection (#475) * Blitzwolf occupancy. * Tuya occupancy quirk * Add tests. * Spelling --- tests/test_tuya.py | 41 +++++++++++++++++++ zhaquirks/tuya/__init__.py | 34 ++++++++++++++++ zhaquirks/tuya/motion.py | 81 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 tests/test_tuya.py create mode 100644 zhaquirks/tuya/__init__.py create mode 100755 zhaquirks/tuya/motion.py diff --git a/tests/test_tuya.py b/tests/test_tuya.py new file mode 100644 index 0000000000..c0d8b944b0 --- /dev/null +++ b/tests/test_tuya.py @@ -0,0 +1,41 @@ +"""Tests for Tuya quirks.""" + +import asyncio +from unittest import mock + +import pytest + +from zhaquirks.const import OFF, ON, ZONE_STATE +import zhaquirks.tuya.motion + +from tests.common import ClusterListener + +ZCL_TUYA_MOTION = b"\tL\x01\x00\x05\x03\x04\x00\x01\x02" + + +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.motion.TuyaMotion,)) +async def test_motion(zigpy_device_from_quirk, quirk): + """Test tuya motion sensor.""" + + motion_dev = zigpy_device_from_quirk(quirk) + + motion_cluster = motion_dev.endpoints[1].ias_zone + motion_listener = ClusterListener(motion_cluster) + + tuya_cluster = motion_dev.endpoints[1].tuya_manufacturer + + # send motion on Tuya manufacturer specific cluster + hdr, args = tuya_cluster.deserialize(ZCL_TUYA_MOTION) + with mock.patch.object(motion_cluster, "reset_s", 0): + tuya_cluster.handle_message(hdr, args) + + assert len(motion_listener.cluster_commands) == 1 + assert len(motion_listener.attribute_updates) == 0 + assert motion_listener.cluster_commands[0][1] == ZONE_STATE + assert motion_listener.cluster_commands[0][2][0] == ON + + await asyncio.gather(asyncio.sleep(0), asyncio.sleep(0), asyncio.sleep(0)) + + assert len(motion_listener.cluster_commands) == 2 + assert motion_listener.cluster_commands[1][1] == ZONE_STATE + assert motion_listener.cluster_commands[1][2][0] == OFF diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py new file mode 100644 index 0000000000..e63b77789e --- /dev/null +++ b/zhaquirks/tuya/__init__.py @@ -0,0 +1,34 @@ +"""Tuya devices.""" + +from zigpy.quirks import CustomCluster +import zigpy.types as t + +TUYA_CLUSTER_ID = 0xEF00 + + +class Data(t.List, item_type=t.uint8_t): + """list of uint8_t.""" + + +class TuyaManufCluster(CustomCluster): + """Tuya manufacturer specific cluster.""" + + name = "Tuya Manufacturer Specicific" + cluster_id = TUYA_CLUSTER_ID + ep_attribute = "tuya_manufacturer" + + class Command(t.Struct): + """Tuya manufacturer cluster command.""" + + status: t.uint8_t + tsn: t.uint8_t + command_id: t.uint16_t + function: t.uint8_t + data: Data + + manufacturer_server_commands = {0x0000: ("set_data", (Command,), False)} + + manufacturer_client_commands = { + 0x0001: ("get_data", (Command,), True), + 0x0002: ("set_data_response", (Command,), True), + } diff --git a/zhaquirks/tuya/motion.py b/zhaquirks/tuya/motion.py new file mode 100755 index 0000000000..aea7c4b6e6 --- /dev/null +++ b/zhaquirks/tuya/motion.py @@ -0,0 +1,81 @@ +"""BlitzWolf IS-3/Tuya motion rechargeable occupancy sensor.""" + +from typing import Tuple + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Identify, Ota +from zigpy.zcl.clusters.security import IasZone + +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, + MOTION_EVENT, +) +from .. import MotionOnEvent, LocalDataCluster, Bus +from . import TuyaManufCluster + + +ZONE_TYPE = 0x0001 + + +class MotionCluster(LocalDataCluster, MotionOnEvent): + """Tuya Motion Sensor.""" + + _CONSTANT_ATTRIBUTES = {ZONE_TYPE: IasZone.ZoneType.Motion_Sensor} + reset_s = 15 + + +class TuyaManufacturerClusterMotion(TuyaManufCluster): + """Manufacturer Specific Cluster of the Motion device.""" + + def handle_cluster_request( + self, tsn: int, command_id: int, args: Tuple[TuyaManufCluster.Command] + ) -> None: + """Handle cluster request.""" + tuya_cmd = args[0] + if command_id == 0x0001 and tuya_cmd.command_id == 1027: + self.endpoint.device.motion_bus.listener_event(MOTION_EVENT) + + +class TuyaMotion(CustomDevice): + """BW-IS3 occupancy sensor.""" + + def __init__(self, *args, **kwargs): + """Init device.""" + self.motion_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + # endpoint=1 profile=260 device_type=0 device_version=0 input_clusters=[0, 3] + # output_clusters=[3, 25]> + MODELS_INFO: [("_TYST11_i5j6ifxj", "5j6ifxj")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [Basic.cluster_id, Identify.cluster_id], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + MotionCluster, + TuyaManufacturerClusterMotion, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + } + } From c6b38c8517e988eb6231952e4527112d42a5d737 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Sun, 6 Sep 2020 07:44:11 -0400 Subject: [PATCH 062/113] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3686077d41..f9604fe9a8 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.43" +VERSION = "0.0.44" def readme(): From e760d7261f4eff507109ad66b1185f365679a29a Mon Sep 17 00:00:00 2001 From: piomis Date: Mon, 14 Sep 2020 16:36:10 +0200 Subject: [PATCH 063/113] feat(tuya): Quirk for tuya-based 1-channel switch (#484) * feat(tuya): First sketch of quirk for single channel Tuya-based switch * feat(tuya): process notification with switch state and update attribute * feat(tuya): First sketch of quirk for single channel Tuya-based switch * feat(tuya): process notification with switch state and update attribute * fix(tuya): Replace __init__.py with current dev branch * feat: TuyaManufacturerOnOff an TuyaOnOffCluster now should support EndpointId is used to calculate TuyaCmd field. Tuya specific clusters definitions were moved to __init__.py Added tests and run black formatter * chore: Updated Supported Device list and Contributors list * fix(tuya): Linter and pytest fixes * fix(tuya): Linter warning removal (Coroutine import not needed) * style(tuya): Few formatting corrections --- Contributors.md | 1 + README.md | 3 ++ tests/test_tuya.py | 65 +++++++++++++++++++++++++ zhaquirks/tuya/__init__.py | 88 +++++++++++++++++++++++++++++++++- zhaquirks/tuya/singleswitch.py | 58 ++++++++++++++++++++++ 5 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 zhaquirks/tuya/singleswitch.py diff --git a/Contributors.md b/Contributors.md index f89727b103..94dcfc68f5 100644 --- a/Contributors.md +++ b/Contributors.md @@ -20,3 +20,4 @@ - [Piotr Majkrzak](https://github.com/majkrzak) - [Gleb Sinyavskiy](https://github.com/zhulik) - [Michael Thingnes](https://github.com/thimic) +- [Piotr Mis](https://github.com/piomis) diff --git a/README.md b/README.md index 76af4991df..1f5ff7c9f0 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,9 @@ If you are looking to make your first code contribution to this project then we - [YRD210](https://www.yalehome.com/Yale/Yale%20US/Real%20Living/installation%20instructions/Yale%20DB%20PUSH%20Quickstart%2018JUL11_Rev%20B.pdf): Yale YRD210 Deadbolt - [YRL220](https://www.yalehome.com/Yale/Yale%20US/Real%20Living/installation%20instructions/Yale%20%20DB%20Touch%20Instructions%2023AUG11_Rev%20B.pdf): Yale YRL220 Lock +### Tuya-based +- [TS0601 switch](https://zigbee.blakadder.com/Lerlink_X701A.html): Tuya-based 1-gang switches with neutral (e.g. Lerlink, Lonsonho) + # Configuration: 1. Update Home Assistant to 0.85.1 or a later version. diff --git a/tests/test_tuya.py b/tests/test_tuya.py index c0d8b944b0..fb19d9f381 100644 --- a/tests/test_tuya.py +++ b/tests/test_tuya.py @@ -6,11 +6,15 @@ import pytest from zhaquirks.const import OFF, ON, ZONE_STATE + import zhaquirks.tuya.motion +from zigpy.zcl import foundation from tests.common import ClusterListener ZCL_TUYA_MOTION = b"\tL\x01\x00\x05\x03\x04\x00\x01\x02" +ZCL_TUYA_SWITCH_ON = b"\tQ\x02\x006\x01\x01\x00\x01\x01" +ZCL_TUYA_SWITCH_OFF = b"\tQ\x02\x006\x01\x01\x00\x01\x00" @pytest.mark.parametrize("quirk", (zhaquirks.tuya.motion.TuyaMotion,)) @@ -39,3 +43,64 @@ async def test_motion(zigpy_device_from_quirk, quirk): assert len(motion_listener.cluster_commands) == 2 assert motion_listener.cluster_commands[1][1] == ZONE_STATE assert motion_listener.cluster_commands[1][2][0] == OFF + + +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.singleswitch.TuyaSingleSwitch,)) +async def test_singleswitch_state_report(zigpy_device_from_quirk, quirk): + """Test tuya single switch.""" + + switch_dev = zigpy_device_from_quirk(quirk) + + switch_cluster = switch_dev.endpoints[1].on_off + switch_listener = ClusterListener(switch_cluster) + + tuya_cluster = switch_dev.endpoints[1].tuya_manufacturer + + hdr, args = tuya_cluster.deserialize(ZCL_TUYA_SWITCH_ON) + tuya_cluster.handle_message(hdr, args) + hdr, args = tuya_cluster.deserialize(ZCL_TUYA_SWITCH_OFF) + tuya_cluster.handle_message(hdr, args) + + assert len(switch_listener.cluster_commands) == 0 + assert len(switch_listener.attribute_updates) == 2 + assert switch_listener.attribute_updates[0][0] == 0x0000 + assert switch_listener.attribute_updates[0][1] == ON + assert switch_listener.attribute_updates[1][0] == 0x0000 + assert switch_listener.attribute_updates[1][1] == OFF + + +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.singleswitch.TuyaSingleSwitch,)) +async def test_singleswitch_requests(zigpy_device_from_quirk, quirk): + """Test tuya single switch.""" + + switch_dev = zigpy_device_from_quirk(quirk) + + switch_cluster = switch_dev.endpoints[1].on_off + tuya_cluster = switch_dev.endpoints[1].tuya_manufacturer + + with mock.patch.object( + tuya_cluster.endpoint, "request", return_value=foundation.Status.SUCCESS + ) as m1: + + status = switch_cluster.command(0x0000) + m1.assert_called_with( + 61184, + 1, + b"\x01\x01\x00\x00\x00\x01\x01\x00\x01\x00", + expect_reply=True, + command_id=0, + ) + assert status == 0 + + status = switch_cluster.command(0x0001) + m1.assert_called_with( + 61184, + 2, + b"\x01\x02\x00\x00\x00\x01\x01\x00\x01\x01", + expect_reply=True, + command_id=0, + ) + assert status == 0 + + status = switch_cluster.command(0x0002) + assert status == foundation.Status.UNSUP_CLUSTER_COMMAND diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py index e63b77789e..ee7ffa03b0 100644 --- a/zhaquirks/tuya/__init__.py +++ b/zhaquirks/tuya/__init__.py @@ -1,9 +1,26 @@ """Tuya devices.""" +import logging -from zigpy.quirks import CustomCluster +from typing import Optional, Tuple, Union + +from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.zcl.clusters.general import OnOff +from zigpy.zcl import foundation import zigpy.types as t +from .. import Bus + + TUYA_CLUSTER_ID = 0xEF00 +TUYA_SET_DATA = 0x0000 +TUYA_GET_DATA = 0x0001 +TUYA_SET_DATA_RESPONSE = 0x0002 + +SWITCH_EVENT = "switch_event" +ATTR_ON_OFF = 0x0000 +TUYA_CMD_BASE = 0x0100 + +_LOGGER = logging.getLogger(__name__) class Data(t.List, item_type=t.uint8_t): @@ -32,3 +49,72 @@ class Command(t.Struct): 0x0001: ("get_data", (Command,), True), 0x0002: ("set_data_response", (Command,), True), } + + +class TuyaOnOff(CustomCluster, OnOff): + """Tuya On/Off cluster for On/Off device.""" + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.switch_bus.add_listener(self) + + def switch_event(self, channel, state): + """Switch event.""" + _LOGGER.debug( + "%s - Received switch event message, channel: %d, state: %d", + self.endpoint.device.ieee, + channel, + state, + ) + self._update_attribute(ATTR_ON_OFF, state) + + def command( + self, + command_id: Union[foundation.Command, int, t.uint8_t], + *args, + manufacturer: Optional[Union[int, t.uint16_t]] = None, + expect_reply: bool = True, + tsn: Optional[Union[int, t.uint8_t]] = None, + ): + """Override the default Cluster command.""" + + if command_id in (0x0000, 0x0001): + cmd_payload = TuyaManufCluster.Command() + cmd_payload.status = 0 + cmd_payload.tsn = 0 + cmd_payload.command_id = TUYA_CMD_BASE + self.endpoint.endpoint_id + cmd_payload.function = 0 + cmd_payload.data = [1, command_id] + + return self.endpoint.tuya_manufacturer.command( + TUYA_SET_DATA, cmd_payload, expect_reply=True + ) + + return foundation.Status.UNSUP_CLUSTER_COMMAND + + +class TuyaManufacturerClusterOnOff(TuyaManufCluster): + """Manufacturer Specific Cluster of On/Off device.""" + + def handle_cluster_request( + self, tsn: int, command_id: int, args: Tuple[TuyaManufCluster.Command] + ) -> None: + """Handle cluster request.""" + + tuya_payload = args[0] + if command_id in (0x0002, 0x0001): + self.endpoint.device.switch_bus.listener_event( + SWITCH_EVENT, + tuya_payload.command_id - TUYA_CMD_BASE, + tuya_payload.data[1], + ) + + +class TuyaSwitch(CustomDevice): + """Tuya switch device.""" + + def __init__(self, *args, **kwargs): + """Init device.""" + self.switch_bus = Bus() + super().__init__(*args, **kwargs) diff --git a/zhaquirks/tuya/singleswitch.py b/zhaquirks/tuya/singleswitch.py new file mode 100644 index 0000000000..0c08d491d6 --- /dev/null +++ b/zhaquirks/tuya/singleswitch.py @@ -0,0 +1,58 @@ +"""Tuya based button sensor.""" +from zigpy.profiles import zha +from zigpy.zcl.clusters.general import Basic, Groups, Scenes, Time, Ota +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from ..tuya import TuyaManufCluster, TuyaManufacturerClusterOnOff, TuyaOnOff, TuyaSwitch + + +class TuyaSingleSwitch(TuyaSwitch): + """Tuya single channel switch device.""" + + signature = { + # "node_descriptor": "", + # device_version=1 + # input_clusters=[0x0000,0x0004, 0x0005,0x000a, 0xef00] + # output_clusters=[0x0019] + # + MODELS_INFO: [("_TZE200_7tdtqgwv", "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, + Time.cluster_id, + TuyaManufCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Time.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + TuyaManufacturerClusterOnOff, + TuyaOnOff, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + } + } From 316b2f6f05aa9d427eae1c0a9cdbddee1e6ae299 Mon Sep 17 00:00:00 2001 From: Adrien Chevrier Date: Thu, 17 Sep 2020 16:56:53 +0200 Subject: [PATCH 064/113] Add support for lumi.switch.b1lacn02 and lumi.switch.b2lacn02 switches (#492) --- zhaquirks/xiaomi/aqara/ctrl_neutral.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/zhaquirks/xiaomi/aqara/ctrl_neutral.py b/zhaquirks/xiaomi/aqara/ctrl_neutral.py index f53ff409e7..c0b7cca342 100644 --- a/zhaquirks/xiaomi/aqara/ctrl_neutral.py +++ b/zhaquirks/xiaomi/aqara/ctrl_neutral.py @@ -54,7 +54,12 @@ class CtrlNeutral(XiaomiCustomDevice): """Aqara single and double key switch device.""" signature = { - MODELS_INFO: [(LUMI, "lumi.ctrl_neutral1"), (LUMI, "lumi.ctrl_neutral2")], + MODELS_INFO: [ + (LUMI, "lumi.ctrl_neutral1"), + (LUMI, "lumi.ctrl_neutral2"), + (LUMI, "lumi.switch.b1lacn02"), + (LUMI, "lumi.switch.b2lacn02"), + ], ENDPOINTS: { # Date: Thu, 17 Sep 2020 17:00:21 +0200 Subject: [PATCH 065/113] Fix link to Aqara Vibration Sensor (#487) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f5ff7c9f0..d9ae2273a9 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ If you are looking to make your first code contribution to this project then we ### Xiaomi Aqara - [Cube](https://www.aqara.com/en/cube_controller-product.html): lumi.sensor_cube.aqgl01 - [Button](https://www.aqara.com/en/wireless_mini_switch.html): lumi.sensor_switch.aq2 -- [Vibration Sensor](http://www.xiaomimagazine.com/new-sensor-for-the-smart-home-xiaomi-check-aqara-smart-motion-sensor/): lumi.vibration.aq1 +- [Vibration Sensor](https://www.aqara.com/en/vibration_sensor.html): lumi.vibration.aq1 - [Contact Sensor](https://www.aqara.com/en/door_and_window_sensor-product.html): lumi.sensor_magnet.aq2 - [Motion Sensor](https://www.aqara.com/en/motion_sensor.html): lumi.sensor_motion.aq2 - [Temperature / Humidity Sensor](https://www.aqara.com/en/temperature_and_humidity_sensor-product.html): lumi.weather From d6b1822cbd7d03a3a6a0f292a4dfb7e6041445f1 Mon Sep 17 00:00:00 2001 From: Goldwing1973 <67235461+Goldwing1973@users.noreply.github.com> Date: Thu, 17 Sep 2020 17:00:56 +0200 Subject: [PATCH 066/113] Add support for the Osram Smart+ GU10 AC05347 (#485) * Create SmartPlusAC05347 * Rename SmartPlusAC05347 to smartplusac05347 * Update smartplusac05347 --- zhaquirks/osram/smartplusac05347 | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 zhaquirks/osram/smartplusac05347 diff --git a/zhaquirks/osram/smartplusac05347 b/zhaquirks/osram/smartplusac05347 new file mode 100644 index 0000000000..6b94727b5c --- /dev/null +++ b/zhaquirks/osram/smartplusac05347 @@ -0,0 +1,74 @@ +"""Osram Smart+ AC05347 GU10 White.""" +from zigpy.profiles import zll +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + Scenes, +) +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from . import OSRAM, OsramLightCluster +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class SmartplusAC05347(CustomDevice): + """Osram Smart+ AC05347 GU10 White.""" + + signature = { + # + MODELS_INFO: [(OSRAM, "Smart+ AC05347")], + ENDPOINTS: { + 3: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.COLOR_DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + LightLink.cluster_id, + OsramLightCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 3: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.COLOR_DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + LightLink.cluster_id, + OsramLightCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + } + } From 15bdc6d3ea41fb4dc0592ebcb1d2148d025e0229 Mon Sep 17 00:00:00 2001 From: Daniel <49846893+danielbrunt57@users.noreply.github.com> Date: Thu, 17 Sep 2020 10:07:53 -0700 Subject: [PATCH 067/113] Update A19TunableWhite max mireds and color capabilities (#488) * Update a19twhite.py * Update a19twhite.py * Update a19twhite.py * Update zhaquirks/osram/a19twhite.py Co-authored-by: Alexei Chetroi Co-authored-by: Alexei Chetroi --- zhaquirks/osram/a19twhite.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/zhaquirks/osram/a19twhite.py b/zhaquirks/osram/a19twhite.py index 539fb0f99d..ab2f683303 100644 --- a/zhaquirks/osram/a19twhite.py +++ b/zhaquirks/osram/a19twhite.py @@ -1,6 +1,6 @@ """Osram A19 tunable white device.""" from zigpy.profiles import zha -from zigpy.quirks import CustomDevice +from zigpy.quirks import CustomDevice, CustomCluster from zigpy.zcl.clusters.general import ( Basic, Groups, @@ -17,6 +17,12 @@ from ..const import DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, OUTPUT_CLUSTERS, PROFILE_ID +class OsramColorCluster(CustomCluster, Color): + """Osram A19 tunable white device.""" + + _CONSTANT_ATTRIBUTES = {0x400A: 16, 0x400C: 370} + + class A19TunableWhite(CustomDevice): """Osram A19 tunable white device.""" @@ -56,7 +62,7 @@ class A19TunableWhite(CustomDevice): Scenes.cluster_id, OnOff.cluster_id, LevelControl.cluster_id, - Color.cluster_id, + OsramColorCluster, ElectricalMeasurement.cluster_id, OsramLightCluster, ], From 0029ce2514b741d48e36e4bd3b97a1c5144d4390 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 24 Sep 2020 05:36:38 +0200 Subject: [PATCH 068/113] Added py extension + fixed device type for Osram AC05347 (#500) * Added .py extension for Osram Smart+ AC05347 quirk * Fix device type * Changed device_type comment from hex to decimal --- zhaquirks/osram/{smartplusac05347 => smartplusac05347.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename zhaquirks/osram/{smartplusac05347 => smartplusac05347.py} (93%) diff --git a/zhaquirks/osram/smartplusac05347 b/zhaquirks/osram/smartplusac05347.py similarity index 93% rename from zhaquirks/osram/smartplusac05347 rename to zhaquirks/osram/smartplusac05347.py index 6b94727b5c..0cd664b4f2 100644 --- a/zhaquirks/osram/smartplusac05347 +++ b/zhaquirks/osram/smartplusac05347.py @@ -28,14 +28,14 @@ class SmartplusAC05347(CustomDevice): """Osram Smart+ AC05347 GU10 White.""" signature = { - # MODELS_INFO: [(OSRAM, "Smart+ AC05347")], ENDPOINTS: { 3: { PROFILE_ID: zll.PROFILE_ID, - DEVICE_TYPE: zll.DeviceType.COLOR_DIMMABLE_LIGHT, + DEVICE_TYPE: zll.DeviceType.COLOR_TEMPERATURE_LIGHT, INPUT_CLUSTERS: [ Basic.cluster_id, Identify.cluster_id, @@ -56,7 +56,7 @@ class SmartplusAC05347(CustomDevice): ENDPOINTS: { 3: { PROFILE_ID: zll.PROFILE_ID, - DEVICE_TYPE: zll.DeviceType.COLOR_DIMMABLE_LIGHT, + DEVICE_TYPE: zll.DeviceType.COLOR_TEMPERATURE_LIGHT, INPUT_CLUSTERS: [ Basic.cluster_id, Identify.cluster_id, From 1f0bd91c746afe3d1576954ceefd74ee2e37fc63 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 25 Sep 2020 14:09:28 -0400 Subject: [PATCH 069/113] Add another signature for the Tuya PIR sensor (#504) --- zhaquirks/tuya/motion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/tuya/motion.py b/zhaquirks/tuya/motion.py index aea7c4b6e6..7043b42a74 100755 --- a/zhaquirks/tuya/motion.py +++ b/zhaquirks/tuya/motion.py @@ -53,7 +53,7 @@ def __init__(self, *args, **kwargs): signature = { # endpoint=1 profile=260 device_type=0 device_version=0 input_clusters=[0, 3] # output_clusters=[3, 25]> - MODELS_INFO: [("_TYST11_i5j6ifxj", "5j6ifxj")], + MODELS_INFO: [("_TYST11_i5j6ifxj", "5j6ifxj"), ("_TYST11_7hfcudw5", "hfcudw5")], ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, From 33d714057605f8fa67272d14fecbe5ecfa86fa97 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 28 Sep 2020 20:50:21 -0400 Subject: [PATCH 070/113] Add replacement info for Osram switch mini quirk (#506) * Test for empty quirks * Add replacement for Osram switchmini quirk --- tests/test_quirks.py | 23 +++++++++++++++++++++++ zhaquirks/osram/switchmini.py | 3 +++ 2 files changed, 26 insertions(+) create mode 100644 tests/test_quirks.py diff --git a/tests/test_quirks.py b/tests/test_quirks.py new file mode 100644 index 0000000000..dfe0701a99 --- /dev/null +++ b/tests/test_quirks.py @@ -0,0 +1,23 @@ +"""General quirk tests.""" + +import pytest +import zigpy.quirks as zq +import zhaquirks # noqa: F401, E402 +from zhaquirks.const import ENDPOINTS + +ALL_QUIRK_CLASSES = ( + quirk + for manufacturer in zq._DEVICE_REGISTRY._registry.values() + for model in manufacturer.values() + for quirk in model +) + + +@pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES) +def test_quirk_replacements(quirk): + """Test all quirks have a replacement.""" + + assert quirk.signature + assert quirk.replacement + + assert quirk.replacement[ENDPOINTS] diff --git a/zhaquirks/osram/switchmini.py b/zhaquirks/osram/switchmini.py index f1a0131d17..a7f19ba2fe 100644 --- a/zhaquirks/osram/switchmini.py +++ b/zhaquirks/osram/switchmini.py @@ -112,6 +112,9 @@ class OsramSwitchMini(CustomDevice): }, } + replacement = {**signature} + replacement.pop(MODELS_INFO) + device_automation_triggers = { (SHORT_PRESS, BUTTON_1): {COMMAND: COMMAND_ON, ENDPOINT_ID: 1}, (LONG_PRESS, BUTTON_1): {COMMAND: COMMAND_STEP_ON_OFF, ENDPOINT_ID: 1}, From ab15fb3592a201872173e48502bf44331cb5cb34 Mon Sep 17 00:00:00 2001 From: Nemesis24 Date: Wed, 30 Sep 2020 00:42:22 +0200 Subject: [PATCH 071/113] Minor correction for triple press, add quadruple and quintuple press WXKG11LM. (#495) * Minor correction for triple press, add quadruple and quintuple press WXKG11LM. can close https://github.com/zigpy/zha-device-handlers/issues/173 and https://github.com/zigpy/zha-device-handlers/issues/494 * Update switch_aq2.py cluster correction * Delete quintuple press --- zhaquirks/xiaomi/aqara/switch_aq2.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/zhaquirks/xiaomi/aqara/switch_aq2.py b/zhaquirks/xiaomi/aqara/switch_aq2.py index 87dd2c3956..b251fa940c 100644 --- a/zhaquirks/xiaomi/aqara/switch_aq2.py +++ b/zhaquirks/xiaomi/aqara/switch_aq2.py @@ -21,6 +21,7 @@ MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, + QUADRUPLE_PRESS, SHORT_PRESS, SKIP_CONFIGURATION, TRIPLE_PRESS, @@ -93,8 +94,14 @@ class SwitchAQ2(XiaomiCustomDevice): }, (TRIPLE_PRESS, TRIPLE_PRESS): { COMMAND: COMMAND_TRIPLE, - CLUSTER_ID: 0, + CLUSTER_ID: 6, ENDPOINT_ID: 1, ARGS: {ATTRIBUTE_ID: 32768, ATTRIBUTE_NAME: UNKNOWN, VALUE: 3}, }, + (QUADRUPLE_PRESS, QUADRUPLE_PRESS): { + COMMAND: COMMAND_ATTRIBUTE_UPDATED, + CLUSTER_ID: 6, + ENDPOINT_ID: 1, + ARGS: {ATTRIBUTE_ID: 32768, ATTRIBUTE_NAME: UNKNOWN, VALUE: 4}, + }, } From 0b8a3c2e6fbd928e1335734ffc14803e11e22952 Mon Sep 17 00:00:00 2001 From: evanreichard <30810613+evanreichard@users.noreply.github.com> Date: Tue, 29 Sep 2020 18:48:09 -0400 Subject: [PATCH 072/113] [RWL021 & RWL020] Add Multiple Click Support (#501) * [RWL021 & RWL020] Add Multiple Click Support * Convert to asyncio * Add required docstrings * Docstrings update * Reformat * Update zhaquirks/philips/__init__.py Co-authored-by: Alexei Chetroi * Update zhaquirks/philips/__init__.py Co-authored-by: Alexei Chetroi Co-authored-by: Alexei Chetroi --- zhaquirks/philips/__init__.py | 84 ++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index dbe714e50f..11c70c911c 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -1,5 +1,8 @@ """Module for Philips quirks implementations.""" import logging +import time + +import asyncio from zigpy.quirks import CustomCluster import zigpy.types as t @@ -12,6 +15,10 @@ COMMAND_ID, DIM_DOWN, DIM_UP, + DOUBLE_PRESS, + TRIPLE_PRESS, + QUADRUPLE_PRESS, + QUINTUPLE_PRESS, LONG_PRESS, LONG_RELEASE, PRESS_TYPE, @@ -35,6 +42,22 @@ (LONG_PRESS, TURN_OFF): {COMMAND: "off_hold"}, (LONG_PRESS, DIM_UP): {COMMAND: "up_hold"}, (LONG_PRESS, DIM_DOWN): {COMMAND: "down_hold"}, + (DOUBLE_PRESS, TURN_ON): {COMMAND: "on_double_press"}, + (DOUBLE_PRESS, TURN_OFF): {COMMAND: "off_double_press"}, + (DOUBLE_PRESS, DIM_UP): {COMMAND: "up_double_press"}, + (DOUBLE_PRESS, DIM_DOWN): {COMMAND: "down_double_press"}, + (TRIPLE_PRESS, TURN_ON): {COMMAND: "on_triple_press"}, + (TRIPLE_PRESS, TURN_OFF): {COMMAND: "off_triple_press"}, + (TRIPLE_PRESS, DIM_UP): {COMMAND: "up_triple_press"}, + (TRIPLE_PRESS, DIM_DOWN): {COMMAND: "down_triple_press"}, + (QUADRUPLE_PRESS, TURN_ON): {COMMAND: "on_quadruple_press"}, + (QUADRUPLE_PRESS, TURN_OFF): {COMMAND: "off_quadruple_press"}, + (QUADRUPLE_PRESS, DIM_UP): {COMMAND: "up_quadruple_press"}, + (QUADRUPLE_PRESS, DIM_DOWN): {COMMAND: "down_quadruple_press"}, + (QUINTUPLE_PRESS, TURN_ON): {COMMAND: "on_quintuple_press"}, + (QUINTUPLE_PRESS, TURN_OFF): {COMMAND: "off_quintuple_press"}, + (QUINTUPLE_PRESS, DIM_UP): {COMMAND: "up_quintuple_press"}, + (QUINTUPLE_PRESS, DIM_DOWN): {COMMAND: "down_quintuple_press"}, (SHORT_RELEASE, TURN_ON): {COMMAND: "on_short_release"}, (SHORT_RELEASE, TURN_OFF): {COMMAND: "off_short_release"}, (SHORT_RELEASE, DIM_UP): {COMMAND: "up_short_release"}, @@ -75,6 +98,34 @@ async def bind(self): return result +class ButtonPressQueue: + """Philips button queue to derive multiple press events.""" + + def __init__(self): + """Init.""" + self._ms_threshold = 500 + self._ms_last_click = 0 + self._click_counter = 1 + self._callback = lambda x: None + self._task = asyncio.ensure_future(self._job()) + + async def _job(self): + await asyncio.sleep(self._ms_threshold / 1000) + self._callback(self._click_counter) + + def press(self, callback): + """Process a button press.""" + self._callback = callback + now_ms = time.time() * 1000 + if now_ms - self._ms_last_click > self._ms_threshold: + self._click_counter = 1 + else: + self._task.cancel() + self._click_counter += 1 + self._ms_last_click = now_ms + self._task = asyncio.ensure_future(self._job()) + + class PhilipsRemoteCluster(CustomCluster): """Philips remote cluster.""" @@ -91,6 +142,8 @@ class PhilipsRemoteCluster(CustomCluster): BUTTONS = {1: "on", 2: "up", 3: "down", 4: "off"} PRESS_TYPES = {0: "press", 1: "hold", 2: "short_release", 3: "long_release"} + button_press_queue = ButtonPressQueue() + def handle_cluster_request(self, tsn, command_id, args): """Handle the cluster command.""" _LOGGER.debug( @@ -99,6 +152,7 @@ def handle_cluster_request(self, tsn, command_id, args): command_id, args, ) + button = self.BUTTONS.get(args[0], args[0]) press_type = self.PRESS_TYPES.get(args[2], args[2]) @@ -108,5 +162,31 @@ def handle_cluster_request(self, tsn, command_id, args): COMMAND_ID: command_id, ARGS: args, } - action = "{}_{}".format(button, press_type) - self.listener_event(ZHA_SEND_EVENT, action, event_args) + + def send_press_event(click_count): + _LOGGER.debug( + "PhilipsRemoteCluster - send_press_event click_count: [%s]", click_count + ) + if click_count == 1: + press_type = "press" + elif click_count == 2: + press_type = "double_press" + elif click_count == 3: + press_type = "triple_press" + elif click_count == 4: + press_type = "quadruple_press" + elif click_count == 5: + press_type = "quintuple_press" + + # Override PRESS_TYPE + event_args[PRESS_TYPE] = press_type + + action = f"{button}_{press_type}" + self.listener_event(ZHA_SEND_EVENT, action, event_args) + + # Derive Multiple Presses + if press_type == "press": + self.button_press_queue.press(send_press_event) + else: + action = f"{button}_{press_type}" + self.listener_event(ZHA_SEND_EVENT, action, event_args) From 28a47a35071495974bc50dcd889f7b0850ac3483 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Tue, 29 Sep 2020 18:50:30 -0400 Subject: [PATCH 073/113] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f9604fe9a8..71c9bbcc76 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.44" +VERSION = "0.0.45" def readme(): From 427761d2b6e7577fe944986908b946e768195a34 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Wed, 7 Oct 2020 00:30:41 +0300 Subject: [PATCH 074/113] Fix XBee manufacturer and model (#513) * Fix XBee manufacturer and model * Fix manufacturer to Digi --- zhaquirks/xbee/__init__.py | 5 ++--- zhaquirks/xbee/xbee3_io.py | 46 +++++++++++++------------------------- zhaquirks/xbee/xbee_io.py | 46 +++++++++++++------------------------- 3 files changed, 34 insertions(+), 63 deletions(-) diff --git a/zhaquirks/xbee/__init__.py b/zhaquirks/xbee/__init__.py index af2a5fb816..da3331b1af 100644 --- a/zhaquirks/xbee/__init__.py +++ b/zhaquirks/xbee/__init__.py @@ -332,10 +332,9 @@ def handle_cluster_request(self, tsn, command_id, args): replacement = { ENDPOINTS: { 232: { - "manufacturer": "XBEE", - "model": "xbee.io", INPUT_CLUSTERS: [DigitalIOCluster, SerialDataCluster], OUTPUT_CLUSTERS: [SerialDataCluster, EventRelayCluster], } - } + }, + "manufacturer": "Digi", } diff --git a/zhaquirks/xbee/xbee3_io.py b/zhaquirks/xbee/xbee3_io.py index 5612567580..0b36e004b5 100644 --- a/zhaquirks/xbee/xbee3_io.py +++ b/zhaquirks/xbee/xbee3_io.py @@ -11,123 +11,109 @@ class XBee3Sensor(XBeeCommon): def __init__(self, application, ieee, nwk, replaces): """Initialize device-specific properties.""" + self.replacement["model"] = "XBee3" self.replacement[ENDPOINTS].update( { 0xD0: { - "manufacturer": "XBEE", - "model": "AD0/DIO0/Commissioning", + # AD0/DIO0/Commissioning DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD1: { - "manufacturer": "XBEE", - "model": "AD1/DIO1/SPI_nATTN", + # AD1/DIO1/SPI_nATTN DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD2: { - "manufacturer": "XBEE", - "model": "AD2/DIO2/SPI_CLK", + # AD2/DIO2/SPI_CLK DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD3: { - "manufacturer": "XBEE", - "model": "AD3/DIO3", + # AD3/DIO3 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD4: { - "manufacturer": "XBEE", - "model": "DIO4/SPI_MOSI", + # DIO4/SPI_MOSI DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xD5: { - "manufacturer": "XBEE", - "model": "DIO5/Assoc", + # DIO5/Assoc DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xD6: { - "manufacturer": "XBEE", - "model": "DIO6/RTS", + # DIO6/RTS DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xD7: { - "manufacturer": "XBEE", - "model": "DIO7/CTS", + # DIO7/CTS DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD8: { - "manufacturer": "XBEE", - "model": "DIO8", + # DIO8 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xD9: { - "manufacturer": "XBEE", - "model": "DIO9", + # DIO9 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xDA: { - "manufacturer": "XBEE", - "model": "DIO10/PWM0", + # DIO10/PWM0 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeePWM], OUTPUT_CLUSTERS: [], }, 0xDB: { - "manufacturer": "XBEE", - "model": "DIO11/PWM1", + # DIO11/PWM1 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeePWM], OUTPUT_CLUSTERS: [], }, 0xDC: { - "manufacturer": "XBEE", - "model": "DIO12/SPI_MISO", + # DIO12/SPI_MISO DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xDD: { - "manufacturer": "XBEE", - "model": "DIO13/DOUT", + # DIO13/DOUT DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xDE: { - "manufacturer": "XBEE", - "model": "DIO14/DIN", + # DIO14/DIN DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], diff --git a/zhaquirks/xbee/xbee_io.py b/zhaquirks/xbee/xbee_io.py index 8769a9bbd3..e1733b27b2 100644 --- a/zhaquirks/xbee/xbee_io.py +++ b/zhaquirks/xbee/xbee_io.py @@ -11,123 +11,109 @@ class XBeeSensor(XBeeCommon): def __init__(self, application, ieee, nwk, replaces): """Initialize device-specific properties.""" + self.replacement["model"] = "XBee2" self.replacement[ENDPOINTS].update( { 0xD0: { - "manufacturer": "XBEE", - "model": "AD0/DIO0/Commissioning", + # AD0/DIO0/Commissioning DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD1: { - "manufacturer": "XBEE", - "model": "AD1/DIO1/SPI_nATTN", + # AD1/DIO1/SPI_nATTN DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD2: { - "manufacturer": "XBEE", - "model": "AD2/DIO2/SPI_CLK", + # AD2/DIO2/SPI_CLK DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD3: { - "manufacturer": "XBEE", - "model": "AD3/DIO3", + # AD3/DIO3 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD4: { - "manufacturer": "XBEE", - "model": "DIO4/SPI_MOSI", + # DIO4/SPI_MOSI DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xD5: { - "manufacturer": "XBEE", - "model": "DIO5/Assoc", + # DIO5/Assoc DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xD6: { - "manufacturer": "XBEE", - "model": "DIO6/RTS", + # DIO6/RTS DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xD7: { - "manufacturer": "XBEE", - "model": "DIO7/CTS", + # DIO7/CTS DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff, XBeeAnalogInput], OUTPUT_CLUSTERS: [], }, 0xD8: { - "manufacturer": "XBEE", - "model": "DIO8", + # DIO8 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xD9: { - "manufacturer": "XBEE", - "model": "DIO9", + # DIO9 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xDA: { - "manufacturer": "XBEE", - "model": "DIO10/PWM0", + # DIO10/PWM0 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xDB: { - "manufacturer": "XBEE", - "model": "DIO11/PWM1", + # DIO11/PWM1 DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xDC: { - "manufacturer": "XBEE", - "model": "DIO12/SPI_MISO", + # DIO12/SPI_MISO DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xDD: { - "manufacturer": "XBEE", - "model": "DIO13/DOUT", + # DIO13/DOUT DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], OUTPUT_CLUSTERS: [], }, 0xDE: { - "manufacturer": "XBEE", - "model": "DIO14/DIN", + # DIO14/DIN DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, INPUT_CLUSTERS: [XBeeOnOff], From d41d0b53a18e7d674d38e418b6c18fc9a91276bc Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Thu, 8 Oct 2020 19:06:04 +0300 Subject: [PATCH 075/113] Philips power on color (#520) --- zhaquirks/philips/__init__.py | 17 ++++++++++++++++- zhaquirks/philips/zhaextendedcolorlight.py | 15 ++++++++++----- zhaquirks/philips/zllextendedcolorlight.py | 11 ++++++++--- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 11c70c911c..61fdc577f7 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -6,7 +6,8 @@ from zigpy.quirks import CustomCluster import zigpy.types as t -from zigpy.zcl.clusters.general import Basic, OnOff +from zigpy.zcl.clusters.general import Basic, LevelControl, OnOff +from zigpy.zcl.clusters.lighting import Color from ..const import ( ARGS, @@ -84,6 +85,20 @@ class PhilipsOnOffCluster(CustomCluster, OnOff): attributes.update({0x4003: ("power_on_state", PowerOnState)}) +class PhilipsLevelControlCluster(CustomCluster, LevelControl): + """Philips LevelControl cluster.""" + + attributes = LevelControl.attributes.copy() + attributes.update({0x4000: ("power_on_level", t.uint8_t)}) + + +class PhilipsColorCluster(CustomCluster, Color): + """Philips Color cluster.""" + + attributes = Color.attributes.copy() + attributes.update({0x4010: ("power_on_color_temperature", t.uint16_t)}) + + class PhilipsBasicCluster(CustomCluster, Basic): """Philips Basic cluster.""" diff --git a/zhaquirks/philips/zhaextendedcolorlight.py b/zhaquirks/philips/zhaextendedcolorlight.py index 4d6cd5bceb..4a70d30b4c 100644 --- a/zhaquirks/philips/zhaextendedcolorlight.py +++ b/zhaquirks/philips/zhaextendedcolorlight.py @@ -23,7 +23,12 @@ PROFILE_ID, MODELS_INFO, ) -from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster +from zhaquirks.philips import ( + PHILIPS, + PhilipsColorCluster, + PhilipsLevelControlCluster, + PhilipsOnOffCluster, +) class ZHAExtendedColorLight(CustomDevice): @@ -77,10 +82,10 @@ class ZHAExtendedColorLight(CustomDevice): Groups.cluster_id, Scenes.cluster_id, PhilipsOnOffCluster, - LevelControl.cluster_id, + PhilipsLevelControlCluster, LightLink.cluster_id, 64514, - Color.cluster_id, + PhilipsColorCluster, 64513, ], OUTPUT_CLUSTERS: [Ota.cluster_id], @@ -149,9 +154,9 @@ class ZHAExtendedColorLight2(CustomDevice): Groups.cluster_id, Scenes.cluster_id, PhilipsOnOffCluster, - LevelControl.cluster_id, + PhilipsLevelControlCluster, LightLink.cluster_id, - Color.cluster_id, + PhilipsColorCluster, 64513, ], OUTPUT_CLUSTERS: [Ota.cluster_id], diff --git a/zhaquirks/philips/zllextendedcolorlight.py b/zhaquirks/philips/zllextendedcolorlight.py index 0fd8cf8b16..e06cc49e13 100644 --- a/zhaquirks/philips/zllextendedcolorlight.py +++ b/zhaquirks/philips/zllextendedcolorlight.py @@ -23,7 +23,12 @@ PROFILE_ID, MODELS_INFO, ) -from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster +from zhaquirks.philips import ( + PHILIPS, + PhilipsColorCluster, + PhilipsLevelControlCluster, + PhilipsOnOffCluster, +) class ZLLExtendedColorLight(CustomDevice): @@ -90,9 +95,9 @@ class ZLLExtendedColorLight(CustomDevice): Groups.cluster_id, Scenes.cluster_id, PhilipsOnOffCluster, - LevelControl.cluster_id, + PhilipsLevelControlCluster, LightLink.cluster_id, - Color.cluster_id, + PhilipsColorCluster, 64513, ], OUTPUT_CLUSTERS: [Ota.cluster_id], From 4f90a9d5f42429ce81f474e0c077e561c9b56e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc?= Date: Thu, 8 Oct 2020 18:06:54 +0200 Subject: [PATCH 076/113] Add Danfoss specific configuration and attributes according to manufacturer (#517) * Create thermostat.py * Create __init__.py * Update thermostat.py * Update thermostat.py * Update thermostat.py * Update thermostat.py --- zhaquirks/danfoss/__init__.py | 2 + zhaquirks/danfoss/thermostat.py | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 zhaquirks/danfoss/__init__.py create mode 100644 zhaquirks/danfoss/thermostat.py diff --git a/zhaquirks/danfoss/__init__.py b/zhaquirks/danfoss/__init__.py new file mode 100644 index 0000000000..6acba8ed68 --- /dev/null +++ b/zhaquirks/danfoss/__init__.py @@ -0,0 +1,2 @@ +"""Module for Sinope quirks implementations.""" +DANFOSS = "Danfoss" diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py new file mode 100644 index 0000000000..2409f9438e --- /dev/null +++ b/zhaquirks/danfoss/thermostat.py @@ -0,0 +1,90 @@ +"""Module to handle quirks of the Fanfoss thermostat. + +manufacturer specific attributes to control displaying and specific configuration. +""" + +import zigpy.profiles.zha as zha_p +from zigpy.quirks import CustomCluster, CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.general import ( + Basic, + Identify, + Ota, + PowerConfiguration, + Time, + PollControl, +) +from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.zcl.clusters.hvac import Thermostat, UserInterface + +from . import DANFOSS +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class DanfossThermostatCluster(CustomCluster, Thermostat): + """Danfoss custom cluster.""" + + manufacturer_attributes = { + 0x4000: ("etrv_open_windows_detection", t.enum8), + 0x4003: ("external_open_windows_detected", t.Bool), + 0x4014: ("orientation", t.Bool), + } + + +class DanfossUserInterfaceCluster(CustomCluster, UserInterface): + """Danfoss custom cluster.""" + + manufacturer_attributes = {0x4000: ("viewing_direction", t.enum8)} + + +class DanfossThermostat(CustomDevice): + """DanfossThermostat custom device.""" + + signature = { + # + MODELS_INFO: [(DANFOSS, "eTRV0100")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha_p.PROFILE_ID, + DEVICE_TYPE: zha_p.DeviceType.THERMOSTAT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Time.cluster_id, + PollControl.cluster_id, + Thermostat.cluster_id, + UserInterface.cluster_id, + Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [Basic.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic, + PowerConfiguration, + Identify, + Time, + PollControl, + DanfossThermostatCluster, + DanfossUserInterfaceCluster, + Diagnostic, + ], + OUTPUT_CLUSTERS: [Basic, Ota], + } + } + } From ad79a1b16f414a310ade0d6a2af039b5481cfabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Sat, 10 Oct 2020 13:37:13 +0100 Subject: [PATCH 077/113] Add alternative signature for Opple remote in alternate mode (#521) --- zhaquirks/xiaomi/aqara/opple_remote.py | 71 ++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/zhaquirks/xiaomi/aqara/opple_remote.py b/zhaquirks/xiaomi/aqara/opple_remote.py index e68db4895c..9efdc66aa2 100644 --- a/zhaquirks/xiaomi/aqara/opple_remote.py +++ b/zhaquirks/xiaomi/aqara/opple_remote.py @@ -270,6 +270,77 @@ class RemoteB286OPCN01(XiaomiCustomDevice): } +class RemoteB286OPCN01Alt(XiaomiCustomDevice): + """Aqara Opple 2 button remote device (after alternate mode is enabled).""" + + signature = { + # + MODELS_INFO: [(LUMI, "lumi.remote.b286opcn01")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + PowerConfigurationCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + }, + 2: {}, + 3: {}, + 4: {}, + 5: {}, + 6: {}, + }, + } + + replacement = { + NODE_DESCRIPTOR: NodeDescriptor( + 0x02, 0x40, 0x80, 0x115F, 0x7F, 0x0064, 0x2C00, 0x0064, 0x00 + ), + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH, + INPUT_CLUSTERS: [ + BasicCluster, + Identify.cluster_id, + PowerConfigurationCluster, + OppleCluster, + MultistateInputCluster, + ], + 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, MultistateInputCluster], + OUTPUT_CLUSTERS: [Identify.cluster_id, OnOff.cluster_id], + }, + 3: {}, + 4: {}, + 5: {}, + 6: {}, + }, + } + + device_automation_triggers = RemoteB286OPCN01.device_automation_triggers + + class RemoteB486OPCN01(XiaomiCustomDevice): """Aqara Opple 4 button remote device.""" From 085116ca0854ad773ba4137d3c447564df97e653 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 10 Oct 2020 16:06:24 -0400 Subject: [PATCH 078/113] fix traceback in philips remotes (#522) --- zhaquirks/philips/__init__.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 61fdc577f7..36fd30bf93 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -118,21 +118,30 @@ class ButtonPressQueue: def __init__(self): """Init.""" - self._ms_threshold = 500 + self._ms_threshold = 300 self._ms_last_click = 0 self._click_counter = 1 + self._button = None self._callback = lambda x: None - self._task = asyncio.ensure_future(self._job()) + self._task = None async def _job(self): await asyncio.sleep(self._ms_threshold / 1000) self._callback(self._click_counter) - def press(self, callback): + def _reset(self, button): + if self._task: + self._task.cancel() + self._click_counter = 1 + self._button = button + + def press(self, callback, button): """Process a button press.""" self._callback = callback now_ms = time.time() * 1000 - if now_ms - self._ms_last_click > self._ms_threshold: + if self._button != button: + self._reset(button) + elif now_ms - self._ms_last_click > self._ms_threshold: self._click_counter = 1 else: self._task.cancel() @@ -182,6 +191,7 @@ def send_press_event(click_count): _LOGGER.debug( "PhilipsRemoteCluster - send_press_event click_count: [%s]", click_count ) + press_type = None if click_count == 1: press_type = "press" elif click_count == 2: @@ -190,18 +200,18 @@ def send_press_event(click_count): press_type = "triple_press" elif click_count == 4: press_type = "quadruple_press" - elif click_count == 5: + elif click_count > 4: press_type = "quintuple_press" - # Override PRESS_TYPE - event_args[PRESS_TYPE] = press_type - - action = f"{button}_{press_type}" - self.listener_event(ZHA_SEND_EVENT, action, event_args) + if press_type: + # Override PRESS_TYPE + event_args[PRESS_TYPE] = press_type + action = f"{button}_{press_type}" + self.listener_event(ZHA_SEND_EVENT, action, event_args) # Derive Multiple Presses if press_type == "press": - self.button_press_queue.press(send_press_event) + self.button_press_queue.press(send_press_event, button) else: action = f"{button}_{press_type}" self.listener_event(ZHA_SEND_EVENT, action, event_args) From 4df1f07965e64aa774c71bb394fa347f3bcc29cc Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Oct 2020 17:14:48 +0100 Subject: [PATCH 079/113] Add model info to Osram A19TunableWhite quirk (Fixes #536) (#543) * Add model info to Osram A19TunableWhite quirk * Renamed A19TunableWhite quirk to OsramTunableWhite, added 'LIGHTIFY RT Tunable White' model info --- .../osram/{a19twhite.py => tunablewhite.py} | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) rename zhaquirks/osram/{a19twhite.py => tunablewhite.py} (82%) diff --git a/zhaquirks/osram/a19twhite.py b/zhaquirks/osram/tunablewhite.py similarity index 82% rename from zhaquirks/osram/a19twhite.py rename to zhaquirks/osram/tunablewhite.py index ab2f683303..9d7e391c62 100644 --- a/zhaquirks/osram/a19twhite.py +++ b/zhaquirks/osram/tunablewhite.py @@ -1,4 +1,4 @@ -"""Osram A19 tunable white device.""" +"""Osram tunable white device.""" from zigpy.profiles import zha from zigpy.quirks import CustomDevice, CustomCluster from zigpy.zcl.clusters.general import ( @@ -13,8 +13,15 @@ from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.lighting import Color -from . import OsramLightCluster -from ..const import DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, OUTPUT_CLUSTERS, PROFILE_ID +from . import OsramLightCluster, OSRAM +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + OUTPUT_CLUSTERS, + PROFILE_ID, + MODELS_INFO, +) class OsramColorCluster(CustomCluster, Color): @@ -23,13 +30,17 @@ class OsramColorCluster(CustomCluster, Color): _CONSTANT_ATTRIBUTES = {0x400A: 16, 0x400C: 370} -class A19TunableWhite(CustomDevice): - """Osram A19 tunable white device.""" +class OsramTunableWhite(CustomDevice): + """Osram tunable white device.""" signature = { # + MODELS_INFO: [ + (OSRAM, "LIGHTIFY A19 Tunable White"), + (OSRAM, "LIGHTIFY RT Tunable White"), + ], ENDPOINTS: { 3: { PROFILE_ID: zha.PROFILE_ID, @@ -47,7 +58,7 @@ class A19TunableWhite(CustomDevice): ], OUTPUT_CLUSTERS: [Ota.cluster_id], } - } + }, } replacement = { From 8792f183dabb3d211fa8ca99fa9837befc2fc078 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 29 Oct 2020 17:15:19 +0100 Subject: [PATCH 080/113] PhilipsOnOffCluster support: LWB014 and LCT012 without cluster 64513 (#546) * Add ZLLExtendedColorLight2 for LCT012 without 64513 cluster (fixes #479) * Refactored Philips LWB010 quirk into ZLLDimmableLight and added LWB014 support (fixes #544) --- .../{lwb010.py => zlldimmablelight.py} | 8 +-- zhaquirks/philips/zllextendedcolorlight.py | 65 +++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) rename zhaquirks/philips/{lwb010.py => zlldimmablelight.py} (92%) diff --git a/zhaquirks/philips/lwb010.py b/zhaquirks/philips/zlldimmablelight.py similarity index 92% rename from zhaquirks/philips/lwb010.py rename to zhaquirks/philips/zlldimmablelight.py index a7c4a876dc..c64a402979 100644 --- a/zhaquirks/philips/lwb010.py +++ b/zhaquirks/philips/zlldimmablelight.py @@ -1,4 +1,4 @@ -"""Quirk for Phillips LWB010.""" +"""Quirk for Phillips dimmable bulbs.""" from zigpy.profiles import zll from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( @@ -25,11 +25,11 @@ from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster -class PhilipsLBW010(CustomDevice): - """Philips LBW010 device.""" +class ZLLDimmableLight(CustomDevice): + """Philips ZigBee LightLink dimmable bulb device.""" signature = { - MODELS_INFO: [(PHILIPS, "LWB010")], + MODELS_INFO: [(PHILIPS, "LWB010"), (PHILIPS, "LWB014")], ENDPOINTS: { 11: { # Date: Thu, 29 Oct 2020 18:02:37 +0000 Subject: [PATCH 081/113] Add Salus SP600 quirks (#531) --- zhaquirks/salus/__init__.py | 3 ++ zhaquirks/salus/sp600.py | 96 +++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 zhaquirks/salus/__init__.py create mode 100644 zhaquirks/salus/sp600.py diff --git a/zhaquirks/salus/__init__.py b/zhaquirks/salus/__init__.py new file mode 100644 index 0000000000..f9f9c5bea5 --- /dev/null +++ b/zhaquirks/salus/__init__.py @@ -0,0 +1,3 @@ +"""Salus quirks elements.""" + +COMPUTIME = "Computime" diff --git a/zhaquirks/salus/sp600.py b/zhaquirks/salus/sp600.py new file mode 100644 index 0000000000..b31c9884ef --- /dev/null +++ b/zhaquirks/salus/sp600.py @@ -0,0 +1,96 @@ +"""Salus SP600 plug.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + OnOff, + Ota, + PowerConfiguration, + Scenes, +) +from zigpy.zcl.clusters.measurement import TemperatureMeasurement +from zigpy.zcl.clusters.smartenergy import Metering + +from . import COMPUTIME +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +MODEL = "SP600" + + +class MeteringCluster(CustomCluster, Metering): + """Fix multiplier and divisor.""" + + cluster_id = Metering.cluster_id + MULTIPLIER = 0x0301 + DIVISOR = 0x0302 + _CONSTANT_ATTRIBUTES = {MULTIPLIER: 1, DIVISOR: 1000} + + +class TemperatureMeasurementCluster(CustomCluster, TemperatureMeasurement): + """Temperature cluster that divides value by 2.""" + + cluster_id = TemperatureMeasurement.cluster_id + ATTR_ID = 0 + + def _update_attribute(self, attrid, value): + # divide values by 2 + if attrid == self.ATTR_ID: + value = value / 2 + super()._update_attribute(attrid, value) + + +class SP600(CustomDevice): + """Salus SP600 smart plug.""" + + signature = { + ENDPOINTS: { + 9: { + PROFILE_ID: 0x0104, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TemperatureMeasurement.cluster_id, + Metering.cluster_id, + 0xFC01, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + }, + MODELS_INFO: [(COMPUTIME, MODEL)], + } + + replacement = { + ENDPOINTS: { + 9: { + PROFILE_ID: 0x0104, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + TemperatureMeasurementCluster, + MeteringCluster, + 0xFC01, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + }, + MODELS_INFO: [(COMPUTIME, MODEL)], + } From 58a12022a738818c4dd4d48eae51da27b5538840 Mon Sep 17 00:00:00 2001 From: choif Date: Thu, 29 Oct 2020 18:08:55 +0000 Subject: [PATCH 082/113] Ikea blind and remote signature fixes (#528) * Add E1766 manufacturer signature * Ikea blind signature fix (#306) --- zhaquirks/ikea/blinds.py | 16 +++++++++------- zhaquirks/ikea/opencloseremote.py | 9 ++++++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/zhaquirks/ikea/blinds.py b/zhaquirks/ikea/blinds.py index 3ef5786dde..731827cd2a 100644 --- a/zhaquirks/ikea/blinds.py +++ b/zhaquirks/ikea/blinds.py @@ -31,9 +31,9 @@ class IkeaTradfriRollerBlinds(CustomDevice): """Custom device representing IKEA of Sweden TRADFRI Fyrtur blinds.""" signature = { - # MODELS_INFO: [ (IKEA, "FYRTUR block-out roller blind"), @@ -52,6 +52,7 @@ class IkeaTradfriRollerBlinds(CustomDevice): PollControl.cluster_id, WindowCovering.cluster_id, LightLink.cluster_id, + IKEA_CLUSTER_ID, ], OUTPUT_CLUSTERS: [Ota.cluster_id, LightLink.cluster_id], } @@ -59,11 +60,11 @@ class IkeaTradfriRollerBlinds(CustomDevice): } replacement = { - "endpoints": { + ENDPOINTS: { 1: { - "profile_id": zha.PROFILE_ID, - "device_type": zha.DeviceType.WINDOW_COVERING_DEVICE, - "input_clusters": [ + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ Basic.cluster_id, DoublingPowerConfigurationCluster, Identify.cluster_id, @@ -72,8 +73,9 @@ class IkeaTradfriRollerBlinds(CustomDevice): PollControl.cluster_id, WindowCovering.cluster_id, LightLink.cluster_id, + IKEA_CLUSTER_ID, ], - "output_clusters": [Ota.cluster_id, LightLink.cluster_id], + OUTPUT_CLUSTERS: [Ota.cluster_id, LightLink.cluster_id], } } } diff --git a/zhaquirks/ikea/opencloseremote.py b/zhaquirks/ikea/opencloseremote.py index 9a6495c5ea..06f12d1293 100644 --- a/zhaquirks/ikea/opencloseremote.py +++ b/zhaquirks/ikea/opencloseremote.py @@ -33,7 +33,14 @@ class IkeaTradfriOpenCloseRemote(CustomDevice): """Custom device representing IKEA of Sweden TRADFRI remote control.""" signature = { - MODELS_INFO: [("\x02KE", "TRADFRI open/close remote")], + # + MODELS_INFO: [ + ("\x02KE", "TRADFRI open/close remote"), + (IKEA, "TRADFRI open/close remote"), + ], ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, From 8951bd8b73640a61464cb430ff37133f3521a80e Mon Sep 17 00:00:00 2001 From: Hedda Date: Thu, 29 Oct 2020 19:09:57 +0100 Subject: [PATCH 083/113] Update README.md to change phrase the supported hardware phrase (#470) Small change in update README.md to change phrase the supported hardware phrase. Read many comments in Home Assistant community where users ask which devices are supported by ZHA and this list of "supported hardware" in the ZHA Device Handlers README.md seems to confuse people more than the fact there is no list of devices that ZHA supports. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9ae2273a9..478b663557 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are looking to make your first code contribution to this project then we - https://github.com/firstcontributions/first-contributions/blob/master/README.md - https://github.com/firstcontributions/first-contributions/blob/master/github-desktop-tutorial.md -# Currently Supported Devices: +# Zigbee devices not following specifications which there already are quirks for: ### CentraLite - [Contact Sensor](http://a.co/g9eWPAQ): CentraLite 3300-S From 8fde1386fb3f784ae342eb9e88ff9929d0a447f4 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 1 Nov 2020 23:43:16 +0100 Subject: [PATCH 084/113] Added quirk for iluminize universal controller: remove color wheel (#534) * Added quirk for iluminize universal actor: CCT lighting mode * Added quirk for iluminize universal actor: DIM lighting mode * Hopefully fix lint --- zhaquirks/iluminize/__init__.py | 3 + zhaquirks/iluminize/cct.py | 101 ++++++++++++++++++++++++++++++++ zhaquirks/iluminize/dim.py | 101 ++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 zhaquirks/iluminize/__init__.py create mode 100644 zhaquirks/iluminize/cct.py create mode 100644 zhaquirks/iluminize/dim.py diff --git a/zhaquirks/iluminize/__init__.py b/zhaquirks/iluminize/__init__.py new file mode 100644 index 0000000000..998941e7ac --- /dev/null +++ b/zhaquirks/iluminize/__init__.py @@ -0,0 +1,3 @@ +"""Module for iluminize devices.""" + +ILUMINIZE = "iluminize" diff --git a/zhaquirks/iluminize/cct.py b/zhaquirks/iluminize/cct.py new file mode 100644 index 0000000000..bba0c40fd6 --- /dev/null +++ b/zhaquirks/iluminize/cct.py @@ -0,0 +1,101 @@ +"""Quirk for iluminize CCT actor.""" +from zigpy.profiles import zll +from zigpy.quirks import CustomDevice, CustomCluster +from zigpy.zcl.clusters.general import ( + OnOff, + Basic, + Identify, + LevelControl, + Scenes, + Groups, + Ota, + GreenPowerProxy, +) +from zigpy.zcl.clusters.homeautomation import Diagnostic + +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.lightlink import LightLink + +from zhaquirks.const import ( + ENDPOINTS, + OUTPUT_CLUSTERS, + INPUT_CLUSTERS, + DEVICE_TYPE, + PROFILE_ID, + MODELS_INFO, +) +from zhaquirks.iluminize import ILUMINIZE + + +class IluminizeCCTColorCluster(CustomCluster, Color): + """iluminize CCT Lighting custom cluster.""" + + # Remove RGB color wheel for CCT Lighting: only expose color temperature + _CONSTANT_ATTRIBUTES = {0x400A: 16} + + +class CCTLight(CustomDevice): + """iluminize ZigBee LightLink CCT Lighting device.""" + + signature = { + MODELS_INFO: [(ILUMINIZE, "CCT Lighting")], + ENDPOINTS: { + 1: { + # Date: Sun, 1 Nov 2020 17:46:03 -0500 Subject: [PATCH 085/113] Add Quirk for Ecolink 4655BC0-R for Battery (#551) * Add Quirk for 4655BC0-R for Battery Add CustomPowerConfigurationCluster. Use MIN_VOLTS = 2.1 and MAX-VOLTS = 3.0 until we know better. * try to fix checks * try again to make checks happy --- zhaquirks/ecolink/__init__.py | 1 + zhaquirks/ecolink/contact.py | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 zhaquirks/ecolink/__init__.py create mode 100644 zhaquirks/ecolink/contact.py diff --git a/zhaquirks/ecolink/__init__.py b/zhaquirks/ecolink/__init__.py new file mode 100644 index 0000000000..33796bac12 --- /dev/null +++ b/zhaquirks/ecolink/__init__.py @@ -0,0 +1 @@ +"""Module for Ecolink quirks implementations.""" diff --git a/zhaquirks/ecolink/contact.py b/zhaquirks/ecolink/contact.py new file mode 100644 index 0000000000..0d203e1e04 --- /dev/null +++ b/zhaquirks/ecolink/contact.py @@ -0,0 +1,75 @@ +"""Ecolink 4655BC0-R device.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Identify, Ota, PollControl + +from zigpy.zcl.clusters.measurement import TemperatureMeasurement + +from zigpy.zcl.clusters.security import IasZone +from zhaquirks import PowerConfigurationCluster + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +DIAGNOSTICS_CLUSTER_ID = 0x0B05 # decimal = 2821 + + +class CustomPowerConfigurationCluster(PowerConfigurationCluster): + """Custom PowerConfigurationCluster.""" + + cluster_id = PowerConfigurationCluster.cluster_id + MIN_VOLTS = 2.1 + MAX_VOLTS = 3.0 + + +class Ecolink4655BC0R(CustomDevice): + """Ecolink 4655BC0-R device.""" + + signature = { + # + MODELS_INFO: [("Ecolink", "4655BC0-R")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + CustomPowerConfigurationCluster.cluster_id, + Identify.cluster_id, + PollControl.cluster_id, + TemperatureMeasurement.cluster_id, + IasZone.cluster_id, + DIAGNOSTICS_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + INPUT_CLUSTERS: [ + Basic.cluster_id, + CustomPowerConfigurationCluster, + Identify.cluster_id, + PollControl.cluster_id, + TemperatureMeasurement.cluster_id, + IasZone.cluster_id, + DIAGNOSTICS_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + } + } From c97d5b0ebf74c32c04c43a8f6308fe754490c81e Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 1 Nov 2020 23:52:53 +0100 Subject: [PATCH 086/113] Support default power on state for Hue Lily and Hue Calla (#540) --- zhaquirks/philips/zllextendedcolorlight.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zhaquirks/philips/zllextendedcolorlight.py b/zhaquirks/philips/zllextendedcolorlight.py index f4cd3042f4..0c04a3b057 100644 --- a/zhaquirks/philips/zllextendedcolorlight.py +++ b/zhaquirks/philips/zllextendedcolorlight.py @@ -49,6 +49,8 @@ class ZLLExtendedColorLight(CustomDevice): (PHILIPS, "LCT021"), (PHILIPS, "LCT024"), (PHILIPS, "LLC020"), + (PHILIPS, "LCF002"), + (PHILIPS, "LCS001"), ], ENDPOINTS: { 11: { From fb9c7d87973e6f7e5c8288046666185afbd8dce3 Mon Sep 17 00:00:00 2001 From: mjumnito Date: Sun, 1 Nov 2020 16:55:41 -0600 Subject: [PATCH 087/113] Added support for imagic by greatstar closes (#519) (#549) * Added support for iMagic by Greatstar (closes #519 ) * Added quirk for centralite 3450-L * Removed white spaces * Code cleanup * add 3450-l to file * Code Corrections * changed power cluster * power cluster config --- zhaquirks/centralite/motionandtemp.py | 150 ++++++++++++++++++++++++++ zhaquirks/imagic/__init__.py | 3 + zhaquirks/imagic/gs1117s.py | 79 ++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 zhaquirks/centralite/motionandtemp.py create mode 100644 zhaquirks/imagic/__init__.py create mode 100644 zhaquirks/imagic/gs1117s.py diff --git a/zhaquirks/centralite/motionandtemp.py b/zhaquirks/centralite/motionandtemp.py new file mode 100644 index 0000000000..df1636f654 --- /dev/null +++ b/zhaquirks/centralite/motionandtemp.py @@ -0,0 +1,150 @@ +"""Device handler for centralite 3450L.""" +# pylint disable=C0103 +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Identify, + OnOff, + OnOffConfiguration, + Ota, + PollControl, + PowerConfiguration, +) + +from zhaquirks import PowerConfigurationCluster +from zhaquirks.centralite import CENTRALITE +from zhaquirks.const import ( + BUTTON_1, + BUTTON_2, + BUTTON_3, + BUTTON_4, + COMMAND, + COMMAND_PRESS, + DEVICE_TYPE, + ENDPOINTS, + ENDPOINT_ID, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, + SHORT_PRESS, +) + +DIAGNOSTICS_CLUSTER_ID = 0x0B05 # decimal = 2821 + + +class CustomPowerConfigurationCluster(PowerConfigurationCluster): + """Custom PowerConfigurationCluster.""" + + cluster_id = PowerConfiguration.cluster_id + MIN_VOLTS = 2.1 + MAX_VOLTS = 3.0 + + +class CentraLite3450L(CustomDevice): + """Custom device representing centralite 3450L.""" + + signature = { + # + MODELS_INFO: [(CENTRALITE, "3450-L"), (CENTRALITE, "3450-L2")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + OnOffConfiguration.cluster_id, + PollControl.cluster_id, + DIAGNOSTICS_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + Ota.cluster_id, + ], + }, + 2: { + # input_clusters=[7] + # output_clusters=[6]> + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, + INPUT_CLUSTERS: [OnOffConfiguration.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 3: { + # input_clusters=[7] + # output_clusters=[6]> + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, + INPUT_CLUSTERS: [OnOffConfiguration.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 4: { + # input_clusters=[7] + # output_clusters=[6]> + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, + INPUT_CLUSTERS: [OnOffConfiguration.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, + INPUT_CLUSTERS: [ + Basic.cluster_id, + CustomPowerConfigurationCluster, + Identify.cluster_id, + OnOffConfiguration.cluster_id, + PollControl.cluster_id, + DIAGNOSTICS_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + OnOff.cluster_id, + Ota.cluster_id, + ], + }, + 2: { + # input_clusters=[7] + # output_clusters=[6]> + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, + INPUT_CLUSTERS: [OnOffConfiguration.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 3: { + # input_clusters=[7] + # output_clusters=[6]> + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, + INPUT_CLUSTERS: [OnOffConfiguration.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + 4: { + # input_clusters=[7] + # output_clusters=[6]> + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL, + INPUT_CLUSTERS: [OnOffConfiguration.cluster_id], + OUTPUT_CLUSTERS: [OnOff.cluster_id], + }, + } + } + + device_automation_triggers = { + (SHORT_PRESS, BUTTON_1): {COMMAND: COMMAND_PRESS, ENDPOINT_ID: 1}, + (SHORT_PRESS, BUTTON_2): {COMMAND: COMMAND_PRESS, ENDPOINT_ID: 2}, + (SHORT_PRESS, BUTTON_3): {COMMAND: COMMAND_PRESS, ENDPOINT_ID: 3}, + (SHORT_PRESS, BUTTON_4): {COMMAND: COMMAND_PRESS, ENDPOINT_ID: 4}, + } diff --git a/zhaquirks/imagic/__init__.py b/zhaquirks/imagic/__init__.py new file mode 100644 index 0000000000..3c47a382e7 --- /dev/null +++ b/zhaquirks/imagic/__init__.py @@ -0,0 +1,3 @@ +"""Module for GreatStar quirks implementations.""" + +IMAGIC = "iMagic by GreatStar" diff --git a/zhaquirks/imagic/gs1117s.py b/zhaquirks/imagic/gs1117s.py new file mode 100644 index 0000000000..9c905d4e7f --- /dev/null +++ b/zhaquirks/imagic/gs1117s.py @@ -0,0 +1,79 @@ +"""Device handler for iMagic by Greatstar.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Identify, + Ota, + PollControl, + PowerConfiguration, +) +from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.zcl.clusters.measurement import TemperatureMeasurement, RelativeHumidity +from zigpy.zcl.clusters.security import IasZone + +from zhaquirks import PowerConfigurationCluster + +from . import IMAGIC +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +MANUFACTURER_SPECIFIC_PROFILE_ID = 0xFC01 +MANUFACTURER_SPECIFIC_PROFILE_ID2 = 0xFC02 + + +class Greatstar(CustomDevice): + """Custom device representing iMagic by Greatstar motion sensors.""" + + signature = { + # + MODELS_INFO: [(IMAGIC, "1117-S")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + PollControl.cluster_id, + TemperatureMeasurement.cluster_id, + RelativeHumidity.cluster_id, + IasZone.cluster_id, + Diagnostic.cluster_id, + MANUFACTURER_SPECIFIC_PROFILE_ID, + MANUFACTURER_SPECIFIC_PROFILE_ID2, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic, + PowerConfigurationCluster, + Identify, + PollControl, + TemperatureMeasurement, + RelativeHumidity, + IasZone, + Diagnostic, + MANUFACTURER_SPECIFIC_PROFILE_ID, + MANUFACTURER_SPECIFIC_PROFILE_ID2, + ], + OUTPUT_CLUSTERS: [Identify, Ota], + } + } + } From b474b7428eb8b293372664e184a7f6a2364a6e46 Mon Sep 17 00:00:00 2001 From: Ignacio Larrain Date: Sun, 1 Nov 2020 19:57:12 -0300 Subject: [PATCH 088/113] Add support for Terncy Awareness switch and Terncy Knob (#539) * Add support for Terncy Awareness switch and Terncy Knob * Add support for Terncy Awareness switch and Terncy Knob (lint) --- zhaquirks/terncy/__init__.py | 153 +++++++++++++++++++++++++++++++++++ zhaquirks/terncy/pp01.py | 103 +++++++++++++++++++++++ zhaquirks/terncy/sd01.py | 67 +++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 zhaquirks/terncy/__init__.py create mode 100755 zhaquirks/terncy/pp01.py create mode 100644 zhaquirks/terncy/sd01.py diff --git a/zhaquirks/terncy/__init__.py b/zhaquirks/terncy/__init__.py new file mode 100644 index 0000000000..f43ab1442f --- /dev/null +++ b/zhaquirks/terncy/__init__.py @@ -0,0 +1,153 @@ +"""Module for Terncy quirks.""" +import math + +from zigpy.quirks import CustomCluster +from zigpy.zcl.clusters.measurement import ( + IlluminanceMeasurement, + TemperatureMeasurement, +) + +from .. import LocalDataCluster, _Motion, OccupancyOnEvent, OCCUPANCY_EVENT +from ..const import ( + BUTTON, + CLUSTER_COMMAND, + COMMAND, + DOUBLE_PRESS, + LEFT, + MOTION_EVENT, + ON, + PRESS_TYPE, + QUADRUPLE_PRESS, + QUINTUPLE_PRESS, + RIGHT, + SHORT_PRESS, + TRIPLE_PRESS, + VALUE, + ZHA_SEND_EVENT, + ZONE_STATE, +) + +CLICK_TYPES = {1: "single", 2: "double", 3: "triple", 4: "quadruple", 5: "quintuple"} +ROTATED = "device_rotated" +ROTATE_LEFT = "rotate_left" +ROTATE_RIGHT = "rotate_right" +SIDE_LOOKUP = {5: RIGHT, 7: RIGHT, 40: LEFT, 56: LEFT} +STEPS = "steps" +MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFCCC # decimal = 64716 +MOTION_TYPE = 0x000D +BUTTON_TRIGGERS = { + (SHORT_PRESS, BUTTON): {COMMAND: "button_single"}, + (DOUBLE_PRESS, BUTTON): {COMMAND: "button_double"}, + (TRIPLE_PRESS, BUTTON): {COMMAND: "button_triple"}, + (QUADRUPLE_PRESS, BUTTON): {COMMAND: "button_quadruple"}, + (QUINTUPLE_PRESS, BUTTON): {COMMAND: "button_quintuple"}, +} +KNOB_TRIGGERS = { + (ROTATED, RIGHT): {COMMAND: ROTATE_RIGHT}, + (ROTATED, LEFT): {COMMAND: ROTATE_LEFT}, +} +ZONE_TYPE = 0x0001 + + +class IlluminanceMeasurementCluster(CustomCluster, IlluminanceMeasurement): + """Terncy Illuminance Measurement Cluster.""" + + cluster_id = IlluminanceMeasurement.cluster_id + ATTR_ID = 0 + + def _update_attribute(self, attrid, value): + if attrid == self.ATTR_ID and value > 0: + value = 10000 * math.log10(value) + 1 + super()._update_attribute(attrid, value) + + +class TemperatureMeasurementCluster(CustomCluster, TemperatureMeasurement): + """Terncy Temperature Cluster.""" + + cluster_id = TemperatureMeasurement.cluster_id + ATTR_ID = 0 + + def _update_attribute(self, attrid, value): + if attrid == self.ATTR_ID: + value = value * 10.0 + super()._update_attribute(attrid, value) + + +class OccupancyCluster(OccupancyOnEvent): + """Occupancy cluster.""" + + +class MotionCluster(LocalDataCluster, _Motion): + """Motion cluster.""" + + _CONSTANT_ATTRIBUTES = {ZONE_TYPE: MOTION_TYPE} + reset_s: int = 5 + send_occupancy_event: bool = True + + def motion_event(self): + """Motion event.""" + super().listener_event(CLUSTER_COMMAND, 254, ZONE_STATE, [ON, 0, 0, 0]) + + if self._timer_handle: + self._timer_handle.cancel() + + self._timer_handle = self._loop.call_later(self.reset_s, self._turn_off) + + if self.send_occupancy_event: + self.endpoint.device.occupancy_bus.listener_event(OCCUPANCY_EVENT) + + +class MotionClusterLeft(MotionCluster): + """Motion cluster.""" + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.motion_left_bus.add_listener(self) + + +class MotionClusterRight(MotionCluster): + """Motion cluster.""" + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.motion_right_bus.add_listener(self) + + +class TerncyRawCluster(CustomCluster): + """Terncy Raw Cluster.""" + + cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID + name = "Terncy Raw cluster" + # ep_attribute = "accelerometer" + + def handle_cluster_request(self, tsn, command_id, args): + """Handle a cluster command received on this cluster.""" + args = list(args) + if command_id == 0: # click event + count = args[0] + state = args[1] + if state > 5: + state = 5 + event_args = {PRESS_TYPE: CLICK_TYPES[state], "count": count, VALUE: state} + action = "button_{}".format(CLICK_TYPES[state]) + self.listener_event(ZHA_SEND_EVENT, action, event_args) + elif command_id == 4: # motion event + state = args[2] + side = SIDE_LOOKUP[state] + if side == LEFT: + self.endpoint.device.motion_left_bus.listener_event(MOTION_EVENT) + elif side == RIGHT: + self.endpoint.device.motion_right_bus.listener_event(MOTION_EVENT) + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == 27: # knob rotate event + if value > 0: + action = ROTATE_RIGHT + else: + action = ROTATE_LEFT + steps = value / 12 + event_args = {STEPS: abs(steps)} + self.listener_event(ZHA_SEND_EVENT, action, event_args) diff --git a/zhaquirks/terncy/pp01.py b/zhaquirks/terncy/pp01.py new file mode 100755 index 0000000000..f5547d8321 --- /dev/null +++ b/zhaquirks/terncy/pp01.py @@ -0,0 +1,103 @@ +"""Device handler for Terncy awareness switch.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Identify, + Ota, + PollControl, + PowerConfiguration, +) +from zigpy.zcl.clusters.measurement import ( + TemperatureMeasurement, + IlluminanceMeasurement, + OccupancySensing, +) + +from zhaquirks import DoublingPowerConfigurationCluster + +from . import ( + IlluminanceMeasurementCluster, + TemperatureMeasurementCluster, + TerncyRawCluster, + OccupancyCluster, + MotionClusterLeft, + MotionClusterRight, + BUTTON_TRIGGERS, +) +from .. import Bus +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +TERNCY_AWARENESS_DEVICE_TYPE = 0x01F0 + + +class TerncyAwarenessSwitch(CustomDevice): + """Terncy awareness switch.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.motion_left_bus = Bus() + self.motion_right_bus = Bus() + self.occupancy_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + # + MODELS_INFO: [("Xiaoyan", "TERNCY-PP01"), (None, "TERNCY-PP01")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: TERNCY_AWARENESS_DEVICE_TYPE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + PollControl.cluster_id, + IlluminanceMeasurement.cluster_id, + TemperatureMeasurement.cluster_id, + OccupancySensing.cluster_id, + TerncyRawCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: TERNCY_AWARENESS_DEVICE_TYPE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DoublingPowerConfigurationCluster, + Identify.cluster_id, + PollControl.cluster_id, + IlluminanceMeasurementCluster, + TemperatureMeasurementCluster, + MotionClusterLeft, + OccupancyCluster, + TerncyRawCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: TERNCY_AWARENESS_DEVICE_TYPE, + INPUT_CLUSTERS: [MotionClusterRight], + OUTPUT_CLUSTERS: [], + }, + } + } + + device_automation_triggers = BUTTON_TRIGGERS diff --git a/zhaquirks/terncy/sd01.py b/zhaquirks/terncy/sd01.py new file mode 100644 index 0000000000..0b0dc22ff5 --- /dev/null +++ b/zhaquirks/terncy/sd01.py @@ -0,0 +1,67 @@ +"""Device handler for Terncy knob smart dimmer.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + PowerConfiguration, + Identify, + PollControl, + Ota, +) + +from zhaquirks import DoublingPowerConfigurationCluster + +from . import TerncyRawCluster, BUTTON_TRIGGERS, KNOB_TRIGGERS +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +TERNCY_KNOB_DEVICE_TYPE = 0x01F2 + + +class TerncyKnobSmartDimmer(CustomDevice): + """Terncy knob smart dimmer.""" + + signature = { + MODELS_INFO: [("Xiaoyan", "TERNCY-SD01"), (None, "TERNCY-SD01")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: TERNCY_KNOB_DEVICE_TYPE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + PollControl.cluster_id, + TerncyRawCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic.cluster_id, + DoublingPowerConfigurationCluster, + Identify.cluster_id, + PollControl.cluster_id, + TerncyRawCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + } + } + } + + device_automation_triggers = {**BUTTON_TRIGGERS, **KNOB_TRIGGERS} From cae0d3717ed99488f12dd363a6e7131a06a81431 Mon Sep 17 00:00:00 2001 From: "Julien \"_FrnchFrgg_\" Rivaud" Date: Mon, 2 Nov 2020 12:43:51 +0100 Subject: [PATCH 089/113] Support for Tuya-based NEO Siren (closes #444) (#530) * Initial support for Tuya-based NEO Siren Also implement a base class to map non-standard tuya data to attributes. Custom tuya clusters can derive from that class and any defined attribute on the cluster will be automatically mapped. * Implement temperature and humidity clusters * Implement the On/Off cluster * Update documentation Co-authored-by: Julien '_FrnchFrgg_' RIVAUD --- Contributors.md | 1 + README.md | 1 + zhaquirks/tuya/__init__.py | 69 ++++++++++++++ zhaquirks/tuya/siren.py | 179 +++++++++++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+) create mode 100644 zhaquirks/tuya/siren.py diff --git a/Contributors.md b/Contributors.md index 94dcfc68f5..e6629d94d6 100644 --- a/Contributors.md +++ b/Contributors.md @@ -21,3 +21,4 @@ - [Gleb Sinyavskiy](https://github.com/zhulik) - [Michael Thingnes](https://github.com/thimic) - [Piotr Mis](https://github.com/piomis) +- [Julien Rivaud](https://github.com/FrnchFrgg) diff --git a/README.md b/README.md index 478b663557..b056c7751c 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ If you are looking to make your first code contribution to this project then we ### Tuya-based - [TS0601 switch](https://zigbee.blakadder.com/Lerlink_X701A.html): Tuya-based 1-gang switches with neutral (e.g. Lerlink, Lonsonho) +- [Neo Siren](https://zigbee.blakadder.com/Neo_NAS-AB02B0.html): Tuya-based alarm siren with temperature and humidity sensors # Configuration: diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py index ee7ffa03b0..29bd65dd5d 100644 --- a/zhaquirks/tuya/__init__.py +++ b/zhaquirks/tuya/__init__.py @@ -51,6 +51,75 @@ class Command(t.Struct): } +class TuyaManufClusterAttributes(TuyaManufCluster): + """Manufacturer specific cluster for Tuya converting attributes <-> commands.""" + + def handle_cluster_request(self, tsn: int, command_id: int, args: Tuple) -> None: + """Handle cluster request.""" + if command_id not in (0x0001, 0x0002): + return super().handle_cluster_request(tsn, command_id, args) + + tuya_cmd = args[0].command_id + tuya_value = args[0].data[1:] # first uint8_t is length + + _LOGGER.debug( + "[0x%04x:%s:0x%04x] Received value %s " + "for attribute 0x%04x (command 0x%04x)", + self.endpoint.device.nwk, + self.endpoint.endpoint_id, + self.cluster_id, + repr(tuya_value), + tuya_cmd, + command_id, + ) + + if tuya_cmd not in self.attributes: + return + + # tuya data is in big endian whereas ztypes use little endian + ztype = self.attributes[tuya_cmd][1] + zvalue, _ = ztype.deserialize(bytes(reversed(tuya_value))) + self._update_attribute(tuya_cmd, zvalue) + + def read_attributes( + self, attributes, allow_cache=False, only_cache=False, manufacturer=None + ): + """Ignore remote reads as the "get_data" command doesn't seem to do anything.""" + + return super().read_attributes( + attributes, allow_cache=True, only_cache=True, manufacturer=manufacturer + ) + + async def write_attributes(self, attributes, manufacturer=None): + """Defer attributes writing to the set_data tuya command.""" + + records = self._write_attr_records(attributes) + + for record in records: + # serialized in little-endian + data = list(record.value.value.serialize()) + # we want big-endian, with length prepended + data.append(len(data)) + data.reverse() + + cmd_payload = TuyaManufCluster.Command() + cmd_payload.status = 0 + cmd_payload.tsn = self.endpoint.device.application.get_sequence() + cmd_payload.command_id = record.attrid + cmd_payload.function = 0 + cmd_payload.data = data + + await super().command( + TUYA_SET_DATA, + cmd_payload, + manufacturer=manufacturer, + expect_reply=False, + tsn=cmd_payload.tsn, + ) + + return (foundation.Status.SUCCESS,) + + class TuyaOnOff(CustomCluster, OnOff): """Tuya On/Off cluster for On/Off device.""" diff --git a/zhaquirks/tuya/siren.py b/zhaquirks/tuya/siren.py new file mode 100644 index 0000000000..5df8613550 --- /dev/null +++ b/zhaquirks/tuya/siren.py @@ -0,0 +1,179 @@ +"""Map from manufacturer to standard clusters for the NEO Siren device.""" +import logging + +from typing import Optional, Union + +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Identify, Ota, OnOff +from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement +from zigpy.zcl import foundation +import zigpy.types as t + +from .. import Bus, LocalDataCluster +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +from . import TuyaManufClusterAttributes + +TUYA_ALARM_ATTR = 0x0168 # [0]/[1] Alarm! +TUYA_TEMP_ALARM_ATTR = 0x0171 # [0]/[1] Disable/Enable alarm by temperature +TUYA_HUMID_ALARM_ATTR = 0x0172 # [0]/[1] Disable/Enable alarm by humidity +TUYA_ALARM_DURATION_ATTR = 0x0267 # [0,0,0,10] duration alarm in second +TUYA_TEMPERATURE_ATTR = 0x0269 # [0,0,0,240] temperature in decidegree +TUYA_HUMIDITY_ATTR = 0x026A # [0,0,0,36] humidity +TUYA_ALARM_MIN_TEMP_ATTR = 0x026B # [0,0,0,18] min alarm temperature threshold +TUYA_ALARM_MAX_TEMP_ATTR = 0x026C # [0,0,0,18] max alarm temperature threshold +TUYA_ALARM_MIN_HUMID_ATTR = 0x026D # [0,0,0,18] min alarm humidity threshold +TUYA_ALARM_MAX_HUMID_ATTR = 0x026E # [0,0,0,18] max alarm humidity threshold +TUYA_MELODY_ATTR = 0x0466 # [5] Melody +TUYA_VOLUME_ATTR = 0x0474 # [0]/[1]/[2] Volume 0-max, 2-low + +_LOGGER = logging.getLogger(__name__) + + +class TuyaManufClusterSiren(TuyaManufClusterAttributes): + """Manufacturer Specific Cluster of the NEO Siren device.""" + + manufacturer_attributes = { + TUYA_ALARM_ATTR: ("alarm", t.uint8_t), + TUYA_TEMP_ALARM_ATTR: ("enable_temperature_alarm", t.uint8_t), + TUYA_HUMID_ALARM_ATTR: ("enable_humidity_alarm", t.uint8_t), + TUYA_ALARM_DURATION_ATTR: ("alarm_duration", t.uint32_t), + TUYA_TEMPERATURE_ATTR: ("temperature", t.uint32_t), + TUYA_HUMIDITY_ATTR: ("humidity", t.uint32_t), + TUYA_ALARM_MIN_TEMP_ATTR: ("alarm_temperature_min", t.uint32_t), + TUYA_ALARM_MAX_TEMP_ATTR: ("alarm_temperature_max", t.uint32_t), + TUYA_ALARM_MIN_HUMID_ATTR: ("alarm_humidity_min", t.uint32_t), + TUYA_ALARM_MAX_HUMID_ATTR: ("alarm_humidity_max", t.uint32_t), + TUYA_MELODY_ATTR: ("melody", t.uint8_t), + TUYA_VOLUME_ATTR: ("volume", t.uint8_t), + } + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == TUYA_TEMPERATURE_ATTR: + self.endpoint.device.temperature_bus.listener_event( + "temperature_reported", value * 10 # decidegree to centidegree + ) + elif attrid == TUYA_HUMIDITY_ATTR: + self.endpoint.device.humidity_bus.listener_event( + "humidity_reported", value * 100 # whole percentage to 1/1000th + ) + elif attrid == TUYA_ALARM_ATTR: + self.endpoint.device.switch_bus.listener_event( + "switch_event", value # boolean 1=on / 0=off + ) + + +class TuyaSirenOnOff(LocalDataCluster, OnOff): + """Tuya On/Off cluster for siren device.""" + + ATTR_ID = 0 + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.switch_bus.add_listener(self) + + def switch_event(self, state): + """Switch event.""" + self._update_attribute(self.ATTR_ID, state) + + def command( + self, + command_id: Union[foundation.Command, int, t.uint8_t], + *args, + manufacturer: Optional[Union[int, t.uint16_t]] = None, + expect_reply: bool = True, + tsn: Optional[Union[int, t.uint8_t]] = None, + ): + """Override the default command and defer to the alarm attribute.""" + + if command_id in (0x0000, 0x0001): + return self.endpoint.tuya_manufacturer.write_attributes( + {TUYA_ALARM_ATTR: command_id}, manufacturer=manufacturer + ) + + return foundation.Status.UNSUP_CLUSTER_COMMAND + + +class TuyaTemperatureMeasurement(LocalDataCluster, TemperatureMeasurement): + """Temperature cluster acting from events from temperature bus.""" + + cluster_id = TemperatureMeasurement.cluster_id + ATTR_ID = 0 + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.temperature_bus.add_listener(self) + + def temperature_reported(self, value): + """Temperature reported.""" + self._update_attribute(self.ATTR_ID, value) + + +class TuyaRelativeHumidity(LocalDataCluster, RelativeHumidity): + """Humidity cluster acting from events from humidity bus.""" + + cluster_id = RelativeHumidity.cluster_id + ATTR_ID = 0 + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.humidity_bus.add_listener(self) + + def humidity_reported(self, value): + """Humidity reported.""" + self._update_attribute(self.ATTR_ID, value) + + +class TuyaSiren(CustomDevice): + """NEO Tuya Siren and humidity/temperature sensor.""" + + def __init__(self, *args, **kwargs): + """Init device.""" + self.temperature_bus = Bus() + self.humidity_bus = Bus() + self.switch_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + # endpoint=1 profile=260 device_type=0 device_version=0 input_clusters=[0, 3] + # output_clusters=[3, 25]> + MODELS_INFO: [("_TYST11_d0yu2xgi", "0yu2xgi")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [Basic.cluster_id, Identify.cluster_id], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_WARNING_DEVICE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + TuyaManufClusterSiren, + TuyaTemperatureMeasurement, + TuyaRelativeHumidity, + TuyaSirenOnOff, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + } + } From 18ecee3c6fb35d70e9b18c10e07e8fdde446c58d Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 3 Nov 2020 16:59:33 +0100 Subject: [PATCH 090/113] Added additional Konke magnet sensor quirk with different model (#564) --- zhaquirks/konke/magnet.py | 43 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/zhaquirks/konke/magnet.py b/zhaquirks/konke/magnet.py index e96134e6f6..4d431cd1e0 100644 --- a/zhaquirks/konke/magnet.py +++ b/zhaquirks/konke/magnet.py @@ -23,7 +23,7 @@ class KonkeMagnet(CustomDevice): """Custom device representing konke magnet sensors.""" signature = { - # @@ -60,3 +60,44 @@ class KonkeMagnet(CustomDevice): } } } + + +class KonkeMagnet2(CustomDevice): + """Custom device representing konke magnet sensors without custom konke cluster.""" + + signature = { + # + MODELS_INFO: [(KONKE, "3AFE130104020015")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + IasZone.cluster_id, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfigurationCluster, + Identify.cluster_id, + IasZone.cluster_id, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id], + } + } + } From 6682592d6b3935e495503ab20177312b8dd6b55a Mon Sep 17 00:00:00 2001 From: sarakha63 Date: Tue, 3 Nov 2020 19:37:39 +0100 Subject: [PATCH 091/113] added power configuration to motion aq2b (#563) --- zhaquirks/xiaomi/aqara/motion_aq2b.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zhaquirks/xiaomi/aqara/motion_aq2b.py b/zhaquirks/xiaomi/aqara/motion_aq2b.py index 8f5ed9d502..d160f89e34 100644 --- a/zhaquirks/xiaomi/aqara/motion_aq2b.py +++ b/zhaquirks/xiaomi/aqara/motion_aq2b.py @@ -10,6 +10,7 @@ IlluminanceMeasurementCluster, MotionCluster, OccupancyCluster, + PowerConfigurationCluster, XiaomiCustomDevice, ) from ... import Bus @@ -63,6 +64,7 @@ def __init__(self, *args, **kwargs): 1: { INPUT_CLUSTERS: [ BasicCluster, + PowerConfigurationCluster, IlluminanceMeasurementCluster, OccupancyCluster, MotionCluster, From 9e88e84e952b7443cea7218d4dc43df1e2a30ce9 Mon Sep 17 00:00:00 2001 From: glassbase Date: Wed, 4 Nov 2020 13:34:26 -0500 Subject: [PATCH 092/113] Hue White Dimmable - Expose PhilipsLevelControlCluster (#556) * Hue White Dimmable - Expose PhilipsLevelControlCluster In order for PhilipsOnOff power restore to work, you also need to be able to set Level Control cluster 0x0008, PowerOn Level attribute 0x4000 to value 255 (0xff). I edited the quirk to add the Level Control cluster like above. When I read/get to attribute ox4000, it is set as 254 as default. This causes the bulb to restore to full brightness after power restore. If set as 255, it restores to previous brightness. Findings based on this https://github.com/dresden-elektronik/deconz-rest-plugin/issues/1055#issuecomment-559993258 * Update zhaquirks/philips/zlldimmablelight.py Co-authored-by: Alexei Chetroi * Change philips import to be one line Co-authored-by: Alexei Chetroi --- zhaquirks/philips/zlldimmablelight.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zhaquirks/philips/zlldimmablelight.py b/zhaquirks/philips/zlldimmablelight.py index c64a402979..33fc153fac 100644 --- a/zhaquirks/philips/zlldimmablelight.py +++ b/zhaquirks/philips/zlldimmablelight.py @@ -22,7 +22,8 @@ PROFILE_ID, MODELS_INFO, ) -from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster + +from zhaquirks.philips import PHILIPS, PhilipsLevelControlCluster, PhilipsOnOffCluster class ZLLDimmableLight(CustomDevice): @@ -73,7 +74,7 @@ class ZLLDimmableLight(CustomDevice): Groups.cluster_id, Scenes.cluster_id, PhilipsOnOffCluster, - LevelControl.cluster_id, + PhilipsLevelControlCluster, LightLink.cluster_id, ], OUTPUT_CLUSTERS: [Ota.cluster_id], From dbc159f27645912ea8c05853e63aae8e5e150fea Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 6 Nov 2020 17:54:33 -0500 Subject: [PATCH 093/113] Ikea open/close remote triggers (#566) --- zhaquirks/ikea/opencloseremote.py | 51 +++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/zhaquirks/ikea/opencloseremote.py b/zhaquirks/ikea/opencloseremote.py index 06f12d1293..a5f21493d6 100644 --- a/zhaquirks/ikea/opencloseremote.py +++ b/zhaquirks/ikea/opencloseremote.py @@ -1,6 +1,8 @@ """Device handler for IKEA of Sweden TRADFRI remote control.""" +from typing import List + from zigpy.profiles import zha -from zigpy.quirks import CustomDevice +from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.closures import WindowCovering from zigpy.zcl.clusters.general import ( Alarms, @@ -18,17 +20,55 @@ from . import IKEA from .. import DoublingPowerConfigurationCluster from ..const import ( + ARGS, + CLOSE, + COMMAND, + COMMAND_STOP, DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, + LONG_RELEASE, MODELS_INFO, + OPEN, OUTPUT_CLUSTERS, PROFILE_ID, + SHORT_PRESS, + ZHA_SEND_EVENT, ) +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): + """Ikea Window covering cluster.""" + + def __init__(self, *args, **kwargs): + """Initialize instance.""" + super().__init__(*args, **kwargs) + self._is_closing = None + + def handle_cluster_request( + self, tsn: int, command_id: int, args: List[int] + ) -> None: + """Handle cluster specific commands. + + We just want to keep track of direction, to associate it with the stop command. + """ + + cmd_name = self.server_commands.get(command_id, [command_id])[0] + if cmd_name == COMMAND_OPEN: + self._is_closing = False + elif cmd_name == COMMAND_CLOSE: + self._is_closing = True + elif cmd_name == COMMAND_STOP: + action = COMMAND_STOP_CLOSING if self._is_closing else COMMAND_STOP_OPENING + self.listener_event(ZHA_SEND_EVENT, action, []) + + class IkeaTradfriOpenCloseRemote(CustomDevice): """Custom device representing IKEA of Sweden TRADFRI remote control.""" @@ -88,9 +128,16 @@ class IkeaTradfriOpenCloseRemote(CustomDevice): OnOff.cluster_id, LevelControl.cluster_id, Ota.cluster_id, - WindowCovering.cluster_id, + IkeaWindowCovering, LightLink.cluster_id, ], } }, } + + device_automation_triggers = { + (SHORT_PRESS, OPEN): {COMMAND: COMMAND_OPEN, ARGS: []}, + (LONG_RELEASE, OPEN): {COMMAND: COMMAND_STOP_OPENING, ARGS: []}, + (SHORT_PRESS, CLOSE): {COMMAND: COMMAND_CLOSE, ARGS: []}, + (LONG_RELEASE, CLOSE): {COMMAND: COMMAND_STOP_CLOSING, ARGS: []}, + } From 0e3cb20aecdcf48c36d87572ef820abf4df2c9b9 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 8 Nov 2020 07:18:37 -0500 Subject: [PATCH 094/113] Switch CI to GitHub Actions (#568) * update precommit * format yaml * add ci workflow and matchers * update ci * remove coverage for now * don't run jobs 2x on PRs * update requirements * update isort * update isort config * sort imports * sort tests * add coverage back * remove travis * set fail under to 79 until we have better coverage * review comment --- .github/workflows/ci.yml | 332 +++++++++++++++++++++ .github/workflows/matchers/flake8.json | 30 ++ .github/workflows/matchers/python.json | 18 ++ .isort.cfg | 10 +- .pre-commit-config.yaml | 16 +- .travis.yml | 19 -- requirements_test_all.txt | 28 +- setup.cfg | 13 +- tests/conftest.py | 2 +- tests/test_ctrl_neutral1.py | 1 - tests/test_kof.py | 1 + tests/test_quirks.py | 1 + tests/test_tuya.py | 3 +- zhaquirks/__init__.py | 10 +- zhaquirks/centralite/3310S.py | 4 +- zhaquirks/centralite/motion.py | 1 + zhaquirks/centralite/motionandtemp.py | 2 +- zhaquirks/danfoss/thermostat.py | 2 +- zhaquirks/develco/open_close.py | 2 +- zhaquirks/echostar/bell.py | 2 +- zhaquirks/ecolink/contact.py | 4 +- zhaquirks/edpwithus/redy_plug.py | 4 +- zhaquirks/eurotronic/__init__.py | 3 +- zhaquirks/ikea/__init__.py | 2 +- zhaquirks/ikea/fivebtnremotezha.py | 2 +- zhaquirks/ikea/motionzha.py | 2 +- zhaquirks/ikea/tradfriplug.py | 8 +- zhaquirks/iluminize/cct.py | 17 +- zhaquirks/iluminize/dim.py | 17 +- zhaquirks/imagic/gs1117s.py | 2 +- zhaquirks/kof/kof_mr101z.py | 8 +- zhaquirks/konke/__init__.py | 2 +- zhaquirks/konke/magnet.py | 2 +- zhaquirks/orvibo/motion.py | 4 +- zhaquirks/osram/switchmini.py | 30 +- zhaquirks/osram/tunablewhite.py | 6 +- zhaquirks/philips/__init__.py | 9 +- zhaquirks/philips/llc011.py | 15 +- zhaquirks/philips/lst002.py | 15 +- zhaquirks/philips/rom001.py | 8 +- zhaquirks/philips/zhaextendedcolorlight.py | 15 +- zhaquirks/philips/zlldimmablelight.py | 16 +- zhaquirks/philips/zllextendedcolorlight.py | 15 +- zhaquirks/samjin/multi2.py | 2 +- zhaquirks/sercomm/szwtd02n.py | 2 +- zhaquirks/sinope/light.py | 3 +- zhaquirks/smartthings/__init__.py | 2 +- zhaquirks/smartthings/multi.py | 7 +- zhaquirks/smartthings/multiv4.py | 1 + zhaquirks/smartthings/pgc313.py | 2 +- zhaquirks/smartthings/pgc314.py | 2 +- zhaquirks/terncy/__init__.py | 2 +- zhaquirks/terncy/pp01.py | 10 +- zhaquirks/terncy/sd01.py | 6 +- zhaquirks/tuya/__init__.py | 6 +- zhaquirks/tuya/motion.py | 7 +- zhaquirks/tuya/singleswitch.py | 5 +- zhaquirks/tuya/siren.py | 10 +- zhaquirks/visonic/mct340e.py | 1 + zhaquirks/xiaomi/__init__.py | 2 +- zhaquirks/xiaomi/aqara/ctrl_ln.py | 5 +- zhaquirks/xiaomi/aqara/ctrl_neutral.py | 4 +- zhaquirks/xiaomi/aqara/cube.py | 2 +- zhaquirks/xiaomi/aqara/plug.py | 14 +- zhaquirks/xiaomi/aqara/wleak_aq1.py | 2 +- zhaquirks/xiaomi/mija/sensor_magnet.py | 2 +- 66 files changed, 579 insertions(+), 223 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/matchers/flake8.json create mode 100644 .github/workflows/matchers/python.json delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..bce4c0d5a5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,332 @@ +name: CI + +# yamllint disable-line rule:truthy +on: + push: + branches: + - dev + - master + pull_request: ~ + +env: + CACHE_VERSION: 1 + DEFAULT_PYTHON: 3.7 + PRE_COMMIT_HOME: ~/.cache/pre-commit + +jobs: + # Separate job to pre-populate the base dependency cache + # This prevent upcoming jobs to do the same individually + prepare-base: + name: Prepare base dependencies + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v2.1.4 + with: + python-version: ${{ matrix.python-version }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements_test_all.txt') }} + restore-keys: | + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + pip install -U pip setuptools pre-commit + pip install -r requirements_test_all.txt + pip install -e . + + pre-commit: + name: Prepare pre-commit environment + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.1.4 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements_test_all.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- + - name: Install pre-commit dependencies + if: steps.cache-precommit.outputs.cache-hit != 'true' + run: | + . venv/bin/activate + pre-commit install-hooks + + lint-black: + name: Check black + runs-on: ubuntu-latest + needs: pre-commit + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.1.4 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements_test_all.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run black + run: | + . venv/bin/activate + pre-commit run --hook-stage manual black --all-files --show-diff-on-failure + + lint-flake8: + name: Check flake8 + runs-on: ubuntu-latest + needs: pre-commit + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.1.4 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements_test_all.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register flake8 problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/flake8.json" + - name: Run flake8 + run: | + . venv/bin/activate + pre-commit run --hook-stage manual flake8 --all-files + + lint-isort: + name: Check isort + runs-on: ubuntu-latest + needs: pre-commit + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2.1.4 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements_test_all.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run isort + run: | + . venv/bin/activate + pre-commit run --hook-stage manual isort --all-files --show-diff-on-failure + + pytest: + runs-on: ubuntu-latest + needs: prepare-base + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + name: >- + Run tests Python ${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2.1.4 + id: python + with: + python-version: ${{ matrix.python-version }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements_test_all.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register Python problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Install Pytest Annotation plugin + run: | + . venv/bin/activate + # Ideally this should be part of our dependencies + # However this plugin is fairly new and doesn't run correctly + # on a non-GitHub environment. + pip install pytest-github-actions-annotate-failures + - name: Run pytest + run: | + . venv/bin/activate + pytest \ + -qq \ + --timeout=9 \ + --durations=10 \ + --cov zhaquirks \ + -o console_output_style=count \ + -p no:sugar \ + tests + - name: Upload coverage artifact + uses: actions/upload-artifact@v2.2.0 + with: + name: coverage-${{ matrix.python-version }} + path: .coverage + - name: Coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_FLAG_NAME: ${{ matrix.python-version }} + COVERALLS_PARALLEL: true + run: | + . venv/bin/activate + coveralls + + coverage: + name: Process test coverage + runs-on: ubuntu-latest + needs: pytest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2.1.4 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements_test_all.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Download all coverage artifacts + uses: actions/download-artifact@v2 + - name: Combine coverage results + run: | + . venv/bin/activate + coverage combine coverage*/.coverage* + coverage report --fail-under=79 + coverage xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1.0.14 + - name: Upload coverage to Coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + . venv/bin/activate + coveralls --finish diff --git a/.github/workflows/matchers/flake8.json b/.github/workflows/matchers/flake8.json new file mode 100644 index 0000000000..e059a1cf5f --- /dev/null +++ b/.github/workflows/matchers/flake8.json @@ -0,0 +1,30 @@ +{ + "problemMatcher": [ + { + "owner": "flake8-error", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + }, + { + "owner": "flake8-warning", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/python.json b/.github/workflows/matchers/python.json new file mode 100644 index 0000000000..3e5d8d5b8b --- /dev/null +++ b/.github/workflows/matchers/python.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "python", + "pattern": [ + { + "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", + "message": 2 + } + ] + } + ] +} diff --git a/.isort.cfg b/.isort.cfg index ac444f2aa2..5f1f46c022 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,8 +2,7 @@ # https://github.com/timothycrosley/isort # https://github.com/timothycrosley/isort/wiki/isort-Settings # splits long import on multiple lines indented by 4 spaces -multi_line_output = 4 -indent = " " +profile = black # by default isort don't check module indexes not_skip = __init__.py # will group `import x` and `from x import` of the same module. @@ -13,4 +12,9 @@ default_section = THIRDPARTY known_first_party = zhaquirks,tests forced_separate = tests combine_as_imports = true -use_parentheses = true \ No newline at end of file +use_parentheses = true +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +line_length = 88 +indent = " " \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 78b7ec2985..1f388198bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,19 @@ repos: -- repo: https://github.com/psf/black - rev: 19.3b0 + - repo: https://github.com/psf/black + rev: 20.8b1 hooks: - - id: black + - id: black args: - --safe - --quiet -- repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.8 + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 hooks: - - id: flake8 + - id: flake8 additional_dependencies: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 + - repo: https://github.com/PyCQA/isort + rev: 5.5.2 + hooks: + - id: isort diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9d106de634..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -sudo: false -dist: xenial -matrix: - fast_finish: true - include: - - python: "3.7" - env: TOXENV=lint - - python: "3.7" - env: TOXENV=pylint - - python: "3.7" - env: TOXENV=py37 - sudo: true - - python: "3.8" - env: TOXENV=py38 - -cache: pip -install: pip install -U tox -language: python -script: travis_wait 40 tox --develop diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1814082b1..68822d421f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -3,19 +3,17 @@ # make new things fail. Manually update these pins when pulling in a # new version asynctest==0.13.0 -black==19.3b0 -codecov==2.0.15 -coveralls==1.2.0 -flake8-docstrings==1.3.1 -flake8==3.7.8 -mock-open==1.3.1 -mypy==0.720 -pre-commit==1.18.3 -pydocstyle==4.0.1 -pylint==2.3.1 +codecov==2.1.10 +coveralls==2.1.2 +flake8-docstrings==1.5.0 +mock-open==1.4.0 +mypy==0.790 +pre-commit==2.8.2 +pydocstyle==5.1.1 +pylint==2.6.0 pytest-aiohttp==0.3.0 -pytest-cov==2.7.1 -pytest-sugar==0.9.2 -pytest-timeout==1.3.3 -pytest==5.1.2 -requests_mock==1.6.0 +pytest-cov==2.10.1 +pytest-sugar==0.9.4 +pytest-timeout==1.4.2 +pytest==6.1.2 +requests_mock==1.8.0 diff --git a/setup.cfg b/setup.cfg index 4623728436..cb6dd04e56 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,12 +20,7 @@ ignore = # https://github.com/timothycrosley/isort # https://github.com/timothycrosley/isort/wiki/isort-Settings # splits long import on multiple lines indented by 4 spaces -multi_line_output = 3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 -indent = " " +profile = black # by default isort don't check module indexes not_skip = __init__.py # will group `import x` and `from x import` of the same module. @@ -35,6 +30,12 @@ default_section = THIRDPARTY known_first_party = zhaquirks,tests forced_separate = tests combine_as_imports = true +use_parentheses = true +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +line_length = 88 +indent = " " [mypy] python_version = 3.7 diff --git a/tests/conftest.py b/tests/conftest.py index 20c962cd95..ade83f320e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,8 +2,8 @@ from asynctest import CoroutineMock import pytest -import zigpy.device import zigpy.application +import zigpy.device import zigpy.types from zhaquirks.const import ( diff --git a/tests/test_ctrl_neutral1.py b/tests/test_ctrl_neutral1.py index 4ca080e1a6..1a12da79af 100644 --- a/tests/test_ctrl_neutral1.py +++ b/tests/test_ctrl_neutral1.py @@ -3,7 +3,6 @@ from zhaquirks.xiaomi.aqara.ctrl_neutral import CtrlNeutral - # zigbee-herdsman:controller:endpoint Command 0x00158d00024be541/2 genOnOff.on({}, # {"timeout":6000,"manufacturerCode":null,"disableDefaultResponse":false}) # zigbee-herdsman:adapter:zStack:znp:SREQ --> AF - dataRequest - diff --git a/tests/test_kof.py b/tests/test_kof.py index 16cb65053b..c65057f9ca 100644 --- a/tests/test_kof.py +++ b/tests/test_kof.py @@ -4,6 +4,7 @@ import zigpy.device import zigpy.endpoint import zigpy.quirks + import zhaquirks.kof.kof_mr101z diff --git a/tests/test_quirks.py b/tests/test_quirks.py index dfe0701a99..28d9dd8937 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -2,6 +2,7 @@ import pytest import zigpy.quirks as zq + import zhaquirks # noqa: F401, E402 from zhaquirks.const import ENDPOINTS diff --git a/tests/test_tuya.py b/tests/test_tuya.py index fb19d9f381..28dd2f43ab 100644 --- a/tests/test_tuya.py +++ b/tests/test_tuya.py @@ -4,11 +4,10 @@ from unittest import mock import pytest +from zigpy.zcl import foundation from zhaquirks.const import OFF, ON, ZONE_STATE - import zhaquirks.tuya.motion -from zigpy.zcl import foundation from tests.common import ClusterListener diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 081f5b853a..e6d491cfb5 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -1,15 +1,15 @@ """Quirks implementations for the ZHA component of Homeassistant.""" import asyncio -import logging import importlib +import logging import pkgutil from zigpy.quirks import CustomCluster from zigpy.util import ListenableMixin from zigpy.zcl import foundation from zigpy.zcl.clusters.general import PowerConfiguration -from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.measurement import OccupancySensing +from zigpy.zcl.clusters.security import IasZone from zigpy.zdo import types as zdotypes from .const import ( @@ -17,13 +17,13 @@ ATTRIBUTE_NAME, CLUSTER_COMMAND, COMMAND_ATTRIBUTE_UPDATED, + MOTION_EVENT, + OFF, + ON, UNKNOWN, VALUE, ZHA_SEND_EVENT, ZONE_STATE, - OFF, - ON, - MOTION_EVENT, ) _LOGGER = logging.getLogger(__name__) diff --git a/zhaquirks/centralite/3310S.py b/zhaquirks/centralite/3310S.py index 198002c50a..3f9c49002c 100755 --- a/zhaquirks/centralite/3310S.py +++ b/zhaquirks/centralite/3310S.py @@ -1,9 +1,9 @@ """Centralite 3310S implementation.""" from zigpy.profiles import zha -from zigpy.quirks import CustomDevice, CustomCluster +from zigpy.quirks import CustomCluster, CustomDevice +import zigpy.types as t from zigpy.zcl.clusters.general import Basic, Identify, Ota, PollControl from zigpy.zcl.clusters.measurement import TemperatureMeasurement -import zigpy.types as t from zhaquirks import PowerConfigurationCluster from zhaquirks.centralite import CENTRALITE diff --git a/zhaquirks/centralite/motion.py b/zhaquirks/centralite/motion.py index 04db0103da..f9e38b5eb9 100755 --- a/zhaquirks/centralite/motion.py +++ b/zhaquirks/centralite/motion.py @@ -6,6 +6,7 @@ from zigpy.zcl.clusters.security import IasZone from zhaquirks import PowerConfigurationCluster + from . import CENTRALITE from ..const import ( DEVICE_TYPE, diff --git a/zhaquirks/centralite/motionandtemp.py b/zhaquirks/centralite/motionandtemp.py index df1636f654..83f301917b 100644 --- a/zhaquirks/centralite/motionandtemp.py +++ b/zhaquirks/centralite/motionandtemp.py @@ -22,8 +22,8 @@ COMMAND, COMMAND_PRESS, DEVICE_TYPE, - ENDPOINTS, ENDPOINT_ID, + ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, OUTPUT_CLUSTERS, diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 2409f9438e..5edf832948 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -10,9 +10,9 @@ Basic, Identify, Ota, + PollControl, PowerConfiguration, Time, - PollControl, ) from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.hvac import Thermostat, UserInterface diff --git a/zhaquirks/develco/open_close.py b/zhaquirks/develco/open_close.py index bd11840ffa..6a2c7855ae 100644 --- a/zhaquirks/develco/open_close.py +++ b/zhaquirks/develco/open_close.py @@ -1,8 +1,8 @@ """Door/Windows sensors.""" from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t -from zigpy.quirks import CustomDevice, CustomCluster from zigpy.zcl.clusters import general, measurement, security from . import DEVELCO, DevelcoPowerConfiguration diff --git a/zhaquirks/echostar/bell.py b/zhaquirks/echostar/bell.py index 64809afdd1..0ecacab32c 100644 --- a/zhaquirks/echostar/bell.py +++ b/zhaquirks/echostar/bell.py @@ -16,8 +16,8 @@ BUTTON_2, CLUSTER_ID, COMMAND, - COMMAND_ON, COMMAND_OFF, + COMMAND_ON, DEVICE_TYPE, ENDPOINT_ID, ENDPOINTS, diff --git a/zhaquirks/ecolink/contact.py b/zhaquirks/ecolink/contact.py index 0d203e1e04..da619445e8 100644 --- a/zhaquirks/ecolink/contact.py +++ b/zhaquirks/ecolink/contact.py @@ -3,12 +3,10 @@ from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import Basic, Identify, Ota, PollControl - from zigpy.zcl.clusters.measurement import TemperatureMeasurement - from zigpy.zcl.clusters.security import IasZone -from zhaquirks import PowerConfigurationCluster +from zhaquirks import PowerConfigurationCluster from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, diff --git a/zhaquirks/edpwithus/redy_plug.py b/zhaquirks/edpwithus/redy_plug.py index fb27bcf709..c3d37e9cc1 100644 --- a/zhaquirks/edpwithus/redy_plug.py +++ b/zhaquirks/edpwithus/redy_plug.py @@ -4,11 +4,11 @@ from zigpy.zcl.clusters.general import ( Alarms, Basic, - Identify, Groups, - Scenes, + Identify, OnOff, Ota, + Scenes, Time, ) from zigpy.zcl.clusters.smartenergy import Metering diff --git a/zhaquirks/eurotronic/__init__.py b/zhaquirks/eurotronic/__init__.py index 2cdc2b19d9..383884416b 100644 --- a/zhaquirks/eurotronic/__init__.py +++ b/zhaquirks/eurotronic/__init__.py @@ -2,12 +2,11 @@ import logging -import zigpy.types as types from zigpy.quirks import CustomCluster +import zigpy.types as types from zigpy.zcl import foundation from zigpy.zcl.clusters.hvac import Thermostat - EUROTRONIC = "Eurotronic" THERMOSTAT_CHANNEL = "thermostat" diff --git a/zhaquirks/ikea/__init__.py b/zhaquirks/ikea/__init__.py index 181219975a..dd822b3792 100644 --- a/zhaquirks/ikea/__init__.py +++ b/zhaquirks/ikea/__init__.py @@ -1,8 +1,8 @@ """Ikea module.""" import logging -import zigpy.types as t from zigpy.quirks import CustomCluster +import zigpy.types as t from zigpy.zcl.clusters.general import Scenes from zigpy.zcl.clusters.lightlink import LightLink diff --git a/zhaquirks/ikea/fivebtnremotezha.py b/zhaquirks/ikea/fivebtnremotezha.py index f95678f351..4531d54bcd 100644 --- a/zhaquirks/ikea/fivebtnremotezha.py +++ b/zhaquirks/ikea/fivebtnremotezha.py @@ -15,6 +15,7 @@ from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.lightlink import LightLink +from . import IKEA, LightLinkCluster, ScenesCluster from .. import DoublingPowerConfigurationCluster from ..const import ( ARGS, @@ -43,7 +44,6 @@ SHORT_PRESS, TURN_ON, ) -from . import IKEA, LightLinkCluster, ScenesCluster IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 diff --git a/zhaquirks/ikea/motionzha.py b/zhaquirks/ikea/motionzha.py index 6122c38b64..34aca1fbe6 100644 --- a/zhaquirks/ikea/motionzha.py +++ b/zhaquirks/ikea/motionzha.py @@ -14,6 +14,7 @@ ) from zigpy.zcl.clusters.lightlink import LightLink +from . import IKEA, LightLinkCluster from .. import DoublingPowerConfigurationCluster from ..const import ( DEVICE_TYPE, @@ -23,7 +24,6 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from . import IKEA, LightLinkCluster IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 DIAGNOSTICS_CLUSTER_ID = 0x0B05 # decimal = 2821 diff --git a/zhaquirks/ikea/tradfriplug.py b/zhaquirks/ikea/tradfriplug.py index 5838b09981..66337eb3c4 100644 --- a/zhaquirks/ikea/tradfriplug.py +++ b/zhaquirks/ikea/tradfriplug.py @@ -3,18 +3,16 @@ from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, - Identify, Groups, - Scenes, - OnOff, + Identify, LevelControl, + OnOff, Ota, PollControl, + Scenes, ) - from zigpy.zcl.clusters.lightlink import LightLink - from . import IKEA IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 diff --git a/zhaquirks/iluminize/cct.py b/zhaquirks/iluminize/cct.py index bba0c40fd6..5950d1f520 100644 --- a/zhaquirks/iluminize/cct.py +++ b/zhaquirks/iluminize/cct.py @@ -1,28 +1,27 @@ """Quirk for iluminize CCT actor.""" from zigpy.profiles import zll -from zigpy.quirks import CustomDevice, CustomCluster +from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( - OnOff, Basic, + GreenPowerProxy, + Groups, Identify, LevelControl, - Scenes, - Groups, + OnOff, Ota, - GreenPowerProxy, + Scenes, ) from zigpy.zcl.clusters.homeautomation import Diagnostic - from zigpy.zcl.clusters.lighting import Color from zigpy.zcl.clusters.lightlink import LightLink from zhaquirks.const import ( + DEVICE_TYPE, ENDPOINTS, - OUTPUT_CLUSTERS, INPUT_CLUSTERS, - DEVICE_TYPE, - PROFILE_ID, MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, ) from zhaquirks.iluminize import ILUMINIZE diff --git a/zhaquirks/iluminize/dim.py b/zhaquirks/iluminize/dim.py index 5539ce57fb..c718cf98b7 100644 --- a/zhaquirks/iluminize/dim.py +++ b/zhaquirks/iluminize/dim.py @@ -1,28 +1,27 @@ """Quirk for iluminize DIM actor.""" from zigpy.profiles import zha -from zigpy.quirks import CustomDevice, CustomCluster +from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( - OnOff, Basic, + GreenPowerProxy, + Groups, Identify, LevelControl, - Scenes, - Groups, + OnOff, Ota, - GreenPowerProxy, + Scenes, ) from zigpy.zcl.clusters.homeautomation import Diagnostic - from zigpy.zcl.clusters.lighting import Color from zigpy.zcl.clusters.lightlink import LightLink from zhaquirks.const import ( + DEVICE_TYPE, ENDPOINTS, - OUTPUT_CLUSTERS, INPUT_CLUSTERS, - DEVICE_TYPE, - PROFILE_ID, MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, ) from zhaquirks.iluminize import ILUMINIZE diff --git a/zhaquirks/imagic/gs1117s.py b/zhaquirks/imagic/gs1117s.py index 9c905d4e7f..bf13d5a6e4 100644 --- a/zhaquirks/imagic/gs1117s.py +++ b/zhaquirks/imagic/gs1117s.py @@ -9,7 +9,7 @@ PowerConfiguration, ) from zigpy.zcl.clusters.homeautomation import Diagnostic -from zigpy.zcl.clusters.measurement import TemperatureMeasurement, RelativeHumidity +from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement from zigpy.zcl.clusters.security import IasZone from zhaquirks import PowerConfigurationCluster diff --git a/zhaquirks/kof/kof_mr101z.py b/zhaquirks/kof/kof_mr101z.py index 0ad29d3ae8..d05636effa 100644 --- a/zhaquirks/kof/kof_mr101z.py +++ b/zhaquirks/kof/kof_mr101z.py @@ -6,15 +6,15 @@ """ from zigpy.profiles import zha -from zigpy.quirks import CustomDevice, CustomCluster +from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( Basic, - Identify, Groups, - Scenes, - OnOff, + Identify, LevelControl, + OnOff, Ota, + Scenes, ) from zigpy.zcl.clusters.hvac import Fan diff --git a/zhaquirks/konke/__init__.py b/zhaquirks/konke/__init__.py index ca32a62cb0..bfe4ab984e 100644 --- a/zhaquirks/konke/__init__.py +++ b/zhaquirks/konke/__init__.py @@ -1,6 +1,6 @@ """Konke sensors.""" -from .. import MotionWithReset, OccupancyOnEvent, LocalDataCluster +from .. import LocalDataCluster, MotionWithReset, OccupancyOnEvent KONKE = "Konke" diff --git a/zhaquirks/konke/magnet.py b/zhaquirks/konke/magnet.py index 4d431cd1e0..3f0321baf8 100644 --- a/zhaquirks/konke/magnet.py +++ b/zhaquirks/konke/magnet.py @@ -2,8 +2,8 @@ from zigpy.profiles import zha from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.general import Basic, Identify, PowerConfiguration +from zigpy.zcl.clusters.security import IasZone from . import KONKE from .. import PowerConfigurationCluster diff --git a/zhaquirks/orvibo/motion.py b/zhaquirks/orvibo/motion.py index 4870ec5da2..8accf6bcdc 100644 --- a/zhaquirks/orvibo/motion.py +++ b/zhaquirks/orvibo/motion.py @@ -7,14 +7,14 @@ from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, + Groups, Identify, PowerConfiguration, - Groups, Scenes, ) from zigpy.zcl.clusters.security import IasZone -from . import ORVIBO_LATIN, OccupancyCluster, MotionCluster +from . import ORVIBO_LATIN, MotionCluster, OccupancyCluster from .. import Bus, PowerConfigurationCluster from ..const import ( DEVICE_TYPE, diff --git a/zhaquirks/osram/switchmini.py b/zhaquirks/osram/switchmini.py index a7f19ba2fe..7b12858aa7 100644 --- a/zhaquirks/osram/switchmini.py +++ b/zhaquirks/osram/switchmini.py @@ -8,35 +8,35 @@ LevelControl, OnOff, Ota, + PollControl, PowerConfiguration, Scenes, - PollControl, ) from zigpy.zcl.clusters.lighting import Color from zigpy.zcl.clusters.lightlink import LightLink from . import OSRAM from ..const import ( + BUTTON_1, + BUTTON_2, + BUTTON_3, + COMMAND, + COMMAND_MOVE, + COMMAND_MOVE_TO_LEVEL_ON_OFF, + COMMAND_OFF, + COMMAND_ON, + COMMAND_STEP_ON_OFF, + COMMAND_STOP, DEVICE_TYPE, + ENDPOINT_ID, ENDPOINTS, INPUT_CLUSTERS, + LONG_PRESS, + LONG_RELEASE, + MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, SHORT_PRESS, - COMMAND, - COMMAND_ON, - MODELS_INFO, - BUTTON_1, - ENDPOINT_ID, - COMMAND_STEP_ON_OFF, - COMMAND_STOP, - BUTTON_2, - BUTTON_3, - LONG_RELEASE, - LONG_PRESS, - COMMAND_MOVE_TO_LEVEL_ON_OFF, - COMMAND_OFF, - COMMAND_MOVE, ) OSRAM_CLUSTER = 0xFD00 diff --git a/zhaquirks/osram/tunablewhite.py b/zhaquirks/osram/tunablewhite.py index 9d7e391c62..1bb4f53542 100644 --- a/zhaquirks/osram/tunablewhite.py +++ b/zhaquirks/osram/tunablewhite.py @@ -1,6 +1,6 @@ """Osram tunable white device.""" from zigpy.profiles import zha -from zigpy.quirks import CustomDevice, CustomCluster +from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( Basic, Groups, @@ -13,14 +13,14 @@ from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.lighting import Color -from . import OsramLightCluster, OSRAM +from . import OSRAM, OsramLightCluster from ..const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, + MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, - MODELS_INFO, ) diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 36fd30bf93..7caf8ff9e4 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -1,9 +1,8 @@ """Module for Philips quirks implementations.""" +import asyncio import logging import time -import asyncio - from zigpy.quirks import CustomCluster import zigpy.types as t from zigpy.zcl.clusters.general import Basic, LevelControl, OnOff @@ -17,14 +16,14 @@ DIM_DOWN, DIM_UP, DOUBLE_PRESS, - TRIPLE_PRESS, - QUADRUPLE_PRESS, - QUINTUPLE_PRESS, LONG_PRESS, LONG_RELEASE, PRESS_TYPE, + QUADRUPLE_PRESS, + QUINTUPLE_PRESS, SHORT_PRESS, SHORT_RELEASE, + TRIPLE_PRESS, TURN_OFF, TURN_ON, ZHA_SEND_EVENT, diff --git a/zhaquirks/philips/llc011.py b/zhaquirks/philips/llc011.py index 29835bd84b..1e23f32b39 100644 --- a/zhaquirks/philips/llc011.py +++ b/zhaquirks/philips/llc011.py @@ -2,26 +2,25 @@ from zigpy.profiles import zll from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( - OnOff, Basic, + GreenPowerProxy, + Groups, Identify, LevelControl, - Scenes, - Groups, + OnOff, Ota, - GreenPowerProxy, + Scenes, ) - from zigpy.zcl.clusters.lighting import Color from zigpy.zcl.clusters.lightlink import LightLink from zhaquirks.const import ( + DEVICE_TYPE, ENDPOINTS, - OUTPUT_CLUSTERS, INPUT_CLUSTERS, - DEVICE_TYPE, - PROFILE_ID, MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, ) from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster diff --git a/zhaquirks/philips/lst002.py b/zhaquirks/philips/lst002.py index a528f98d18..5c23990562 100644 --- a/zhaquirks/philips/lst002.py +++ b/zhaquirks/philips/lst002.py @@ -2,26 +2,25 @@ from zigpy.profiles import zll from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( - OnOff, Basic, + GreenPowerProxy, + Groups, Identify, LevelControl, - Scenes, - Groups, + OnOff, Ota, - GreenPowerProxy, + Scenes, ) - from zigpy.zcl.clusters.lighting import Color from zigpy.zcl.clusters.lightlink import LightLink from zhaquirks.const import ( + DEVICE_TYPE, ENDPOINTS, - OUTPUT_CLUSTERS, INPUT_CLUSTERS, - DEVICE_TYPE, - PROFILE_ID, MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, ) from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster diff --git a/zhaquirks/philips/rom001.py b/zhaquirks/philips/rom001.py index ad34786796..b5e24fb14d 100644 --- a/zhaquirks/philips/rom001.py +++ b/zhaquirks/philips/rom001.py @@ -14,17 +14,17 @@ from zigpy.zcl.clusters.lightlink import LightLink from ..const import ( + COMMAND, + COMMAND_OFF_WITH_EFFECT, + COMMAND_ON, DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, OUTPUT_CLUSTERS, PROFILE_ID, SHORT_PRESS, - TURN_ON, TURN_OFF, - COMMAND, - COMMAND_ON, - COMMAND_OFF_WITH_EFFECT, + TURN_ON, ) DEVICE_SPECIFIC_UNKNOWN = 64512 diff --git a/zhaquirks/philips/zhaextendedcolorlight.py b/zhaquirks/philips/zhaextendedcolorlight.py index 4a70d30b4c..88df213fbc 100644 --- a/zhaquirks/philips/zhaextendedcolorlight.py +++ b/zhaquirks/philips/zhaextendedcolorlight.py @@ -2,26 +2,25 @@ from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( - OnOff, Basic, + GreenPowerProxy, + Groups, Identify, LevelControl, - Scenes, - Groups, + OnOff, Ota, - GreenPowerProxy, + Scenes, ) - from zigpy.zcl.clusters.lighting import Color from zigpy.zcl.clusters.lightlink import LightLink from zhaquirks.const import ( + DEVICE_TYPE, ENDPOINTS, - OUTPUT_CLUSTERS, INPUT_CLUSTERS, - DEVICE_TYPE, - PROFILE_ID, MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, ) from zhaquirks.philips import ( PHILIPS, diff --git a/zhaquirks/philips/zlldimmablelight.py b/zhaquirks/philips/zlldimmablelight.py index 33fc153fac..18fdd9ff56 100644 --- a/zhaquirks/philips/zlldimmablelight.py +++ b/zhaquirks/philips/zlldimmablelight.py @@ -2,27 +2,25 @@ from zigpy.profiles import zll from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( - OnOff, Basic, + GreenPowerProxy, + Groups, Identify, LevelControl, - Scenes, - Groups, + OnOff, Ota, - GreenPowerProxy, + Scenes, ) - from zigpy.zcl.clusters.lightlink import LightLink from zhaquirks.const import ( + DEVICE_TYPE, ENDPOINTS, - OUTPUT_CLUSTERS, INPUT_CLUSTERS, - DEVICE_TYPE, - PROFILE_ID, MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, ) - from zhaquirks.philips import PHILIPS, PhilipsLevelControlCluster, PhilipsOnOffCluster diff --git a/zhaquirks/philips/zllextendedcolorlight.py b/zhaquirks/philips/zllextendedcolorlight.py index 0c04a3b057..d18168fb77 100644 --- a/zhaquirks/philips/zllextendedcolorlight.py +++ b/zhaquirks/philips/zllextendedcolorlight.py @@ -2,26 +2,25 @@ from zigpy.profiles import zll from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( - OnOff, Basic, + GreenPowerProxy, + Groups, Identify, LevelControl, - Scenes, - Groups, + OnOff, Ota, - GreenPowerProxy, + Scenes, ) - from zigpy.zcl.clusters.lighting import Color from zigpy.zcl.clusters.lightlink import LightLink from zhaquirks.const import ( + DEVICE_TYPE, ENDPOINTS, - OUTPUT_CLUSTERS, INPUT_CLUSTERS, - DEVICE_TYPE, - PROFILE_ID, MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, ) from zhaquirks.philips import ( PHILIPS, diff --git a/zhaquirks/samjin/multi2.py b/zhaquirks/samjin/multi2.py index c07d93c70f..c1cbcd21dd 100644 --- a/zhaquirks/samjin/multi2.py +++ b/zhaquirks/samjin/multi2.py @@ -10,6 +10,7 @@ from zigpy.zcl.clusters.measurement import TemperatureMeasurement from zigpy.zcl.clusters.security import IasZone +from . import SAMJIN from ..const import ( DEVICE_TYPE, ENDPOINTS, @@ -18,7 +19,6 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from . import SAMJIN from ..smartthings import SmartThingsAccelCluster DIAGNOSTICS_CLUSTER_ID = 0x0B05 # decimal = 2821 diff --git a/zhaquirks/sercomm/szwtd02n.py b/zhaquirks/sercomm/szwtd02n.py index 1962e37a0a..dfec9f2094 100644 --- a/zhaquirks/sercomm/szwtd02n.py +++ b/zhaquirks/sercomm/szwtd02n.py @@ -3,8 +3,8 @@ from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import Basic, Identify, Ota, PollControl -from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.zcl.clusters.security import IasZone from zhaquirks import PowerConfigurationCluster from zhaquirks.const import ( diff --git a/zhaquirks/sinope/light.py b/zhaquirks/sinope/light.py index 67aae6987b..25a4b1c497 100644 --- a/zhaquirks/sinope/light.py +++ b/zhaquirks/sinope/light.py @@ -11,13 +11,14 @@ DeviceTemperature, Groups, Identify, - OnOff, LevelControl, + OnOff, Ota, Scenes, ) from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.smartenergy import Metering + from . import SINOPE from ..const import ( DEVICE_TYPE, diff --git a/zhaquirks/smartthings/__init__.py b/zhaquirks/smartthings/__init__.py index f0e702ce81..b08ca3001c 100644 --- a/zhaquirks/smartthings/__init__.py +++ b/zhaquirks/smartthings/__init__.py @@ -1,7 +1,7 @@ """Module for smartthings quirks.""" -import zigpy.types as t from zigpy.quirks import CustomCluster +import zigpy.types as t from zigpy.zcl.clusters.security import IasZone SMART_THINGS = "SmartThings" diff --git a/zhaquirks/smartthings/multi.py b/zhaquirks/smartthings/multi.py index b66cca827e..37cc77fa75 100644 --- a/zhaquirks/smartthings/multi.py +++ b/zhaquirks/smartthings/multi.py @@ -4,13 +4,14 @@ from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, - PowerConfiguration, Identify, - PollControl, Ota, + PollControl, + PowerConfiguration, ) -from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.measurement import TemperatureMeasurement +from zigpy.zcl.clusters.security import IasZone + from . import SmartThingsAccelCluster diff --git a/zhaquirks/smartthings/multiv4.py b/zhaquirks/smartthings/multiv4.py index 2b7d469c63..3b40869a09 100755 --- a/zhaquirks/smartthings/multiv4.py +++ b/zhaquirks/smartthings/multiv4.py @@ -6,6 +6,7 @@ from zigpy.zcl.clusters.security import IasZone from zhaquirks import PowerConfigurationCluster + from . import SMART_THINGS from ..centralite import CentraLiteAccelCluster from ..const import ( diff --git a/zhaquirks/smartthings/pgc313.py b/zhaquirks/smartthings/pgc313.py index 863a085c8c..636186369a 100755 --- a/zhaquirks/smartthings/pgc313.py +++ b/zhaquirks/smartthings/pgc313.py @@ -3,6 +3,7 @@ from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import Basic, Ota +from . import SMART_THINGS, SmartThingsIasZone from ..const import ( DEVICE_TYPE, ENDPOINTS, @@ -11,7 +12,6 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from . import SMART_THINGS, SmartThingsIasZone SMARTSENSE_MULTI_DEVICE_TYPE = 0x0139 # decimal = 313 diff --git a/zhaquirks/smartthings/pgc314.py b/zhaquirks/smartthings/pgc314.py index 7c1f194e50..dd025b4580 100644 --- a/zhaquirks/smartthings/pgc314.py +++ b/zhaquirks/smartthings/pgc314.py @@ -3,6 +3,7 @@ from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import Basic, Ota +from . import SMART_THINGS, SmartThingsIasZone from ..const import ( DEVICE_TYPE, ENDPOINTS, @@ -11,7 +12,6 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from . import SMART_THINGS, SmartThingsIasZone SMARTSENSE_MOTION_DEVICE_TYPE = 0x013A # decimal = 314 diff --git a/zhaquirks/terncy/__init__.py b/zhaquirks/terncy/__init__.py index f43ab1442f..606fbd6185 100644 --- a/zhaquirks/terncy/__init__.py +++ b/zhaquirks/terncy/__init__.py @@ -7,7 +7,7 @@ TemperatureMeasurement, ) -from .. import LocalDataCluster, _Motion, OccupancyOnEvent, OCCUPANCY_EVENT +from .. import OCCUPANCY_EVENT, LocalDataCluster, OccupancyOnEvent, _Motion from ..const import ( BUTTON, CLUSTER_COMMAND, diff --git a/zhaquirks/terncy/pp01.py b/zhaquirks/terncy/pp01.py index f5547d8321..f97125ec73 100755 --- a/zhaquirks/terncy/pp01.py +++ b/zhaquirks/terncy/pp01.py @@ -9,21 +9,21 @@ PowerConfiguration, ) from zigpy.zcl.clusters.measurement import ( - TemperatureMeasurement, IlluminanceMeasurement, OccupancySensing, + TemperatureMeasurement, ) from zhaquirks import DoublingPowerConfigurationCluster from . import ( + BUTTON_TRIGGERS, IlluminanceMeasurementCluster, - TemperatureMeasurementCluster, - TerncyRawCluster, - OccupancyCluster, MotionClusterLeft, MotionClusterRight, - BUTTON_TRIGGERS, + OccupancyCluster, + TemperatureMeasurementCluster, + TerncyRawCluster, ) from .. import Bus from ..const import ( diff --git a/zhaquirks/terncy/sd01.py b/zhaquirks/terncy/sd01.py index 0b0dc22ff5..550f4d7b76 100644 --- a/zhaquirks/terncy/sd01.py +++ b/zhaquirks/terncy/sd01.py @@ -4,15 +4,15 @@ from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, - PowerConfiguration, Identify, - PollControl, Ota, + PollControl, + PowerConfiguration, ) from zhaquirks import DoublingPowerConfigurationCluster -from . import TerncyRawCluster, BUTTON_TRIGGERS, KNOB_TRIGGERS +from . import BUTTON_TRIGGERS, KNOB_TRIGGERS, TerncyRawCluster from ..const import ( DEVICE_TYPE, ENDPOINTS, diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py index 29bd65dd5d..7a2980e1b0 100644 --- a/zhaquirks/tuya/__init__.py +++ b/zhaquirks/tuya/__init__.py @@ -1,16 +1,14 @@ """Tuya devices.""" import logging - from typing import Optional, Tuple, Union from zigpy.quirks import CustomCluster, CustomDevice -from zigpy.zcl.clusters.general import OnOff -from zigpy.zcl import foundation import zigpy.types as t +from zigpy.zcl import foundation +from zigpy.zcl.clusters.general import OnOff from .. import Bus - TUYA_CLUSTER_ID = 0xEF00 TUYA_SET_DATA = 0x0000 TUYA_GET_DATA = 0x0001 diff --git a/zhaquirks/tuya/motion.py b/zhaquirks/tuya/motion.py index 7043b42a74..2b5883e650 100755 --- a/zhaquirks/tuya/motion.py +++ b/zhaquirks/tuya/motion.py @@ -7,18 +7,17 @@ from zigpy.zcl.clusters.general import Basic, Identify, Ota from zigpy.zcl.clusters.security import IasZone +from . import TuyaManufCluster +from .. import Bus, LocalDataCluster, MotionOnEvent from ..const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, + MOTION_EVENT, OUTPUT_CLUSTERS, PROFILE_ID, - MOTION_EVENT, ) -from .. import MotionOnEvent, LocalDataCluster, Bus -from . import TuyaManufCluster - ZONE_TYPE = 0x0001 diff --git a/zhaquirks/tuya/singleswitch.py b/zhaquirks/tuya/singleswitch.py index 0c08d491d6..d6f196ad10 100644 --- a/zhaquirks/tuya/singleswitch.py +++ b/zhaquirks/tuya/singleswitch.py @@ -1,6 +1,7 @@ """Tuya based button sensor.""" from zigpy.profiles import zha -from zigpy.zcl.clusters.general import Basic, Groups, Scenes, Time, Ota +from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time + from ..const import ( DEVICE_TYPE, ENDPOINTS, @@ -9,7 +10,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from ..tuya import TuyaManufCluster, TuyaManufacturerClusterOnOff, TuyaOnOff, TuyaSwitch +from ..tuya import TuyaManufacturerClusterOnOff, TuyaManufCluster, TuyaOnOff, TuyaSwitch class TuyaSingleSwitch(TuyaSwitch): diff --git a/zhaquirks/tuya/siren.py b/zhaquirks/tuya/siren.py index 5df8613550..978a2ee488 100644 --- a/zhaquirks/tuya/siren.py +++ b/zhaquirks/tuya/siren.py @@ -1,15 +1,15 @@ """Map from manufacturer to standard clusters for the NEO Siren device.""" import logging - from typing import Optional, Union from zigpy.profiles import zha from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.general import Basic, Identify, Ota, OnOff -from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement -from zigpy.zcl import foundation import zigpy.types as t +from zigpy.zcl import foundation +from zigpy.zcl.clusters.general import Basic, Identify, OnOff, Ota +from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement +from . import TuyaManufClusterAttributes from .. import Bus, LocalDataCluster from ..const import ( DEVICE_TYPE, @@ -20,8 +20,6 @@ PROFILE_ID, ) -from . import TuyaManufClusterAttributes - TUYA_ALARM_ATTR = 0x0168 # [0]/[1] Alarm! TUYA_TEMP_ALARM_ATTR = 0x0171 # [0]/[1] Disable/Enable alarm by temperature TUYA_HUMID_ALARM_ATTR = 0x0172 # [0]/[1] Disable/Enable alarm by humidity diff --git a/zhaquirks/visonic/mct340e.py b/zhaquirks/visonic/mct340e.py index e8fad64841..49794c24b6 100755 --- a/zhaquirks/visonic/mct340e.py +++ b/zhaquirks/visonic/mct340e.py @@ -6,6 +6,7 @@ from zigpy.zcl.clusters.general import Basic, Identify, Ota, PollControl from zigpy.zcl.clusters.measurement import TemperatureMeasurement from zigpy.zcl.clusters.security import IasZone + from zhaquirks import PowerConfigurationCluster from ..const import ( diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index aa517ca334..7d17db7210 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -4,7 +4,6 @@ import math from typing import Optional, Union -import zigpy.zcl.foundation as foundation from zigpy import types as t from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice @@ -16,6 +15,7 @@ RelativeHumidity, TemperatureMeasurement, ) +import zigpy.zcl.foundation as foundation from .. import Bus, LocalDataCluster, MotionOnEvent, OccupancyWithReset from ..const import ( diff --git a/zhaquirks/xiaomi/aqara/ctrl_ln.py b/zhaquirks/xiaomi/aqara/ctrl_ln.py index ed92cf4119..9178a4d083 100644 --- a/zhaquirks/xiaomi/aqara/ctrl_ln.py +++ b/zhaquirks/xiaomi/aqara/ctrl_ln.py @@ -3,18 +3,19 @@ from zigpy.zcl.clusters.general import ( AnalogInput, Basic, + BinaryOutput, + DeviceTemperature, Groups, Identify, MultistateInput, OnOff, Ota, Scenes, - DeviceTemperature, Time, - BinaryOutput, ) from zhaquirks import Bus + from .. import ( LUMI, AnalogInputCluster, diff --git a/zhaquirks/xiaomi/aqara/ctrl_neutral.py b/zhaquirks/xiaomi/aqara/ctrl_neutral.py index c0b7cca342..4bd0fc4e57 100644 --- a/zhaquirks/xiaomi/aqara/ctrl_neutral.py +++ b/zhaquirks/xiaomi/aqara/ctrl_neutral.py @@ -5,15 +5,15 @@ from zigpy.zcl.clusters.general import ( AnalogInput, Basic, + BinaryOutput, + DeviceTemperature, Groups, Identify, MultistateInput, OnOff, Ota, Scenes, - DeviceTemperature, Time, - BinaryOutput, ) from .. import ( diff --git a/zhaquirks/xiaomi/aqara/cube.py b/zhaquirks/xiaomi/aqara/cube.py index 55497214a4..c21f9e2216 100644 --- a/zhaquirks/xiaomi/aqara/cube.py +++ b/zhaquirks/xiaomi/aqara/cube.py @@ -11,6 +11,7 @@ Scenes, ) +from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice from ... import CustomCluster from ...const import ( ARGS, @@ -27,7 +28,6 @@ VALUE, ZHA_SEND_EVENT, ) -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice ACTIVATED_FACE = "activated_face" DESCRIPTION = "description" diff --git a/zhaquirks/xiaomi/aqara/plug.py b/zhaquirks/xiaomi/aqara/plug.py index 5e10d79ee0..fdb5ef5d73 100644 --- a/zhaquirks/xiaomi/aqara/plug.py +++ b/zhaquirks/xiaomi/aqara/plug.py @@ -16,13 +16,6 @@ Time, ) -from zhaquirks.xiaomi import ( - LUMI, - AnalogInputCluster, - BasicCluster, - ElectricalMeasurementCluster, - XiaomiCustomDevice, -) from zhaquirks import Bus from zhaquirks.const import ( DEVICE_TYPE, @@ -33,6 +26,13 @@ PROFILE_ID, SKIP_CONFIGURATION, ) +from zhaquirks.xiaomi import ( + LUMI, + AnalogInputCluster, + BasicCluster, + ElectricalMeasurementCluster, + XiaomiCustomDevice, +) _LOGGER = logging.getLogger(__name__) diff --git a/zhaquirks/xiaomi/aqara/wleak_aq1.py b/zhaquirks/xiaomi/aqara/wleak_aq1.py index c8f67fe673..fe514105e1 100644 --- a/zhaquirks/xiaomi/aqara/wleak_aq1.py +++ b/zhaquirks/xiaomi/aqara/wleak_aq1.py @@ -4,6 +4,7 @@ from zigpy.zcl.clusters.general import Identify, Ota from zigpy.zcl.clusters.security import IasZone +from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice from ...const import ( DEVICE_TYPE, ENDPOINTS, @@ -13,7 +14,6 @@ PROFILE_ID, SKIP_CONFIGURATION, ) -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice class CustomIasZone(CustomCluster, IasZone): diff --git a/zhaquirks/xiaomi/mija/sensor_magnet.py b/zhaquirks/xiaomi/mija/sensor_magnet.py index 2b8b6b31ce..77c0d09f0d 100644 --- a/zhaquirks/xiaomi/mija/sensor_magnet.py +++ b/zhaquirks/xiaomi/mija/sensor_magnet.py @@ -11,6 +11,7 @@ Scenes, ) +from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice from ...const import ( DEVICE_TYPE, ENDPOINTS, @@ -20,7 +21,6 @@ PROFILE_ID, SKIP_CONFIGURATION, ) -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice OPEN_CLOSE_DEVICE_TYPE = 0x5F01 XIAOMI_CLUSTER_ID = 0xFFFF From 5c799f15dc37c44617c9f62f5ceedd80562ce169 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 8 Nov 2020 07:34:50 -0500 Subject: [PATCH 095/113] Add Cwntralite contact sensor a (#569) --- zhaquirks/centralite/ias.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhaquirks/centralite/ias.py b/zhaquirks/centralite/ias.py index ef70d107f7..f074a59144 100755 --- a/zhaquirks/centralite/ias.py +++ b/zhaquirks/centralite/ias.py @@ -38,6 +38,7 @@ class CentraLiteIASSensor(CustomDevice): (CENTRALITE, "3315-Seu"), (CENTRALITE, "3315"), (CENTRALITE, "3320-L"), + (CENTRALITE, "Contact Sensor-A"), ], ENDPOINTS: { 1: { From d835a4300842464302415e2febb0e328bf62ba1c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 8 Nov 2020 08:34:47 -0500 Subject: [PATCH 096/113] Reorganize readme (#570) * Reorganize readme * update badges --- .markdownlint.json | 6 + CONTRIBUTING.md | 410 ----------------------------------- README.md | 521 +++++++++++++++++++++++++++++++++++---------- 3 files changed, 417 insertions(+), 520 deletions(-) create mode 100644 .markdownlint.json delete mode 100644 CONTRIBUTING.md diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000000..dbbb353d20 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,6 @@ +{ + "default": true, + "single-title": false, + "no-inline-html": false, + "line-length": false +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 1f84d78be2..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,410 +0,0 @@ -# Primer - -ZHA device handlers and it's provided Quirks allow Zigpy, ZHA and Home Assistant to work with non standard Zigbee devices. If you are reading this you may have a device that isn't working as expected. This can be the case for a number of reasons but in this guide we will cover the cases where functionality is provided by a device in a non specification compliant manner by the device manufacturer. - -## What are these specifications? - -[Zigbee Specification](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf) - -[Zigbee Cluster Library](https://zigbeealliance.org/wp-content/uploads/2019/12/07-5123-06-zigbee-cluster-library-specification.pdf) - -[Zigbee Base Device Specification](https://zigbeealliance.org/wp-content/uploads/zip/zigbee-base-device-behavior-bdb-v1-0.zip) - -[Zigbee Primer](https://docs.smartthings.com/en/latest/device-type-developers-guide/zigbee-primer.html) - -## What is a device in human terms? - -A device is a physical object that you want to join to a Zigbee network: a light bulb, a switch, a sensor etc. The host application, in this case Zigpy, needs to understand how to interact with the device so there are standards that define how the application and devices can communicate. The device's functionality is described by several **descriptors** while the device itself contains **endpoints** and **endpoints** contain **clusters**. There are two types of clusters an endpoint contains: -- **in_clusters** - are "Server" clusters in ZCL terms. These clusters control the device, e.g. a smart plug or light bulb would have an `on_off` server cluster. **in_clusters** are also the ones which also send attribute reports and/or you can read an attribute from a **in_cluster**. -- **out_clusters** - are "Client" clusters. These clusters control some other device, as "Client" cluster sends commands to "Server" cluster. For example an On/Off remote would have an `on_off` client cluster and will generate cluster commands and send those to some other device. -Zigpy needs to understand all these elements in order to correctly work with the device. - -### Endpoints - -Endpoints are essentially groupings of functionality. For example, a typical Zigbee light bulb will have a single endpoint for the light. A multi-gang wall switch may have an endpoint for each individual switch so they can all be controlled separately. Each endpoint has several functions represented by clusters. - -### Clusters - -Clusters are objects that contain the information (attributes and commands) for individual functions. There is the ability to turn the switch on and off, maybe there is energy monitoring, maybe there is the ability to add each switch to an individual group or a scene, etc. Each of these functions belong to a cluster. - -### Descriptors - -For the purposes of Zigpy and Quirks we will focus on two descriptors: **Node Descriptor** and **Simple Descriptor**. - - -#### Node Descriptor - -A node descriptor explains some basic device attributes to Zigpy. The manufacturer code and the power type are the ones that we generally care about. In most cases you won't have to worry about this but it is good to know why it is there in case you come across it while looking at an existing quirk. Here is an example: -`` - -#### Simple Descriptor - -A simple descriptor is a description of a Zigbee device endpoint and is responsible for explaining the endpoint's functionality. It contains a profile id, the device type, and collections of clusters. The profile id tells the application what set of Zigbee rules to use. The most common profile will be 260 (0x0104) for the Home Automation profile. The device type tells the application what logical type of device this is ex: on off light, color light, etc. The clusters explain to the application what types of functionality exist on the endpoint. Here is an example: -`` - -## What the heck is a quirk? - -In human terms you can think of a quirk like google translate. I know it's a weird comparison but lets dig in a bit. You may only speak one language but there is an interesting article written in another language that you really want to read. Google translate takes the original article and displays it in a format (language) that you understand. A quirk is a file that translates device functionality from the format that the manufacturer chose to implement it in to a format that Zigpy and in turn ZHA understand. The main purpose of a quirk is to serve as a translator. A quirk is comprised of several parts: - -- Signature - To identify and apply the correct quirk -- Replacement - To allow Zigpy and ZHA to correctly work with the device -- device_automation_triggers - To let the Home Assistant Device Automation engine and users interact with the device - -### Signature - -The signature on a quirk identifies the device as the manufacturer implemented it. You can think of it as a fingerprint or the dna of the device. The signature is what we use to identify the device. If any part of the signature doesn't match what the device returns during discovery the quirk will not match and as a result it will not be applied. The signature is made up of several parts: - -- `models_info` -- `endpoints` - -Models info tells the application which devices should use this particular quirk. Endpoints are the simple descriptors that we spoke about earlier exactly as they are on the device. `endpoints` is a dict where the key is the id of the endpoint and the value is an object with the following properties: `profile_id`, `device_type`, `input_clusters` and `output_clusters`. Creating the signature element is generally just a job of transcribing what the device gives us. Here is an example: - -```python -signature = { - MODELS_INFO: [(LUMI, "lumi.plug.maus01")], - ENDPOINTS: { - # - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.SMART_PLUG, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - DeviceTemperature.cluster_id, - Groups.cluster_id, - Identify.cluster_id, - OnOff.cluster_id, - Scenes.cluster_id, - BinaryOutput.cluster_id, - Time.cluster_id, - ElectricalMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], - }, - }, -} -``` - -### Replacement - -The replacement on a quirk is what we want the device to be. Remember, we said that quirks were like Google translate... you can think of the replacement like the output from Google translate. The replacement dict is what will actually be used by Zigpy and ZHA to interact with the device. The structure of `replacement` is the same as signature with 2 key differences: `models_info` is generally omitted and there is an extra element `skip_configuration` that instructs the application to skip configuration if necessary. Some manufacturers have not implemented the specifications correctly and the devices come pre-configured and therefore the configuration calls fail (non Zigbee 3.0 Xiaomi devices for instance). Usually, you should not add `skip_configuration`. - -Here is an example: - -```python -replacement = { - SKIP_CONFIGURATION: True, - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.SMART_PLUG, - INPUT_CLUSTERS: [ - BasicCluster, - PowerConfiguration.cluster_id, - DeviceTemperature.cluster_id, - Groups.cluster_id, - Identify.cluster_id, - OnOff.cluster_id, - Scenes.cluster_id, - BinaryOutput.cluster_id, - Time.cluster_id, - ElectricalMeasurementCluster, - ], - OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], - }, - }, -} -``` - -### device_automation_triggers - -Device automation triggers are essentially representations of the events that the devices fire in HA. They allow users to use actions in the UI instead of using the raw events. - -# Building a quirk - -Now that we got that out of the way we can focus on the task at hand: make our devices work the way they should with Zigpy and ZHA. Because the device doesn't work correctly out of the box we have to write a quirk for it. First lets look at what the quirk looks like when complete: - -```python -class Plug(XiaomiCustomDevice): - """lumi.plug.maus01 plug.""" - - def __init__(self, *args, **kwargs): - """Init.""" - self.voltage_bus = Bus() - self.consumption_bus = Bus() - self.power_bus = Bus() - super().__init__(*args, **kwargs) - - signature = { - MODELS_INFO: [(LUMI, "lumi.plug.maus01")], - ENDPOINTS: { - # - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.SMART_PLUG, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - DeviceTemperature.cluster_id, - Groups.cluster_id, - Identify.cluster_id, - OnOff.cluster_id, - Scenes.cluster_id, - BinaryOutput.cluster_id, - Time.cluster_id, - ElectricalMeasurement.cluster_id, - ], - OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], - }, - # - 2: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, - INPUT_CLUSTERS: [AnalogInput.cluster_id], - OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], - }, - # - 3: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, - INPUT_CLUSTERS: [AnalogInput.cluster_id], - OUTPUT_CLUSTERS: [AnalogInput.cluster_id], - }, - # - 100: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [BinaryInput.cluster_id], - OUTPUT_CLUSTERS: [BinaryInput.cluster_id, Groups.cluster_id], - }, - }, - } - replacement = { - SKIP_CONFIGURATION: True, - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.SMART_PLUG, - INPUT_CLUSTERS: [ - BasicCluster, - PowerConfiguration.cluster_id, - DeviceTemperature.cluster_id, - Groups.cluster_id, - Identify.cluster_id, - OnOff.cluster_id, - Scenes.cluster_id, - BinaryOutput.cluster_id, - Time.cluster_id, - ElectricalMeasurementCluster, - ], - OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], - }, - 2: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, - INPUT_CLUSTERS: [AnalogInputCluster], - OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], - }, - 3: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, - INPUT_CLUSTERS: [AnalogInput.cluster_id], - OUTPUT_CLUSTERS: [AnalogInput.cluster_id], - }, - 100: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [BinaryInput.cluster_id], - OUTPUT_CLUSTERS: [BinaryInput.cluster_id, Groups.cluster_id], - }, - }, - } -``` - -This quirk is for the US version of the Xiaomi plug. Xiaomi is notorious for not following the Zigbee specifications and most of their non Zigbee 3.0 devices need a quirk to function correctly. In this case we are correcting the `ElectricalMeasurement` cluster readings. Xiaomi decided to report the values for this cluster on the `AnalogInput` cluster instead. To fix this we will create a custom cluster to replace the `AnalogInput` and `ElectricalMeasurement` clusters. We will take the values that are reported on the `AnalogInput` cluster and publish them to the `ElectricalMeasurement` cluster. Doing this allows the device to work as if Xiaomi had implemented this in the first place. This is the act of translating that was mentioned in the Google Translate analogy above. - -First things first. All device definitions in quirks must extend `CustomDevice` or a derivative of it and all clusters that you define must extend `CustomCluster` or a derivative of it. If you want to send messages between `CustomCluster` definitions as we do here you need to create channels for the communication to flow through. We do this by adding instances of `Bus` on our `CustomDevice` implementation. `Bus` is a utility class used specifically for this purpose and adding it to the device implementation ensures that all clusters that you define will have access to the `Bus` so that they can communicate with eachother. - -```python -class Plug(XiaomiCustomDevice): - """lumi.plug.maus01 plug.""" - - def __init__(self, *args, **kwargs): - """Init.""" - self.voltage_bus = Bus() - self.consumption_bus = Bus() - self.power_bus = Bus() - super().__init__(*args, **kwargs) -``` - -You can see that we have extended `XiaomiCustomDevice` which is a derivative of `CustomDevice` shared by Xiaomi devices. You can also see that we have added some instances of `Bus` so that we can pass messages between `CustomCluster` definitions. To be clear, this is not always necessary. Quirks can be used to change formats of data on an existing cluster, to add manufacturer specific attributes or commands to clusters etc. In these instances you just need to create a derivative of `CustomCluster` and add your logic. This is more of an advanced example to illustrate what is possible. - -Here are the custom cluster definitions: - -```python -class AnalogInputCluster(CustomCluster, AnalogInput): - """Analog input cluster, only used to relay power consumtion information to ElectricalMeasurementCluster.""" - - cluster_id = AnalogInput.cluster_id - - def __init__(self, *args, **kwargs): - """Init.""" - self._current_state = {} - super().__init__(*args, **kwargs) - - def _update_attribute(self, attrid, value): - super()._update_attribute(attrid, value) - if value is not None and value >= 0: - self.endpoint.device.power_bus.listener_event(POWER_REPORTED, value) - - -class ElectricalMeasurementCluster(LocalDataCluster, ElectricalMeasurement): - """Electrical measurement cluster to receive reports that are sent to the basic cluster.""" - - cluster_id = ElectricalMeasurement.cluster_id - POWER_ID = 0x050B - VOLTAGE_ID = 0x0500 - CONSUMPTION_ID = 0x0304 - - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self.endpoint.device.voltage_bus.add_listener(self) - self.endpoint.device.consumption_bus.add_listener(self) - self.endpoint.device.power_bus.add_listener(self) - - def power_reported(self, value): - """Power reported.""" - self._update_attribute(self.POWER_ID, value) - - def voltage_reported(self, value): - """Voltage reported.""" - self._update_attribute(self.VOLTAGE_ID, value) - - def consumption_reported(self, value): - """Consumption reported.""" - self._update_attribute(self.CONSUMPTION_ID, value) -``` - -In the `AnalogInput` cluster we override the `_update_attribute` method so that we can access the data that the cluster receives when the device sends a report and we send the data via an event on a bus to the `ElectricalMeasurement` cluster. This is the line that does the heavy lifting: - -`self.endpoint.device.power_bus.listener_event(POWER_REPORTED, value)` - -Then in the `ElectricalMeasurement` cluster we need to subscribe to these events and handle them. This is how we subscribe to our custom events: - -`self.endpoint.device.power_bus.add_listener(self)` - -and this method (the method name must match the event name that you publish EXACTLY): - -```python -def power_reported(self, value): - """Power reported.""" - self._update_attribute(self.POWER_ID, value) -``` - -receives the event and handles updating the attribute on the correct zigbee cluster. As you can see there really isn't much here that needs to be done to accomplish our goal. - -Once we have created our `CustomCluster` implementations we have to tell the `CustomDevice` implementation to use them. We do this in the `replacement` dict in the quirk definition. Start by copying the `signature` dict and remove the `models_info` from it. Then we replace the cluster ids that we want to override with the names of our `CustomCluster` implementations that we have created. The result looks like this: - -```python -replacement = { - SKIP_CONFIGURATION: True, - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.SMART_PLUG, - INPUT_CLUSTERS: [ - BasicCluster, - PowerConfiguration.cluster_id, - DeviceTemperature.cluster_id, - Groups.cluster_id, - Identify.cluster_id, - OnOff.cluster_id, - Scenes.cluster_id, - BinaryOutput.cluster_id, - Time.cluster_id, - ElectricalMeasurementCluster, - ], - OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], - }, - 2: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, - INPUT_CLUSTERS: [AnalogInputCluster], - OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], - }, - 3: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, - INPUT_CLUSTERS: [AnalogInput.cluster_id], - OUTPUT_CLUSTERS: [AnalogInput.cluster_id], - }, - 100: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [BinaryInput.cluster_id], - OUTPUT_CLUSTERS: [BinaryInput.cluster_id, Groups.cluster_id], - }, -``` - -You can see that we have replaced `ElectricalMeasurement.cluster_id` from endpoint 1 in the `signature` dict with the name of our cluster that we created: `ElectricalMeasurementCluster` and on endpoint 2 we replaced `AnalogInput.cluster_id` with the implementation we created for that: `AnalogInputCluster`. This instructs Zigpy to use these `CustomCluster` derivatives instead of the normal cluster definitions for these clusters and this is why this part of the quirk is called `replacement`. - -Now lets put this all together. If you examine the device definition above you will see that we have defined our custom device, we defined the `signature` dict where we transcribed the `SimpleDescriptor` output we obtained when the device joined the network and we defined the `replacement` dict where we swapped the cluster ids for the culsters that we wanted to replace with the `CustomCluster` implementations that we created. - -# Contribution Guidelines - -- All code is formatted with black. The check format script that runs in CI will ensure that code meets this requirement and that it is correctly formatted with black. Instructions for installing black in many editors can be found here: - -- Capture the SimpleDescriptor log entries for each endpoint on the device. These can be found in the HA logs after joining a device and they look like this: ``. This information can also be obtained from the zigbee.db if you want to take the time to query the tables and reconstitute the log entry. I find it easier to just remove and rejoin the device. ZHA entity ids are stable for the most part so it _shouldn't_ disrupt anything you have configured. These need to match what the device reports EXACTLY or zigpy will not match them when a device joins and the handler will not be used for the device. You can also obtain this information from the device screen in HA for the device. The `Zigbee Device Signature` button will launch a dialog that contains all of the information necessary to create quirks. - -- All custom device definitions must extend `CustomDevice` or a derivative of it - -- All custom cluster definitions must extend `CustomCluster` or a derivative of it - -- Use constants for all attribute values referencing the appropriate labels from Zigpy / HA as necessary - -- Use an existing handler as a guide. signature and replacement dicts are required. Include the SimpleDescriptor entry for each endpoint in the signature dict above the definition of the endpoint in this format: - - ```yaml - # - ``` - -### How `device_automation_triggers` work: - - Device automation triggers are essentially representations of the events that the devices fire in HA. They allow users to use actions in the UI instead of using the raw events. Ex: For the Hue remote - the on button fires this event: - - `` - - and the action defined for this is: - - `(SHORT_PRESS, TURN_ON): {COMMAND: COMMAND_ON}` - - The first part `(SHORT_PRESS, TURN_ON)` corresponds to the txt the user will see in the UI: - -image - - The second part is the event data. You only need to supply enough of the event data to uniquely match the event which in this case is just the command for this event fired by this device: `{COMMAND: COMMAND_ON}` - - If you look at another example for the same device: - - `(SHORT_PRESS, DIM_UP): {COMMAND: COMMAND_STEP, CLUSTER_ID: 8, ENDPOINT_ID: 1, ARGS: [0, 30, 9],}` - - You can see a pattern that illustrates how to match a more complex event. In this case the step command is used for the dim up and dim down buttons so we need to match more of the event data to uniquely match the event. diff --git a/README.md b/README.md index b056c7751c..3ecb5b12ff 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,462 @@ -[![Build Status](https://travis-ci.org/zigpy/zha-device-handlers.svg?branch=master)](https://travis-ci.org/zigpy/zha-device-handlers) - # ZHA Device Handlers For Home Assistant -ZHA Device Handlers are custom quirks implementations for [Zigpy](https://github.com/zigpy/zigpy), the library that provides the [Zigbee](http://www.zigbee.org) support for the [ZHA](https://www.home-assistant.io/components/zha/) component in [Home Assistant](https://www.home-assistant.io). +![CI](https://github.com/zigpy/zha-device-handlers/workflows/CI/badge.svg?branch=dev) +[![Coverage Status](https://coveralls.io/repos/github/zigpy/zha-device-handlers/badge.svg)](https://coveralls.io/github/zigpy/zha-device-handlers) + +ZHA Device Handlers are custom quirks implementations for [Zigpy](https://github.com/zigpy/zigpy), the library that provides the [Zigbee](http://www.zigbee.org) support for the [ZHA](https://www.home-assistant.io/components/zha/) component in [Home Assistant](https://www.home-assistant.io). -ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices. Zigbee devices that deviate from or do not fully conform to the standard specifications set by the Zigbee Alliance may require the development of custom ZHA Device Handlers (ZHA custom quirks handler implementation) to for all their functions to work properly with the ZHA component in Home Assistant. +ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices. Zigbee devices that deviate from or do not fully conform to the standard specifications set by the Zigbee Alliance may require the development of custom ZHA Device Handlers (ZHA custom quirks handler implementation) to for all their functions to work properly with the ZHA component in Home Assistant. Custom quirks implementations for zigpy implemented as ZHA Device Handlers are a similar concept to that of [Hub-connected Device Handlers for the SmartThings Classics platform](https://docs.smartthings.com/en/latest/device-type-developers-guide/) as well that of [Zigbee-Herdsman Converters / Zigbee-Shepherd Converters as used by Zigbee2mqtt](https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html), meaning they are virtual representation of a physical device that expose additional functionality that is not provided out-of-the-box by the existing integration between these platforms. See [Device Specifics](#Device-Specifics) for details. # How to contribute -For specific Zigbee debugging instructions on capturing logs and more, see the contributing guidelines in the CONTRIBUTING.md file: -- [Guidelines in CONTRIBUTING.md](./CONTRIBUTING.md) +## Primer + +ZHA device handlers and it's provided Quirks allow Zigpy, ZHA and Home Assistant to work with non standard Zigbee devices. If you are reading this you may have a device that isn't working as expected. This can be the case for a number of reasons but in this guide we will cover the cases where functionality is provided by a device in a non specification compliant manner by the device manufacturer. -If you are looking to make your first code contribution to this project then we also suggest that you follow the steps in these guides: -- https://github.com/firstcontributions/first-contributions/blob/master/README.md -- https://github.com/firstcontributions/first-contributions/blob/master/github-desktop-tutorial.md +## What are these specifications -# Zigbee devices not following specifications which there already are quirks for: +[Zigbee Specification](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf) -### CentraLite -- [Contact Sensor](http://a.co/g9eWPAQ): CentraLite 3300-S -- [Motion Sensor](http://a.co/9PCEorM): CentraLite 3305-S -- [Dimmer Switch](https://centralite.com/products/smart-switch): CentraLite 3130 -- [Water Sensor](https://centralite.com/products/water-sensor): CentraLite 3315-S -- [Contact Sensor](https://www.irisbylowes.com/support/?guideTitle=Iris-Contact-Sensor-3320-L-(2nd-Gen)&guideId=441744fa-3e2b-3bc9-87b2-a8fc76d85341): CentraLite 3320-L -- [Motion Sensor](http://a.co/iYjshAP): CentraLite 3325-S -- [Motion Sensor](https://www.irisbylowes.com/support/?guideTitle=Iris-Motion-Sensor&guideId=4be71b61-5938-30b6-8154-bd90cb9b4796): CentraLite 3326-L -- [Contact Sensor](http://a.co/9PCEorM): CentraLite 3321-S -- [Temperature / Humidity Sensor](https://bit.ly/2GYguGR): CentraLite 3310-S -- [Smart Button](http://pdf.lowes.com/useandcareguides/812489023018_use.pdf): CentraLite 3460-L -- [Thermostat](https://centralite.com/products/pearl-thermostat): CentraLite 3157100 +[Zigbee Cluster Library](https://zigbeealliance.org/wp-content/uploads/2019/12/07-5123-06-zigbee-cluster-library-specification.pdf) -### Xiaomi Aqara -- [Cube](https://www.aqara.com/en/cube_controller-product.html): lumi.sensor_cube.aqgl01 -- [Button](https://www.aqara.com/en/wireless_mini_switch.html): lumi.sensor_switch.aq2 -- [Vibration Sensor](https://www.aqara.com/en/vibration_sensor.html): lumi.vibration.aq1 -- [Contact Sensor](https://www.aqara.com/en/door_and_window_sensor-product.html): lumi.sensor_magnet.aq2 -- [Motion Sensor](https://www.aqara.com/en/motion_sensor.html): lumi.sensor_motion.aq2 -- [Temperature / Humidity Sensor](https://www.aqara.com/en/temperature_and_humidity_sensor-product.html): lumi.weather -- [Water Leak](https://www.aqara.com/en/water_leak_sensor.html): lumi.sensor_wleak.aq1 -- [US Plug](https://www.aqara.com/en/smart_plug.html): lumi.plug.maus01 -- [EU Plug](https://zigbee.blakadder.com/Xiaomi_ZNCZ04LM.html): lumi.plug.mmeu01 -- [CN Plug](https://zigbee.blakadder.com/Xiaomi_ZNCZ02LM.html): lumi.plug +[Zigbee Base Device Specification](https://zigbeealliance.org/wp-content/uploads/zip/zigbee-base-device-behavior-bdb-v1-0.zip) -### Osram -- [OSRAM LIGHTIFY Dimming Switch](https://assets.osram-americas.com/assets/Documents/LTFY012.06c0d6e6-17c7-4dcb-bd2c-1fca7feecfb4.pdf): +[Zigbee Primer](https://docs.smartthings.com/en/latest/device-type-developers-guide/zigbee-primer.html) -### SmartThings -- [Arrival Sensor](https://support.smartthings.com/hc/en-us/articles/212417083): tagv4 -- [Motion Sensor](http://a.co/65rSQjZ): MotionV4 -- [Multi Sensor](http://a.co/gez6SzW): MultiV4 +## What is a device in human terms -### Keen Home -- [Temperature / Humidity / Pressure Sensor](https://keenhome.io/products/temp-sensor): LUMI RS-THP-MP-1.0 +A device is a physical object that you want to join to a Zigbee network: a light bulb, a switch, a sensor etc. The host application, in this case Zigpy, needs to understand how to interact with the device so there are standards that define how the application and devices can communicate. The device's functionality is described by several **descriptors** while the device itself contains **endpoints** and **endpoints** contain **clusters**. There are two types of clusters an endpoint contains: -### Lutron -- [Connected Bulb Remote](https://www.lutron.com/TechnicalDocumentLibrary/040421_Zigbee_Programming_Guide.pdf): Lutron LZL4BWHL01 Remote +- **in_clusters** - are "Server" clusters in ZCL terms. These clusters control the device, e.g. a smart plug or light bulb would have an `on_off` server cluster. **in_clusters** are also the ones which also send attribute reports and/or you can read an attribute from a **in_cluster**. +- **out_clusters** - are "Client" clusters. These clusters control some other device, as "Client" cluster sends commands to "Server" cluster. For example an On/Off remote would have an `on_off` client cluster and will generate cluster commands and send those to some other device. + Zigpy needs to understand all these elements in order to correctly work with the device. -### WAXMANN -- [Water Sensor](https://leaksmart.com/sensor/): leakSMART Water Sensor V2 +### Endpoints -### Digi -- [XBee Series 2](https://www.digi.com/products/embedded-systems/rf-modules/2-4-ghz-modules/xbee-zigbee): xbee -- [XBee Series 3](https://www.digi.com/products/embedded-systems/rf-modules/2-4-ghz-modules/xbee3-zigbee-3): xbee3 +Endpoints are essentially groupings of functionality. For example, a typical Zigbee light bulb will have a single endpoint for the light. A multi-gang wall switch may have an endpoint for each individual switch so they can all be controlled separately. Each endpoint has several functions represented by clusters. -### Yale -- [YRD210](https://www.yalehome.com/Yale/Yale%20US/Real%20Living/installation%20instructions/Yale%20DB%20PUSH%20Quickstart%2018JUL11_Rev%20B.pdf): Yale YRD210 Deadbolt -- [YRL220](https://www.yalehome.com/Yale/Yale%20US/Real%20Living/installation%20instructions/Yale%20%20DB%20Touch%20Instructions%2023AUG11_Rev%20B.pdf): Yale YRL220 Lock +### Clusters -### Tuya-based -- [TS0601 switch](https://zigbee.blakadder.com/Lerlink_X701A.html): Tuya-based 1-gang switches with neutral (e.g. Lerlink, Lonsonho) -- [Neo Siren](https://zigbee.blakadder.com/Neo_NAS-AB02B0.html): Tuya-based alarm siren with temperature and humidity sensors +Clusters are objects that contain the information (attributes and commands) for individual functions. There is the ability to turn the switch on and off, maybe there is energy monitoring, maybe there is the ability to add each switch to an individual group or a scene, etc. Each of these functions belong to a cluster. -# Configuration: +### Descriptors -1. Update Home Assistant to 0.85.1 or a later version. +For the purposes of Zigpy and Quirks we will focus on two descriptors: **Node Descriptor** and **Simple Descriptor**. -**NOTE:** Some devices will need to be unpaired and repaired in order to see sensor values populate in Home Assistant. +#### Node Descriptor -# Device Specifics: +A node descriptor explains some basic device attributes to Zigpy. The manufacturer code and the power type are the ones that we generally care about. In most cases you won't have to worry about this but it is good to know why it is there in case you come across it while looking at an existing quirk. Here is an example: +`` -### Centralite +#### Simple Descriptor -- All supported devices report battery level -- Dimmer Switch publishes events to Home Assistant -- Dimmer Switch temperature sensor is removed because it is non functional -- 3321-S reports acceleration -- 3310-S reports humidity +A simple descriptor is a description of a Zigbee device endpoint and is responsible for explaining the endpoint's functionality. It contains a profile id, the device type, and collections of clusters. The profile id tells the application what set of Zigbee rules to use. The most common profile will be 260 (0x0104) for the Home Automation profile. The device type tells the application what logical type of device this is ex: on off light, color light, etc. The clusters explain to the application what types of functionality exist on the endpoint. Here is an example: +`` -### Osram +## What the heck is a quirk -- Dimmer Switch publishes events to Home Assistant and reports battery level -- Dimmer Switch temperature sensor is removed because it is non functional +In human terms you can think of a quirk like google translate. I know it's a weird comparison but lets dig in a bit. You may only speak one language but there is an interesting article written in another language that you really want to read. Google translate takes the original article and displays it in a format (language) that you understand. A quirk is a file that translates device functionality from the format that the manufacturer chose to implement it in to a format that Zigpy and in turn ZHA understand. The main purpose of a quirk is to serve as a translator. A quirk is comprised of several parts: -### Xiaomi Aqara +- Signature - To identify and apply the correct quirk +- Replacement - To allow Zigpy and ZHA to correctly work with the device +- device_automation_triggers - To let the Home Assistant Device Automation engine and users interact with the device -- All supported devices report battery level -- All supported devices report temperature but I am unsure if it is correct or accurate -- Vibration sensor exposes a binary sensor in Home Assistant that reports current vibration state -- Vibration sensor sends `tilt` and `drop` events to Home Assistant -- Cube sends the following events: `flip (90 and 180 degrees)`, `rotate_left`, `rotate_right`, `knock`, `drop`, `slide` and `shake` -- Motion sensor exposes binary sensors for motion and occupancy. -- Button sends events to Home Assistant -- All supported plugs report power consumption and can be toggled +### Signature + +The signature on a quirk identifies the device as the manufacturer implemented it. You can think of it as a fingerprint or the dna of the device. The signature is what we use to identify the device. If any part of the signature doesn't match what the device returns during discovery the quirk will not match and as a result it will not be applied. The signature is made up of several parts: + +- `models_info` +- `endpoints` + +Models info tells the application which devices should use this particular quirk. Endpoints are the simple descriptors that we spoke about earlier exactly as they are on the device. `endpoints` is a dict where the key is the id of the endpoint and the value is an object with the following properties: `profile_id`, `device_type`, `input_clusters` and `output_clusters`. Creating the signature element is generally just a job of transcribing what the device gives us. Here is an example: + +```python +signature = { + MODELS_INFO: [(LUMI, "lumi.plug.maus01")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + }, +} +``` + +### Replacement + +The replacement on a quirk is what we want the device to be. Remember, we said that quirks were like Google translate... you can think of the replacement like the output from Google translate. The replacement dict is what will actually be used by Zigpy and ZHA to interact with the device. The structure of `replacement` is the same as signature with 2 key differences: `models_info` is generally omitted and there is an extra element `skip_configuration` that instructs the application to skip configuration if necessary. Some manufacturers have not implemented the specifications correctly and the devices come pre-configured and therefore the configuration calls fail (non Zigbee 3.0 Xiaomi devices for instance). Usually, you should not add `skip_configuration`. + +Here is an example: + +```python +replacement = { + SKIP_CONFIGURATION: True, + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + BasicCluster, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurementCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + }, +} +``` + +### device_automation_triggers + +Device automation triggers are essentially representations of the events that the devices fire in HA. They allow users to use actions in the UI instead of using the raw events. + +# Building a quirk + +Now that we got that out of the way we can focus on the task at hand: make our devices work the way they should with Zigpy and ZHA. Because the device doesn't work correctly out of the box we have to write a quirk for it. First lets look at what the quirk looks like when complete: + +```python +class Plug(XiaomiCustomDevice): + """lumi.plug.maus01 plug.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.voltage_bus = Bus() + self.consumption_bus = Bus() + self.power_bus = Bus() + super().__init__(*args, **kwargs) + + signature = { + MODELS_INFO: [(LUMI, "lumi.plug.maus01")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurement.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + # + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], + }, + # + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id], + }, + # + 100: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [BinaryInput.cluster_id], + OUTPUT_CLUSTERS: [BinaryInput.cluster_id, Groups.cluster_id], + }, + }, + } + replacement = { + SKIP_CONFIGURATION: True, + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + BasicCluster, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurementCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInputCluster], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id], + }, + 100: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [BinaryInput.cluster_id], + OUTPUT_CLUSTERS: [BinaryInput.cluster_id, Groups.cluster_id], + }, + }, + } +``` + +This quirk is for the US version of the Xiaomi plug. Xiaomi is notorious for not following the Zigbee specifications and most of their non Zigbee 3.0 devices need a quirk to function correctly. In this case we are correcting the `ElectricalMeasurement` cluster readings. Xiaomi decided to report the values for this cluster on the `AnalogInput` cluster instead. To fix this we will create a custom cluster to replace the `AnalogInput` and `ElectricalMeasurement` clusters. We will take the values that are reported on the `AnalogInput` cluster and publish them to the `ElectricalMeasurement` cluster. Doing this allows the device to work as if Xiaomi had implemented this in the first place. This is the act of translating that was mentioned in the Google Translate analogy above. + +First things first. All device definitions in quirks must extend `CustomDevice` or a derivative of it and all clusters that you define must extend `CustomCluster` or a derivative of it. If you want to send messages between `CustomCluster` definitions as we do here you need to create channels for the communication to flow through. We do this by adding instances of `Bus` on our `CustomDevice` implementation. `Bus` is a utility class used specifically for this purpose and adding it to the device implementation ensures that all clusters that you define will have access to the `Bus` so that they can communicate with eachother. + +```python +class Plug(XiaomiCustomDevice): + """lumi.plug.maus01 plug.""" + + def __init__(self, *args, **kwargs): + """Init.""" + self.voltage_bus = Bus() + self.consumption_bus = Bus() + self.power_bus = Bus() + super().__init__(*args, **kwargs) +``` + +You can see that we have extended `XiaomiCustomDevice` which is a derivative of `CustomDevice` shared by Xiaomi devices. You can also see that we have added some instances of `Bus` so that we can pass messages between `CustomCluster` definitions. To be clear, this is not always necessary. Quirks can be used to change formats of data on an existing cluster, to add manufacturer specific attributes or commands to clusters etc. In these instances you just need to create a derivative of `CustomCluster` and add your logic. This is more of an advanced example to illustrate what is possible. + +Here are the custom cluster definitions: + +```python +class AnalogInputCluster(CustomCluster, AnalogInput): + """Analog input cluster, only used to relay power consumtion information to ElectricalMeasurementCluster.""" + + cluster_id = AnalogInput.cluster_id + + def __init__(self, *args, **kwargs): + """Init.""" + self._current_state = {} + super().__init__(*args, **kwargs) + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if value is not None and value >= 0: + self.endpoint.device.power_bus.listener_event(POWER_REPORTED, value) + + +class ElectricalMeasurementCluster(LocalDataCluster, ElectricalMeasurement): + """Electrical measurement cluster to receive reports that are sent to the basic cluster.""" + + cluster_id = ElectricalMeasurement.cluster_id + POWER_ID = 0x050B + VOLTAGE_ID = 0x0500 + CONSUMPTION_ID = 0x0304 + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.voltage_bus.add_listener(self) + self.endpoint.device.consumption_bus.add_listener(self) + self.endpoint.device.power_bus.add_listener(self) + + def power_reported(self, value): + """Power reported.""" + self._update_attribute(self.POWER_ID, value) + + def voltage_reported(self, value): + """Voltage reported.""" + self._update_attribute(self.VOLTAGE_ID, value) + + def consumption_reported(self, value): + """Consumption reported.""" + self._update_attribute(self.CONSUMPTION_ID, value) +``` + +In the `AnalogInput` cluster we override the `_update_attribute` method so that we can access the data that the cluster receives when the device sends a report and we send the data via an event on a bus to the `ElectricalMeasurement` cluster. This is the line that does the heavy lifting: + +`self.endpoint.device.power_bus.listener_event(POWER_REPORTED, value)` + +Then in the `ElectricalMeasurement` cluster we need to subscribe to these events and handle them. This is how we subscribe to our custom events: + +`self.endpoint.device.power_bus.add_listener(self)` + +and this method (the method name must match the event name that you publish EXACTLY): + +```python +def power_reported(self, value): + """Power reported.""" + self._update_attribute(self.POWER_ID, value) +``` + +receives the event and handles updating the attribute on the correct zigbee cluster. As you can see there really isn't much here that needs to be done to accomplish our goal. + +Once we have created our `CustomCluster` implementations we have to tell the `CustomDevice` implementation to use them. We do this in the `replacement` dict in the quirk definition. Start by copying the `signature` dict and remove the `models_info` from it. Then we replace the cluster ids that we want to override with the names of our `CustomCluster` implementations that we have created. The result looks like this: + +```python +replacement = { + SKIP_CONFIGURATION: True, + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.SMART_PLUG, + INPUT_CLUSTERS: [ + BasicCluster, + PowerConfiguration.cluster_id, + DeviceTemperature.cluster_id, + Groups.cluster_id, + Identify.cluster_id, + OnOff.cluster_id, + Scenes.cluster_id, + BinaryOutput.cluster_id, + Time.cluster_id, + ElectricalMeasurementCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, + INPUT_CLUSTERS: [AnalogInputCluster], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id, Groups.cluster_id], + }, + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.METER_INTERFACE, + INPUT_CLUSTERS: [AnalogInput.cluster_id], + OUTPUT_CLUSTERS: [AnalogInput.cluster_id], + }, + 100: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [BinaryInput.cluster_id], + OUTPUT_CLUSTERS: [BinaryInput.cluster_id, Groups.cluster_id], + }, +``` + +You can see that we have replaced `ElectricalMeasurement.cluster_id` from endpoint 1 in the `signature` dict with the name of our cluster that we created: `ElectricalMeasurementCluster` and on endpoint 2 we replaced `AnalogInput.cluster_id` with the implementation we created for that: `AnalogInputCluster`. This instructs Zigpy to use these `CustomCluster` derivatives instead of the normal cluster definitions for these clusters and this is why this part of the quirk is called `replacement`. + +Now lets put this all together. If you examine the device definition above you will see that we have defined our custom device, we defined the `signature` dict where we transcribed the `SimpleDescriptor` output we obtained when the device joined the network and we defined the `replacement` dict where we swapped the cluster ids for the culsters that we wanted to replace with the `CustomCluster` implementations that we created. + +# Contribution Guidelines + +- All code is formatted with black. The check format script that runs in CI will ensure that code meets this requirement and that it is correctly formatted with black. Instructions for installing black in many editors can be found here: + +- Capture the SimpleDescriptor log entries for each endpoint on the device. These can be found in the HA logs after joining a device and they look like this: ``. This information can also be obtained from the zigbee.db if you want to take the time to query the tables and reconstitute the log entry. I find it easier to just remove and rejoin the device. ZHA entity ids are stable for the most part so it _shouldn't_ disrupt anything you have configured. These need to match what the device reports EXACTLY or zigpy will not match them when a device joins and the handler will not be used for the device. You can also obtain this information from the device screen in HA for the device. The `Zigbee Device Signature` button will launch a dialog that contains all of the information necessary to create quirks. + +- All custom device definitions must extend `CustomDevice` or a derivative of it + +- All custom cluster definitions must extend `CustomCluster` or a derivative of it + +- Use constants for all attribute values referencing the appropriate labels from Zigpy / HA as necessary + +- Use an existing handler as a guide. signature and replacement dicts are required. Include the SimpleDescriptor entry for each endpoint in the signature dict above the definition of the endpoint in this format: + + ```yaml + # + ``` -### SmartThings +# How `device_automation_triggers` work -- All supported devices report battery level -- tagV4 exposed as a device tracker in Home Assistant. The current implementation will use batteries rapidly -- MultiV4 reports acceleration +Device automation triggers are essentially representations of the events that the devices fire in HA. They allow users to use actions in the UI instead of using the raw events. Ex: For the Hue remote - the on button fires this event: -### Lutron +`` -- Connected bulb remote publishes events to Home Assistant +and the action defined for this is: -### WAXMANN +`(SHORT_PRESS, TURN_ON): {COMMAND: COMMAND_ON}` -- leakSMART water sensor is exposed as a binary_sensor with DEVICE_CLASS_MOISTURE +The first part `(SHORT_PRESS, TURN_ON)` corresponds to the txt the user will see in the UI: -### Digi XBee +image -- Some functionality requires a coordinator device to be XBee as well -- GPIO pins are exposed to Home Assistant as switches -- Analog inputs are exposed as sensors -- PWM output on XBee3 can be controlled by writing 0x0055 (present_value) cluster attribute with `zha.set_zigbee_cluster_attribute` service -- Outgoing UART data can be sent with `zha.issue_zigbee_cluster_command` service -- Incoming UART data will generate `zha_event` event. -- PWM can be controlled with `zha.set_zigbee_cluster_attribute` service +The second part is the event data. You only need to supply enough of the event data to uniquely match the event which in this case is just the command for this event fired by this device: `{COMMAND: COMMAND_ON}` -Please refer to [xbee.md](xbee.md) for details on configuration and usage examples. +If you look at another example for the same device: -### Yale +`(SHORT_PRESS, DIM_UP): {COMMAND: COMMAND_STEP, CLUSTER_ID: 8, ENDPOINT_ID: 1, ARGS: [0, 30, 9],}` -- All supported devices report battery level +You can see a pattern that illustrates how to match a more complex event. In this case the step command is used for the dim up and dim down buttons so we need to match more of the event data to uniquely match the event. # Testing new releases Testing a new release of the zha-quirks package before it is released in Home Assistant. If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io distro): -- Add https://github.com/home-assistant/hassio-addons-development as "add-on" repository + +- Add as "add-on" repository - Install "Custom deps deployment" addon -- Update config like: - ``` +- Update config like: + + ```yml pypi: - zha-quirks==0.0.38 apk: [] ``` + where 0.0.38 is the new version + - Start the addon If you are instead using some custom python installation of Home Assistant then do this: + - Activate your python virtual env -- Update package with ``pip`` - ``` +- Update package with `pip` + + ```bash pip install zha-quirks==0.0.38 + ``` # Testing quirks in development in docker based install + If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io distro) you will need to get access to the home-assistant docker container. Directions below are given for using the portainer add-on to do this, there are other methods as well not covered here. -- Install the portainer add-on (https://github.com/hassio-addons/addon-portainer) from Home Assistant Community Add-ons. -- Follow the add-on documentation to un-hide the home-assistant container (https://github.com/hassio-addons/addon-portainer/blob/master/portainer/DOCS.md) + +- Install the portainer add-on () from Home Assistant Community Add-ons. +- Follow the add-on documentation to un-hide the home-assistant container () - Stage the update quirk in a directory within your config directory - Use portainer to access a console in the home-assistant container: @@ -169,9 +465,9 @@ If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io - Access the quirks directory - on HA > 0.113: /usr/local/lib/python3.8/site-packages/zhaquirks/ - on HA < 0.113: /usr/local/lib/python3.7/site-packages/zhaquirks/ -- Copy updated/new quirk to zhaquirks directory: ```cp -a /config/temp/NEW_QUIRK ./``` -- Remove the __pycache__ folder so it is regenerated ```rm -rf ./__pycache__/``` -- Close out the console and restart HA. +- Copy updated/new quirk to zhaquirks directory: `cp -a /config/temp/NEW_QUIRK ./` +- Remove the **pycache** folder so it is regenerated `rm -rf ./__pycache__/` +- Close out the console and restart HA. - Note: The added/update quirk will not survive a HA version update. # Thanks @@ -182,17 +478,22 @@ If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io # Related projects -### Zigpy +## Zigpy + **[zigpy](https://github.com/zigpy/zigpy)** is **[Zigbee protocol stack](https://en.wikipedia.org/wiki/Zigbee)** integration project to implement the **[Zigbee Home Automation](https://www.zigbee.org/)** standard as a Python 3 library. Zigbee Home Automation integration with zigpy allows you to connect one of many off-the-shelf Zigbee adapters using one of the available Zigbee radio library modules compatible with zigpy to control Zigbee based devices. There is currently support for controlling Zigbee device types such as binary sensors (e.g., motion and door sensors), sensors (e.g., temperature sensors), lightbulbs, switches, locks, fans, covers (blinds, marquees, and more). A working implementation of zigpy exist in **[Home Assistant](https://www.home-assistant.io)** (Python based open source home automation software) as part of its **[ZHA component](https://www.home-assistant.io/components/zha/)** -### ZHA Map +## ZHA Map + [zha-map](https://github.com/zha-ng/zha-map) project allow building a Zigbee network topology map for ZHA component in Home Assistant. -### zha-network-visualization-card +## zha-network-visualization-card + [zha-network-visualization-card](https://github.com/dmulcahey/zha-network-visualization-card) is a custom Lovelace element for visualizing the Zigbee network map for ZHA component in Home Assistant. -### ZHA Network Card +## ZHA Network Card + [zha-network-card](https://github.com/dmulcahey/zha-network-card) is a custom Lovelace card that displays ZHA network and device information in Home Assistant -### zigpy-deconz-parser +## zigpy-deconz-parser + [zigpy-deconz-parser](https://github.com/zha-ng/zigpy-deconz-parser) project can parse Home Assistant ZHA component debug log using `zigpy-deconz` library if you have ConBee or RaspBee hardware. From 0f0ae685638ee67b93b5fb6c4526f951311b2ce7 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 9 Nov 2020 13:47:56 +0100 Subject: [PATCH 097/113] Added support for Philips LCE002 (Color E14 Bluetooth model) (#567) --- zhaquirks/philips/zhaextendedcolorlight.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/zhaquirks/philips/zhaextendedcolorlight.py b/zhaquirks/philips/zhaextendedcolorlight.py index 88df213fbc..d84b1073d8 100644 --- a/zhaquirks/philips/zhaextendedcolorlight.py +++ b/zhaquirks/philips/zhaextendedcolorlight.py @@ -34,7 +34,12 @@ class ZHAExtendedColorLight(CustomDevice): """Philips ZigBee HomeAutomation extended color bulb device.""" signature = { - MODELS_INFO: [(PHILIPS, "LCA001"), (PHILIPS, "LCA003"), (PHILIPS, "LCB001")], + MODELS_INFO: [ + (PHILIPS, "LCA001"), + (PHILIPS, "LCA003"), + (PHILIPS, "LCB001"), + (PHILIPS, "LCE002"), + ], ENDPOINTS: { 11: { # Date: Wed, 11 Nov 2020 11:49:26 -0500 Subject: [PATCH 098/113] Bump up version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 71c9bbcc76..9009fa2613 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.45" +VERSION = "0.0.46" def readme(): From 347b5084bf1cbc767e71f916087b6437b3b2d1c8 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 11 Nov 2020 12:06:59 -0500 Subject: [PATCH 099/113] Update release drafter version --- .github/workflows/release-management.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-management.yml b/.github/workflows/release-management.yml index 3c3045c711..c246792ec0 100644 --- a/.github/workflows/release-management.yml +++ b/.github/workflows/release-management.yml @@ -10,6 +10,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: toolmantim/release-drafter@v5.2.0 + - uses: release-drafter/release-drafter@v5 env: - GITHUB_TOKEN: ${{ secrets.GH_RELEASE_DRAFTER }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GH_RELEASE_DRAFTER }} From 448cb3f40a33b938f00f2c8a8409401602256646 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Thu, 19 Nov 2020 23:03:25 -0500 Subject: [PATCH 100/113] Add Philips sml001 motion sensor quirk (#583) * Add sml001 motion sensor * Move occupancy cluster to init --- zhaquirks/philips/__init__.py | 10 ++++ zhaquirks/philips/sml001.py | 106 ++++++++++++++++++++++++++++++++++ zhaquirks/philips/sml002.py | 14 +---- 3 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 zhaquirks/philips/sml001.py diff --git a/zhaquirks/philips/__init__.py b/zhaquirks/philips/__init__.py index 7caf8ff9e4..c73e3f5f65 100644 --- a/zhaquirks/philips/__init__.py +++ b/zhaquirks/philips/__init__.py @@ -7,6 +7,7 @@ import zigpy.types as t from zigpy.zcl.clusters.general import Basic, LevelControl, OnOff from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.measurement import OccupancySensing from ..const import ( ARGS, @@ -77,6 +78,15 @@ class PowerOnState(t.enum8): LastState = 0xFF +class OccupancyCluster(CustomCluster, OccupancySensing): + """Philips occupancy cluster.""" + + manufacturer_attributes = { + 0x0030: ("sensitivity", t.uint8_t), + 0x0031: ("sensitivity_max", t.uint8_t), + } + + class PhilipsOnOffCluster(CustomCluster, OnOff): """Philips OnOff cluster.""" diff --git a/zhaquirks/philips/sml001.py b/zhaquirks/philips/sml001.py new file mode 100644 index 0000000000..e3d641fa36 --- /dev/null +++ b/zhaquirks/philips/sml001.py @@ -0,0 +1,106 @@ +"""Quirk for Philips SML001.""" +from zigpy.profiles import zha, zll +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import ( + Basic, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + PowerConfiguration, + Scenes, +) +from zigpy.zcl.clusters.lighting import Color +from zigpy.zcl.clusters.measurement import ( + IlluminanceMeasurement, + OccupancySensing, + TemperatureMeasurement, +) + +from . import PHILIPS, OccupancyCluster +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class PhilipsSML001(CustomDevice): + """philips SML001 device.""" + + signature = { + MODELS_INFO: [(PHILIPS, "SML001")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.ON_OFF_SENSOR, + INPUT_CLUSTERS: [Basic.cluster_id], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + }, + # + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + IlluminanceMeasurement.cluster_id, + TemperatureMeasurement.cluster_id, + OccupancySensing.cluster_id, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.ON_OFF_SENSOR, + INPUT_CLUSTERS: [Basic.cluster_id], + OUTPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Color.cluster_id, + ], + }, + 2: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + IlluminanceMeasurement.cluster_id, + TemperatureMeasurement.cluster_id, + OccupancyCluster, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id], + }, + } + } diff --git a/zhaquirks/philips/sml002.py b/zhaquirks/philips/sml002.py index 39ec317b62..91f4da3e16 100644 --- a/zhaquirks/philips/sml002.py +++ b/zhaquirks/philips/sml002.py @@ -1,7 +1,6 @@ """Quirk for Philips SML002.""" from zigpy.profiles import zha, zll -from zigpy.quirks import CustomCluster, CustomDevice -import zigpy.types as t +from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( Basic, Groups, @@ -19,7 +18,7 @@ TemperatureMeasurement, ) -from . import PHILIPS +from . import PHILIPS, OccupancyCluster from ..const import ( DEVICE_TYPE, ENDPOINTS, @@ -30,15 +29,6 @@ ) -class OccupancyCluster(CustomCluster, OccupancySensing): - """philips occupancy cluster.""" - - manufacturer_attributes = { - 0x0030: ("sensitivity", t.uint8_t), - 0x0031: ("sensitivity_max", t.uint8_t), - } - - class PhilipsSML002(CustomDevice): """philips SML002 device.""" From 1980fdf840fccfec5800671686c45590b42cea80 Mon Sep 17 00:00:00 2001 From: Andreas Billmeier Date: Sat, 21 Nov 2020 05:43:08 +0100 Subject: [PATCH 101/113] Update setup.py, fix bad install exclude mask (#584) otherwise it would install a package called 'tests' at top level. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9009fa2613..af0afb74de 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def readme(): author_email="david.mulcahey@icloud.com", license="Apache License Version 2.0", keywords="zha quirks homeassistant hass", - packages=find_packages(exclude=["*.tests"]), + packages=find_packages(exclude=["tests"]), python_requires=">=3", install_requires=["zigpy>=0.22.1"], tests_require=["pytest"], From 1817ee3ca9e3fd584e8e18c37be3a4b78ec2eacf Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 24 Nov 2020 19:00:04 -0500 Subject: [PATCH 102/113] Xiaomi quick initialization (#594) * Create the device from quirk's signature * Framework for Xiaomi quick init * Refactor some Xiaomi devices for the quick init * Add leak sensor to quick init * Require specific attributes in signature of QuickInit devices * Init NodeDescriptor from signature * Bump up zigpy dependency * Logging * Bump up test requirements to invalidate cache * Fix tests * Update tests * Update tests * Bump up zigpy dependency * Optimize quirk tests * Add Smoke Sensor to quick init * Add Mija's sensors to quick init * Take setup.py requirements into consideration for caching * Avoid collision with existing quirks --- .github/workflows/ci.yml | 7 + requirements_test_all.txt | 2 +- setup.py | 2 +- tests/test_quirks.py | 231 ++++++++++++++++++++- tests/test_xiaomi.py | 167 ++++++++++++++- zhaquirks/__init__.py | 60 +++++- zhaquirks/const.py | 31 ++- zhaquirks/xiaomi/__init__.py | 91 +++++++- zhaquirks/xiaomi/aqara/cube.py | 12 +- zhaquirks/xiaomi/aqara/magnet_aq2.py | 12 +- zhaquirks/xiaomi/aqara/motion_aq2.py | 7 +- zhaquirks/xiaomi/aqara/remote_b186acn01.py | 12 +- zhaquirks/xiaomi/aqara/vibration_aq1.py | 12 +- zhaquirks/xiaomi/aqara/weather.py | 8 +- zhaquirks/xiaomi/aqara/wleak_aq1.py | 12 +- zhaquirks/xiaomi/mija/motion.py | 7 +- zhaquirks/xiaomi/mija/sensor_magnet.py | 12 +- zhaquirks/xiaomi/mija/smoke.py | 16 +- 18 files changed, 658 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bce4c0d5a5..17a096f2fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ jobs: key: >- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ + hashFiles('setup.py') }}-${{ hashFiles('requirements_test_all.txt') }} restore-keys: | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- @@ -70,6 +71,7 @@ jobs: key: >- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ + hashFiles('setup.py') }}-${{ hashFiles('requirements_test_all.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -111,6 +113,7 @@ jobs: key: >- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ + hashFiles('setup.py') }}-${{ hashFiles('requirements_test_all.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -154,6 +157,7 @@ jobs: key: >- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ + hashFiles('setup.py') }}-${{ hashFiles('requirements_test_all.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -200,6 +204,7 @@ jobs: key: >- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ + hashFiles('setup.py') }}-${{ hashFiles('requirements_test_all.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -247,6 +252,7 @@ jobs: key: >- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ + hashFiles('setup.py') }}-${{ hashFiles('requirements_test_all.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -308,6 +314,7 @@ jobs: key: >- ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ + hashFiles('setup.py') }}-${{ hashFiles('requirements_test_all.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68822d421f..7df0ae5e19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ # new version asynctest==0.13.0 codecov==2.1.10 -coveralls==2.1.2 +coveralls==2.2.0 flake8-docstrings==1.5.0 mock-open==1.4.0 mypy==0.790 diff --git a/setup.py b/setup.py index af0afb74de..9014132cac 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,6 @@ def readme(): keywords="zha quirks homeassistant hass", packages=find_packages(exclude=["tests"]), python_requires=">=3", - install_requires=["zigpy>=0.22.1"], + install_requires=["zigpy>=0.28.1"], tests_require=["pytest"], ) diff --git a/tests/test_quirks.py b/tests/test_quirks.py index 28d9dd8937..76cf12864d 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -1,17 +1,35 @@ """General quirk tests.""" +from unittest import mock + import pytest +import zigpy.device +import zigpy.endpoint +import zigpy.profiles import zigpy.quirks as zq +import zigpy.types import zhaquirks # noqa: F401, E402 -from zhaquirks.const import ENDPOINTS - -ALL_QUIRK_CLASSES = ( - quirk - for manufacturer in zq._DEVICE_REGISTRY._registry.values() - for model in manufacturer.values() - for quirk in model +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MANUFACTURER, + MODEL, + MODELS_INFO, + NODE_DESCRIPTOR, + OUTPUT_CLUSTERS, + PROFILE_ID, ) +from zhaquirks.xiaomi import XIAOMI_NODE_DESC + +ALL_QUIRK_CLASSES = [] +for manufacturer in zq._DEVICE_REGISTRY._registry.values(): + for model_quirk_list in manufacturer.values(): + for quirk in model_quirk_list: + if quirk in ALL_QUIRK_CLASSES: + continue + ALL_QUIRK_CLASSES.append(quirk) @pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES) @@ -22,3 +40,202 @@ def test_quirk_replacements(quirk): assert quirk.replacement assert quirk.replacement[ENDPOINTS] + + +@pytest.fixture +def raw_device(): + """Raw device.""" + app = mock.MagicMock() + ieee = zigpy.types.EUI64.convert("11:22:33:44:55:66:77:88") + nwk = 0x1234 + return zigpy.device.Device(app, ieee, nwk) + + +def test_dev_from_signature_incomplete_sig(raw_device): + """Test device initialization from quirk's based on incomplete signature.""" + + class BadSigNoSignature(zhaquirks.QuickInitDevice): + pass + + with pytest.raises(AssertionError): + BadSigNoSignature.from_signature(raw_device, model="test_model") + + class BadSigNoModel(zhaquirks.QuickInitDevice): + signature = { + MODELS_INFO: [("manufacturer_1", "model_1")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, + ENDPOINTS: { + 3: { + PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, + DEVICE_TYPE: zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH, + INPUT_CLUSTERS: [1, 6], + OUTPUT_CLUSTERS: [0x19], + } + }, + } + + with pytest.raises(KeyError): + BadSigNoModel.from_signature(raw_device) + + # require model in method, if none is present in signature + BadSigNoModel.from_signature(raw_device, model="model_model") + + # require manufacturer, if no signature[MODELS_INFO] + BadSigNoModel.signature.pop(MODELS_INFO) + with pytest.raises(KeyError): + BadSigNoModel.from_signature(raw_device, model="model_model") + BadSigNoModel.signature[MANUFACTURER] = "some manufacturer" + BadSigNoModel.from_signature(raw_device, model="model_model") + + ep_sig_complete = { + PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, + DEVICE_TYPE: zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH, + INPUT_CLUSTERS: [1, 6], + OUTPUT_CLUSTERS: [0x19], + } + + class BadSigIncompleteEp(zhaquirks.QuickInitDevice): + signature = { + MANUFACTURER: "manufacturer", + MODEL: "model", + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, + ENDPOINTS: {3: {**ep_sig_complete}}, + } + + BadSigIncompleteEp.from_signature(raw_device) + + for missing_item in ep_sig_complete: + incomplete_ep = {**ep_sig_complete} + incomplete_ep.pop(missing_item) + BadSigIncompleteEp.signature[ENDPOINTS][3] = incomplete_ep + with pytest.raises(KeyError): + BadSigIncompleteEp.from_signature(raw_device) + + +@pytest.mark.parametrize( + "quirk_signature", + ( + { + ENDPOINTS: { + 1: { + PROFILE_ID: 11, + DEVICE_TYPE: 22, + INPUT_CLUSTERS: [0, 1, 4], + OUTPUT_CLUSTERS: [0, 6, 8, 768], + } + }, + MANUFACTURER: "manufacturer_3", + MODEL: "model_3", + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, + }, + { + ENDPOINTS: { + 2: { + PROFILE_ID: 33, + DEVICE_TYPE: 44, + INPUT_CLUSTERS: [0, 0x0201, 0x0402], + OUTPUT_CLUSTERS: [0x0019, 0x0401], + }, + 5: { + PROFILE_ID: 55, + DEVICE_TYPE: 66, + INPUT_CLUSTERS: [0, 6, 8], + OUTPUT_CLUSTERS: [0x0019, 0x0500], + }, + }, + MANUFACTURER: "manufacturer_4", + MODEL: "model_4", + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, + }, + { + ENDPOINTS: { + 2: { + PROFILE_ID: 33, + DEVICE_TYPE: 44, + INPUT_CLUSTERS: [0, 0x0201, 0x0402], + OUTPUT_CLUSTERS: [0x0019, 0x0401], + }, + 5: { + PROFILE_ID: 55, + DEVICE_TYPE: 66, + INPUT_CLUSTERS: [0, 6, 8], + OUTPUT_CLUSTERS: [0x0019, 0x0500], + }, + }, + MODELS_INFO: (("LUMI", "model_4"),), + MODEL: "model_4", + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, + }, + { + ENDPOINTS: { + 1: { + "profile_id": 260, + "device_type": 0x0100, + INPUT_CLUSTERS: [ + 0x0000, + 0x0003, + 0x0004, + 0x0005, + 0x0006, + 0x0702, + 0x0B05, + ], + OUTPUT_CLUSTERS: [0x000A, 0x0019], + }, + 2: { + "profile_id": 260, + "device_type": 0x0103, + INPUT_CLUSTERS: [0x0000, 0x0003, 0x0B05], + OUTPUT_CLUSTERS: [0x0003, 0x0006], + }, + }, + MANUFACTURER: "manufacturer 5", + MODEL: "model 5", + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, + }, + ), +) +def test_dev_from_signature(raw_device, quirk_signature): + """Test device initialization from quirk's based on signature.""" + + class QuirkDevice(zhaquirks.QuickInitDevice): + signature = quirk_signature + + device = QuirkDevice.from_signature(raw_device) + assert device.status == zigpy.device.Status.ENDPOINTS_INIT + if MANUFACTURER in quirk_signature: + assert device.manufacturer == quirk_signature[MANUFACTURER] + else: + assert device.manufacturer == "LUMI" + assert device.model == quirk_signature[MODEL] + + ep_signature = quirk_signature[ENDPOINTS] + assert len(device.endpoints) == len(ep_signature) + 1 # ZDO endpoint + for ep_id, ep_data in ep_signature.items(): + assert ep_id in device.endpoints + ep = device.endpoints[ep_id] + assert ep.status == zigpy.endpoint.Status.ZDO_INIT + assert ep.profile_id == ep_data[PROFILE_ID] + assert ep.device_type == ep_data[DEVICE_TYPE] + assert [cluster_id for cluster_id in ep.in_clusters] == ep_data[INPUT_CLUSTERS] + assert [cluster_id for cluster_id in ep.out_clusters] == ep_data[ + OUTPUT_CLUSTERS + ] + + +@pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES) +def test_quirk_quickinit(quirk): + """Make sure signature in QuickInit Devices have all required attributes.""" + + if not issubclass(quirk, zhaquirks.QuickInitDevice): + return + + assert quirk.signature.get(MODELS_INFO) or quirk.signature[MANUFACTURER] + assert quirk.signature[NODE_DESCRIPTOR] + assert quirk.signature[ENDPOINTS] + for ep_id, ep_data in quirk.signature[ENDPOINTS].items(): + assert ep_id + assert PROFILE_ID in ep_data + assert DEVICE_TYPE in ep_data + assert isinstance(ep_data[INPUT_CLUSTERS], list) + assert isinstance(ep_data[OUTPUT_CLUSTERS], list) diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 6e9a18fa83..88cf77da32 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -3,9 +3,30 @@ from unittest import mock import pytest - -from zhaquirks.const import OFF, ON, ZONE_STATE -from zhaquirks.xiaomi import BasicCluster +import zigpy.device +import zigpy.types as t + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MANUFACTURER, + MODEL, + NODE_DESCRIPTOR, + OFF, + ON, + OUTPUT_CLUSTERS, + PROFILE_ID, + ZONE_STATE, +) +from zhaquirks.xiaomi import ( + LUMI, + XIAOMI_NODE_DESC, + BasicCluster, + XiaomiCustomDevice, + XiaomiQuickInitDevice, + handle_quick_init, +) import zhaquirks.xiaomi.aqara.motion_aq2 import zhaquirks.xiaomi.aqara.motion_aq2b import zhaquirks.xiaomi.mija.motion @@ -85,3 +106,143 @@ async def test_konke_motion(zigpy_device_from_quirk, quirk): assert len(occupancy_listener.attribute_updates) == 2 assert occupancy_listener.attribute_updates[1][0] == 0x0000 assert occupancy_listener.attribute_updates[1][1] == 0 + + +@pytest.fixture +def raw_device(): + """Raw device fixture.""" + + ieee = t.EUI64.convert("11:22:33:44:55:66:77:88") + device = zigpy.device.Device(mock.MagicMock(), ieee, 0x1234) + with mock.patch.object(device, "cancel_initialization"): + yield device + + +@pytest.mark.parametrize( + "ep_id, cluster, message", + ( + (0, 0, b"\x18\x00\n\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01"), + (0, 1, b"\x18\x00\n\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01"), + ), +) +def test_xiaomi_quick_init_wrong_ep(raw_device, ep_id, cluster, message): + """Test quick init when message is received on wrong endpoint.""" + + with mock.patch("zigpy.zcl.foundation.ZCLHeader.deserialize") as hdr_deserialize: + assert ( + handle_quick_init(raw_device, 0x0260, cluster, ep_id, ep_id, message) + is None + ) + assert hdr_deserialize.call_count == 0 + assert raw_device.cancel_initialization.call_count == 0 + assert raw_device.application.device_initialized.call_count == 0 + + +@pytest.mark.parametrize( + "cluster, message", + ( + ( + 0, + b"\x19\x00\n\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01", + ), # cluster command + (1, b"\x18\x00\n\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01"), # wrong cluster + ( + 0, + b"\x18\x00\x01\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01", + ), # wrong command + ( + 0, + b"\x18\x00\xFF\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01", + ), # unknown command + (0, b"\x18\x00\n\x04\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01"), # wrong attr id + (0, b"\x18\x00\n\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 "), # data under run + (0, b"\x18\x00\n\x05\x00B\x00\x01\x00 \x01"), # no model + (0, b"\x18\x00\n\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01"), # no quirk + ), +) +def test_xiaomi_quick_init_wrong_cluster_or_message(raw_device, cluster, message): + """Test quick init when message is received on wrong cluster or wrong endpoint.""" + + assert handle_quick_init(raw_device, 0x0260, cluster, 1, 1, message) is None + assert raw_device.cancel_initialization.call_count == 0 + assert raw_device.application.device_initialized.call_count == 0 + + +def test_xiaomi_quick_init_wrong_quirk_type(raw_device): + """Test quick init for existing quirk which is not enabled for quick joining.""" + + class WrongDevice(XiaomiCustomDevice): + signature = { + MANUFACTURER: LUMI, + MODEL: "lumi.sensor_smoke_2", + } + + assert ( + handle_quick_init( + raw_device, + 0x0260, + 0, + 1, + 1, + b"\x18\x00\n\x05\x00B\x13lumi.sensor_smoke_2\x01\x00 \x01", + ) + is None + ) + assert raw_device.cancel_initialization.call_count == 0 + assert raw_device.application.device_initialized.call_count == 0 + + +def test_xiaomi_quick_init_wrong_signature(raw_device): + """Test quick init for existing quirk with wrong signature for quick joining.""" + + class WrongSignature(XiaomiQuickInitDevice): + signature = { + MODEL: "lumi.sensor_sm0ke", + } + + assert ( + handle_quick_init( + raw_device, + 0x0260, + 0, + 1, + 1, + b"\x18\x00\n\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01", + ) + is None + ) + assert raw_device.cancel_initialization.call_count == 0 + assert raw_device.application.device_initialized.call_count == 0 + + +def test_xiaomi_quick_init(raw_device): + """Test quick init.""" + + class XiaomiQuirk(XiaomiQuickInitDevice): + signature = { + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, + ENDPOINTS: { + 1: { + PROFILE_ID: 0x0260, + DEVICE_TYPE: 0x0000, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [], + } + }, + MANUFACTURER: LUMI, + MODEL: "lumi.sensor_sm0ke", + } + + assert ( + handle_quick_init( + raw_device, + 0x0260, + 0, + 1, + 1, + b"\x18\x00\n\x05\x00B\x11lumi.sensor_sm0ke\x01\x00 \x01", + ) + is True + ) + assert raw_device.cancel_initialization.call_count == 1 + assert raw_device.application.device_initialized.call_count == 1 diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index e6d491cfb5..6fc606d0f5 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -3,8 +3,11 @@ import importlib import logging import pkgutil +from typing import Any, Dict, Optional -from zigpy.quirks import CustomCluster +import zigpy.device +import zigpy.endpoint +from zigpy.quirks import CustomCluster, CustomDevice from zigpy.util import ListenableMixin from zigpy.zcl import foundation from zigpy.zcl.clusters.general import PowerConfiguration @@ -17,9 +20,18 @@ ATTRIBUTE_NAME, CLUSTER_COMMAND, COMMAND_ATTRIBUTE_UPDATED, + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MANUFACTURER, + MODEL, + MODELS_INFO, MOTION_EVENT, + NODE_DESCRIPTOR, OFF, ON, + OUTPUT_CLUSTERS, + PROFILE_ID, UNKNOWN, VALUE, ZHA_SEND_EVENT, @@ -301,6 +313,52 @@ def _update_attribute(self, attrid, value): self._timer_handle = self._loop.call_later(self.reset_s, self._turn_off) +class QuickInitDevice(CustomDevice): + """Devices with quick initialization from quirk signature.""" + + signature: Optional[Dict[str, Any]] = None + + @classmethod + def from_signature( + cls, device: zigpy.device.Device, model: Optional[str] = None + ) -> zigpy.device.Device: + """Update device accordingly to quirk signature.""" + + assert isinstance(cls.signature, dict) + if model is None: + model = cls.signature[MODEL] + manufacturer = cls.signature.get(MANUFACTURER) + if manufacturer is None: + manufacturer = cls.signature[MODELS_INFO][0][0] + + device.node_desc = cls.signature[NODE_DESCRIPTOR] + + endpoints = cls.signature[ENDPOINTS] + for ep_id, ep_data in endpoints.items(): + endpoint = device.add_endpoint(ep_id) + endpoint.profile_id = ep_data[PROFILE_ID] + endpoint.device_type = ep_data[DEVICE_TYPE] + for cluster_id in ep_data[INPUT_CLUSTERS]: + cluster = endpoint.add_input_cluster(cluster_id) + if cluster.ep_attribute == "basic": + manuf_attr_id = cluster.attridx[MANUFACTURER] + cluster._update_attribute( # pylint: disable=W0212 + manuf_attr_id, manufacturer + ) + cluster._update_attribute( # pylint: disable=W0212 + cluster.attridx[MODEL], model + ) + for cluster_id in ep_data[OUTPUT_CLUSTERS]: + endpoint.add_output_cluster(cluster_id) + endpoint.status = zigpy.endpoint.Status.ZDO_INIT + + device.status = zigpy.device.Status.ENDPOINTS_INIT + device.manufacturer = manufacturer + device.model = model + + return device + + NAME = __name__ PATH = __path__ for importer, modname, ispkg in pkgutil.walk_packages(path=PATH, prefix=NAME + "."): diff --git a/zhaquirks/const.py b/zhaquirks/const.py index 72c9674f98..33a55403a9 100644 --- a/zhaquirks/const.py +++ b/zhaquirks/const.py @@ -1,4 +1,17 @@ """Common constants for zhaquirks.""" +from zigpy.quirks import ( + SIG_ENDPOINTS, + SIG_EP_INPUT, + SIG_EP_OUTPUT, + SIG_EP_PROFILE, + SIG_EP_TYPE, + SIG_MANUFACTURER, + SIG_MODEL, + SIG_MODELS_INFO, + SIG_NODE_DESC, + SIG_SKIP_CONFIG, +) + ARGS = "args" ATTR_ID = "attr_id" ATTRIBUTE_ID = "attribute_id" @@ -44,28 +57,30 @@ COMMAND_TOGGLE = "toggle" COMMAND_TRIPLE = "triple" DESCRIPTION = "description" -DEVICE_TYPE = "device_type" +DEVICE_TYPE = SIG_EP_TYPE DIM_DOWN = "dim_down" DIM_UP = "dim_up" DOUBLE_PRESS = "remote_button_double_press" ALT_DOUBLE_PRESS = "remote_button_alt_double_press" ENDPOINT_ID = "endpoint_id" -ENDPOINTS = "endpoints" -INPUT_CLUSTERS = "input_clusters" +ENDPOINTS = SIG_ENDPOINTS +INPUT_CLUSTERS = SIG_EP_INPUT LEFT = "left" LONG_PRESS = "remote_button_long_press" LONG_RELEASE = "remote_button_long_release" ALT_LONG_PRESS = "remote_button_alt_long_press" ALT_LONG_RELEASE = "remote_button_alt_long_release" -MODELS_INFO = "models_info" +MANUFACTURER = SIG_MANUFACTURER +MODEL = SIG_MODEL +MODELS_INFO = SIG_MODELS_INFO MOTION_EVENT = "motion_event" -NODE_DESCRIPTOR = "node_desc" +NODE_DESCRIPTOR = SIG_NODE_DESC OFF = 0 ON = 1 OPEN = "open" -OUTPUT_CLUSTERS = "output_clusters" +OUTPUT_CLUSTERS = SIG_EP_OUTPUT PRESS_TYPE = "press_type" -PROFILE_ID = "profile_id" +PROFILE_ID = SIG_EP_PROFILE QUADRUPLE_PRESS = "remote_button_quadruple_press" QUINTUPLE_PRESS = "remote_button_quintuple_press" RELATIVE_DEGREES = "relative_degrees" @@ -73,7 +88,7 @@ SHAKEN = "device_shaken" SHORT_PRESS = "remote_button_short_press" ALT_SHORT_PRESS = "remote_button_alt_short_press" -SKIP_CONFIGURATION = "skip_configuration" +SKIP_CONFIGURATION = SIG_SKIP_CONFIG SHORT_RELEASE = "remote_button_short_release" TRIPLE_PRESS = "remote_button_triple_press" TURN_OFF = "turn_off" diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index 7d17db7210..206fea6c6e 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -5,6 +5,7 @@ from typing import Optional, Union from zigpy import types as t +import zigpy.device from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import AnalogInput, Basic, OnOff, PowerConfiguration @@ -16,8 +17,10 @@ TemperatureMeasurement, ) import zigpy.zcl.foundation as foundation +import zigpy.zdo +from zigpy.zdo.types import NodeDescriptor -from .. import Bus, LocalDataCluster, MotionOnEvent, OccupancyWithReset +from .. import Bus, LocalDataCluster, MotionOnEvent, OccupancyWithReset, QuickInitDevice from ..const import ( ATTRIBUTE_ID, ATTRIBUTE_NAME, @@ -60,8 +63,20 @@ XIAOMI_ATTR_5 = "X-attrib-5" XIAOMI_ATTR_6 = "X-attrib-6" XIAOMI_MIJA_ATTRIBUTE = 0xFF02 +XIAOMI_NODE_DESC = NodeDescriptor( + byte1=2, + byte2=64, + mac_capability_flags=128, + manufacturer_code=4151, + maximum_buffer_size=127, + maximum_incoming_transfer_size=100, + server_mask=0, + maximum_outgoing_transfer_size=100, + descriptor_capability_field=0, +) ZONE_TYPE = 0x0001 + _LOGGER = logging.getLogger(__name__) @@ -76,6 +91,10 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +class XiaomiQuickInitDevice(XiaomiCustomDevice, QuickInitDevice): + """Xiaomi devices eligible for QuickInit.""" + + class BasicCluster(CustomCluster, Basic): """Xiaomi basic cluster implementation.""" @@ -469,3 +488,73 @@ def command( bytes([src_ep, tsn, command_id]), expect_reply=expect_reply, ) + + +def handle_quick_init( + sender: zigpy.device.Device, + profile: int, + cluster: int, + src_ep: int, + dst_ep: int, + message: bytes, +) -> Optional[bool]: + """Handle message from an uninitialized device which could be a xiaomi.""" + if src_ep == 0: + return + + hdr, data = foundation.ZCLHeader.deserialize(message) + sender.debug( + """Received ZCL while uninitialized on endpoint id %s, cluster 0x%04x """ + """id, hdr: %s, payload: %s""", + src_ep, + cluster, + hdr, + data, + ) + if hdr.frame_control.is_cluster: + return + + try: + schema = foundation.COMMANDS[hdr.command_id][0] + args, data = t.deserialize(data, schema) + except (KeyError, ValueError): + sender.debug("Failed to deserialize ZCL global command") + return + + sender.debug("Uninitialized device command '%s' args: %s", hdr.command_id, args) + if hdr.command_id != foundation.Command.Report_Attributes or cluster != 0: + return + + for attr_rec in args[0]: + if attr_rec.attrid == 5: + break + else: + return + + model = attr_rec.value.value + if not model: + return + + for quirk in zigpy.quirks.get_model_quirks(model): + if issubclass(quirk, XiaomiQuickInitDevice): + sender.debug("Found '%s' quirk for '%s' model", quirk.__name__, model) + try: + sender = quirk.from_signature(sender, model) + except (AssertionError, KeyError) as ex: + _LOGGER.debug( + "Found quirk for quick init, but failed to init: %s", str(ex) + ) + continue + break + else: + return + + sender.cancel_initialization() + sender.application.device_initialized(sender) + sender.info( + "Was quickly initialized from '%s.%s' quirk", quirk.__module__, quirk.__name__ + ) + return True + + +zigpy.quirks.register_uninitialized_device_message_handler(handle_quick_init) diff --git a/zhaquirks/xiaomi/aqara/cube.py b/zhaquirks/xiaomi/aqara/cube.py index c21f9e2216..c8e8eba425 100644 --- a/zhaquirks/xiaomi/aqara/cube.py +++ b/zhaquirks/xiaomi/aqara/cube.py @@ -11,7 +11,13 @@ Scenes, ) -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice +from .. import ( + LUMI, + XIAOMI_NODE_DESC, + BasicCluster, + PowerConfigurationCluster, + XiaomiQuickInitDevice, +) from ... import CustomCluster from ...const import ( ARGS, @@ -20,6 +26,7 @@ ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, SHAKEN, @@ -146,7 +153,7 @@ def extend_dict(dictionary, value, ranges): extend_dict(MOVEMENT_TYPE, FLIP, range(FLIP_BEGIN, FLIP_END)) -class Cube(XiaomiCustomDevice): +class Cube(XiaomiQuickInitDevice): """Aqara magic cube device.""" def __init__(self, *args, **kwargs): @@ -221,6 +228,7 @@ def _update_attribute(self, attrid, value): # input_clusters=[0, 3, 25, 18] # output_clusters=[0, 4, 3, 5, 25, 18]> MODELS_INFO: [(LUMI, "lumi.sensor_cube")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/xiaomi/aqara/magnet_aq2.py b/zhaquirks/xiaomi/aqara/magnet_aq2.py index c9ae8d3130..3bd15f8f0a 100644 --- a/zhaquirks/xiaomi/aqara/magnet_aq2.py +++ b/zhaquirks/xiaomi/aqara/magnet_aq2.py @@ -4,12 +4,19 @@ from zigpy.profiles import zha from zigpy.zcl.clusters.general import Groups, Identify, OnOff -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice +from .. import ( + LUMI, + XIAOMI_NODE_DESC, + BasicCluster, + PowerConfigurationCluster, + XiaomiQuickInitDevice, +) from ...const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, @@ -21,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) -class MagnetAQ2(XiaomiCustomDevice): +class MagnetAQ2(XiaomiQuickInitDevice): """Xiaomi contact sensor device.""" def __init__(self, *args, **kwargs): @@ -35,6 +42,7 @@ def __init__(self, *args, **kwargs): # input_clusters=[0, 3, 65535, 6] # output_clusters=[0, 4, 65535]> MODELS_INFO: [(LUMI, "lumi.sensor_magnet.aq2")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/xiaomi/aqara/motion_aq2.py b/zhaquirks/xiaomi/aqara/motion_aq2.py index 52b35a6e14..f979c45598 100644 --- a/zhaquirks/xiaomi/aqara/motion_aq2.py +++ b/zhaquirks/xiaomi/aqara/motion_aq2.py @@ -7,12 +7,13 @@ from .. import ( LUMI, + XIAOMI_NODE_DESC, BasicCluster, IlluminanceMeasurementCluster, MotionCluster, OccupancyCluster, PowerConfigurationCluster, - XiaomiCustomDevice, + XiaomiQuickInitDevice, ) from ... import Bus from ...const import ( @@ -20,6 +21,7 @@ ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, @@ -28,7 +30,7 @@ XIAOMI_CLUSTER_ID = 0xFFFF -class MotionAQ2(XiaomiCustomDevice): +class MotionAQ2(XiaomiQuickInitDevice): """Custom device representing aqara body sensors.""" def __init__(self, *args, **kwargs): @@ -44,6 +46,7 @@ def __init__(self, *args, **kwargs): # input_clusters=[0, 65535, 1030, 1024, 1280, 1, 3] # output_clusters=[0, 25]> MODELS_INFO: [(LUMI, "lumi.sensor_motion.aq2")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/xiaomi/aqara/remote_b186acn01.py b/zhaquirks/xiaomi/aqara/remote_b186acn01.py index c82ea927ed..bf632e870d 100644 --- a/zhaquirks/xiaomi/aqara/remote_b186acn01.py +++ b/zhaquirks/xiaomi/aqara/remote_b186acn01.py @@ -13,7 +13,13 @@ Scenes, ) -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice +from .. import ( + LUMI, + XIAOMI_NODE_DESC, + BasicCluster, + PowerConfigurationCluster, + XiaomiQuickInitDevice, +) from ... import CustomCluster from ...const import ( ATTR_ID, @@ -24,6 +30,7 @@ INPUT_CLUSTERS, LONG_PRESS, MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PRESS_TYPE, PROFILE_ID, @@ -46,7 +53,7 @@ _LOGGER = logging.getLogger(__name__) -class RemoteB186ACN01(XiaomiCustomDevice): +class RemoteB186ACN01(XiaomiQuickInitDevice): """Aqara single key switch device.""" class MultistateInputCluster(CustomCluster, MultistateInput): @@ -82,6 +89,7 @@ def _update_attribute(self, attrid, value): (LUMI, "lumi.remote.b186acn02"), (LUMI, "lumi.sensor_86sw1"), ], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/xiaomi/aqara/vibration_aq1.py b/zhaquirks/xiaomi/aqara/vibration_aq1.py index 234b60e68f..ee45bde02d 100644 --- a/zhaquirks/xiaomi/aqara/vibration_aq1.py +++ b/zhaquirks/xiaomi/aqara/vibration_aq1.py @@ -16,7 +16,13 @@ ) from zigpy.zcl.clusters.security import IasZone -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice +from .. import ( + LUMI, + XIAOMI_NODE_DESC, + BasicCluster, + PowerConfigurationCluster, + XiaomiQuickInitDevice, +) from ... import Bus, LocalDataCluster from ...const import ( CLUSTER_COMMAND, @@ -29,6 +35,7 @@ INPUT_CLUSTERS, MODELS_INFO, MOTION_EVENT, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, @@ -58,7 +65,7 @@ _LOGGER = logging.getLogger(__name__) -class VibrationAQ1(XiaomiCustomDevice): +class VibrationAQ1(XiaomiQuickInitDevice): """Xiaomi aqara smart motion sensor device.""" manufacturer_id_override = 0x115F @@ -153,6 +160,7 @@ def _turn_off(self): signature = { MODELS_INFO: [(LUMI, "lumi.vibration.aq1")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/xiaomi/aqara/weather.py b/zhaquirks/xiaomi/aqara/weather.py index 1551d7e93d..33ba4e3bed 100644 --- a/zhaquirks/xiaomi/aqara/weather.py +++ b/zhaquirks/xiaomi/aqara/weather.py @@ -7,12 +7,13 @@ from .. import ( LUMI, + XIAOMI_NODE_DESC, BasicCluster, PowerConfigurationCluster, PressureMeasurementCluster, RelativeHumidityCluster, TemperatureMeasurementCluster, - XiaomiCustomDevice, + XiaomiQuickInitDevice, ) from ... import Bus from ...const import ( @@ -20,6 +21,7 @@ ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, @@ -31,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) -class Weather(XiaomiCustomDevice): +class Weather(XiaomiQuickInitDevice): """Xiaomi weather sensor device.""" def __init__(self, *args, **kwargs): @@ -47,6 +49,7 @@ def __init__(self, *args, **kwargs): # input_clusters=[0, 3, 65535, 1026, 1027, 1029] # output_clusters=[0, 4, 65535]> MODELS_INFO: [(LUMI, "lumi.weather")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, @@ -100,6 +103,7 @@ class Weather2(Weather): # input_clusters=[0, 3, 65535, 1026, 1027, 1029] # output_clusters=[0, 4, 65535]> MODELS_INFO: [(LUMI, "lumi.weather")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/xiaomi/aqara/wleak_aq1.py b/zhaquirks/xiaomi/aqara/wleak_aq1.py index fe514105e1..398d321ba1 100644 --- a/zhaquirks/xiaomi/aqara/wleak_aq1.py +++ b/zhaquirks/xiaomi/aqara/wleak_aq1.py @@ -4,12 +4,19 @@ from zigpy.zcl.clusters.general import Identify, Ota from zigpy.zcl.clusters.security import IasZone -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice +from .. import ( + LUMI, + XIAOMI_NODE_DESC, + BasicCluster, + PowerConfigurationCluster, + XiaomiQuickInitDevice, +) from ...const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, @@ -29,7 +36,7 @@ def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) -class LeakAQ1(XiaomiCustomDevice): +class LeakAQ1(XiaomiQuickInitDevice): """Xiaomi aqara leak sensor device.""" signature = { @@ -38,6 +45,7 @@ class LeakAQ1(XiaomiCustomDevice): # input_clusters=[0, 3, 1] # output_clusters=[25]> MODELS_INFO: [(LUMI, "lumi.sensor_wleak.aq1")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/xiaomi/mija/motion.py b/zhaquirks/xiaomi/mija/motion.py index afd9580f7c..5d4628242b 100644 --- a/zhaquirks/xiaomi/mija/motion.py +++ b/zhaquirks/xiaomi/mija/motion.py @@ -14,11 +14,12 @@ from .. import ( LUMI, + XIAOMI_NODE_DESC, BasicCluster, MotionCluster, OccupancyCluster, PowerConfigurationCluster, - XiaomiCustomDevice, + XiaomiQuickInitDevice, ) from ... import Bus from ...const import ( @@ -26,6 +27,7 @@ ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, @@ -35,7 +37,7 @@ _LOGGER = logging.getLogger(__name__) -class Motion(XiaomiCustomDevice): +class Motion(XiaomiQuickInitDevice): """Custom device representing mija body sensors.""" def __init__(self, *args, **kwargs): @@ -50,6 +52,7 @@ def __init__(self, *args, **kwargs): # input_clusters=[0, 65535, 3, 25] # output_clusters=[0, 3, 4, 5, 6, 8, 25]> MODELS_INFO: [(LUMI, "lumi.sensor_motion")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/xiaomi/mija/sensor_magnet.py b/zhaquirks/xiaomi/mija/sensor_magnet.py index 77c0d09f0d..587e79857b 100644 --- a/zhaquirks/xiaomi/mija/sensor_magnet.py +++ b/zhaquirks/xiaomi/mija/sensor_magnet.py @@ -11,12 +11,19 @@ Scenes, ) -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice +from .. import ( + LUMI, + XIAOMI_NODE_DESC, + BasicCluster, + PowerConfigurationCluster, + XiaomiQuickInitDevice, +) from ...const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, @@ -28,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) -class Magnet(XiaomiCustomDevice): +class Magnet(XiaomiQuickInitDevice): """Xiaomi mija contact sensor device.""" def __init__(self, *args, **kwargs): @@ -42,6 +49,7 @@ def __init__(self, *args, **kwargs): # input_clusters=[0, 3, 65535, 25] # output_clusters=[0, 4, 3, 6, 8, 5, 25]> MODELS_INFO: [(LUMI, "lumi.sensor_magnet")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/xiaomi/mija/smoke.py b/zhaquirks/xiaomi/mija/smoke.py index f98d03bd7e..fa3c83108e 100644 --- a/zhaquirks/xiaomi/mija/smoke.py +++ b/zhaquirks/xiaomi/mija/smoke.py @@ -25,12 +25,20 @@ ) from zigpy.zcl.clusters.security import IasZone -from .. import BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice +from .. import ( + LUMI, + XIAOMI_NODE_DESC, + BasicCluster, + PowerConfigurationCluster, + XiaomiQuickInitDevice, +) from ... import CustomCluster from ...const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, + MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, @@ -50,7 +58,7 @@ class XiaomiSmokeIASCluster(CustomCluster, IasZone): } -class MijiaHoneywellSmokeDetectorSensor(XiaomiCustomDevice): +class MijiaHoneywellSmokeDetectorSensor(XiaomiQuickInitDevice): """MijiaHoneywellSmokeDetectorSensor custom device.""" def __init__(self, *args, **kwargs): @@ -63,6 +71,8 @@ def __init__(self, *args, **kwargs): # device_version= # input_clusters=[0, 1, 3, 12, 18, 1280] # output_clusters=[25]> + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, + MODELS_INFO: ((LUMI, "lumi.sensor_smoke"),), ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, @@ -77,7 +87,7 @@ def __init__(self, *args, **kwargs): ], OUTPUT_CLUSTERS: [Ota.cluster_id], } - } + }, } replacement = { From 925dd017c1353b961836e8d78fd142548920bae1 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 24 Nov 2020 19:04:26 -0500 Subject: [PATCH 103/113] Fix consumption reporting for Lumi plugs (#587) --- zhaquirks/xiaomi/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index 206fea6c6e..747014e78a 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -419,6 +419,12 @@ class ElectricalMeasurementCluster(LocalDataCluster, ElectricalMeasurement): POWER_ID = 0x050B VOLTAGE_ID = 0x0500 CONSUMPTION_ID = 0x0304 + _CONSTANT_ATTRIBUTES = { + 0x0402: 1, # power_multiplier + 0x0403: 1, # power_divisor + 0x0604: 1, # ac_power_multiplier + 0x0605: 1, # ac_power_divisor + } def __init__(self, *args, **kwargs): """Init.""" From 0bc72887ebb122f804015e06275c195983ebd47d Mon Sep 17 00:00:00 2001 From: glassbase Date: Tue, 24 Nov 2020 19:07:59 -0500 Subject: [PATCH 104/113] =?UTF-8?q?Add=20LST001=20Light=20Strip,=20Expose?= =?UTF-8?q?=20PhilipsColorCluster=20and=20PhilipsLevelCo=E2=80=A6=20(#573)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add LST001 Light Strip, Expose PhilipsColorCluster and PhilipsLevelControlCluster for both LST00x LST001 device signature: ``` { "node_descriptor": "NodeDescriptor(byte1=1, byte2=64, mac_capability_flags=142, manufacturer_code=4107, maximum_buffer_size=80, maximum_incoming_transfer_size=80, server_mask=0, maximum_outgoing_transfer_size=80, descriptor_capability_field=0)", "endpoints": { "11": { "profile_id": 49246, "device_type": "0x0210", "in_clusters": [ "0x0000", "0x0003", "0x0004", "0x0005", "0x0006", "0x0008", "0x0300", "0x1000", "0xfc01" ], "out_clusters": [ "0x0019" ] }, "242": { "profile_id": 41440, "device_type": "0x0061", "in_clusters": [ "0x0021" ], "out_clusters": [ "0x0021" ] } }, "manufacturer": "Philips", "model": "LST001", "class": "zhaquirks.philips.lst001.PhilipsLST001" } ``` * Fix spacing --- zhaquirks/philips/{lst002.py => lst.py} | 80 +++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) rename zhaquirks/philips/{lst002.py => lst.py} (52%) diff --git a/zhaquirks/philips/lst002.py b/zhaquirks/philips/lst.py similarity index 52% rename from zhaquirks/philips/lst002.py rename to zhaquirks/philips/lst.py index 5c23990562..189c45b242 100644 --- a/zhaquirks/philips/lst002.py +++ b/zhaquirks/philips/lst.py @@ -1,4 +1,4 @@ -"""Quirk for Phillips LST002.""" +"""Quirk for Phillips LST001 and LST002 Light Strips.""" from zigpy.profiles import zll from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( @@ -22,7 +22,79 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster +from zhaquirks.philips import ( + PHILIPS, + PhilipsColorCluster, + PhilipsLevelControlCluster, + PhilipsOnOffCluster, +) + + +class PhilipsLST001(CustomDevice): + """Philips LST001 device.""" + + signature = { + MODELS_INFO: [(PHILIPS, "LST001")], + ENDPOINTS: { + 11: { + # Date: Tue, 24 Nov 2020 19:08:54 -0500 Subject: [PATCH 105/113] Add LLC012 Bloom, Expose PhilipsLevelControlCluster and PhilipsColorCluster (#574) LLC012 device signature: ``` { "node_descriptor": "NodeDescriptor(byte1=1, byte2=64, mac_capability_flags=142, manufacturer_code=4107, maximum_buffer_size=80, maximum_incoming_transfer_size=80, server_mask=0, maximum_outgoing_transfer_size=80, descriptor_capability_field=0)", "endpoints": { "11": { "profile_id": 49246, "device_type": "0x0200", "in_clusters": [ "0x0000", "0x0003", "0x0004", "0x0005", "0x0006", "0x0008", "0x0300", "0x1000", "0xfc01" ], "out_clusters": [ "0x0019" ] }, "242": { "profile_id": 41440, "device_type": "0x0061", "in_clusters": [ "0x0021" ], "out_clusters": [ "0x0021" ] } }, "manufacturer": "Philips", "model": "LLC012", "class": "zhaquirks.philips.llcbloom.PhilipsLLCBloom" } ``` --- zhaquirks/philips/{llc011.py => llcbloom.py} | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) rename zhaquirks/philips/{llc011.py => llcbloom.py} (85%) diff --git a/zhaquirks/philips/llc011.py b/zhaquirks/philips/llcbloom.py similarity index 85% rename from zhaquirks/philips/llc011.py rename to zhaquirks/philips/llcbloom.py index 1e23f32b39..c13ced1a35 100644 --- a/zhaquirks/philips/llc011.py +++ b/zhaquirks/philips/llcbloom.py @@ -1,4 +1,4 @@ -"""Quirk for Phillips Hue LivingColors Bloom.""" +"""Quirk for Phillips Hue LivingColors Bloom LLC011 and LLC012.""" from zigpy.profiles import zll from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import ( @@ -22,14 +22,19 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.philips import PHILIPS, PhilipsOnOffCluster +from zhaquirks.philips import ( + PHILIPS, + PhilipsColorCluster, + PhilipsLevelControlCluster, + PhilipsOnOffCluster, +) -class PhilipsLLC011(CustomDevice): - """Philips LLC device.""" +class PhilipsLLCBloom(CustomDevice): + """Philips LLC Bloom device.""" signature = { - MODELS_INFO: [(PHILIPS, "LLC011")], + MODELS_INFO: [(PHILIPS, "LLC011"), (PHILIPS, "LLC012")], ENDPOINTS: { 11: { # Date: Tue, 24 Nov 2020 19:12:35 -0500 Subject: [PATCH 106/113] Add iMagic by GreatStar 1116-S quirk (#588) * Add iMagic by GreatStar 11116-S quirk * Add to correct location Wasn't on the latest version of zha-device-handlers, didn't see there was already an imagic folder. --- zhaquirks/imagic/1116s.py | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 zhaquirks/imagic/1116s.py diff --git a/zhaquirks/imagic/1116s.py b/zhaquirks/imagic/1116s.py new file mode 100644 index 0000000000..978c366db0 --- /dev/null +++ b/zhaquirks/imagic/1116s.py @@ -0,0 +1,71 @@ +"""Device handler for GreatStar iMagic 1116-S sensor.""" +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters.general import Basic, Identify, Ota, PollControl +from zigpy.zcl.clusters.measurement import TemperatureMeasurement +from zigpy.zcl.clusters.security import IasZone + +from zhaquirks import PowerConfigurationCluster + +from . import IMAGIC +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +DIAGNOSTICS_CLUSTER_ID = 0x0B05 # decimal = 2821 +MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFC01 # decimal = 64513 +MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC02 # decimal = 64514 + + +class iMagic1116(CustomDevice): + """Custom device representing iMagic 1116-S sensor.""" + + signature = { + # + MODELS_INFO: [(IMAGIC, "1116-S")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfigurationCluster.cluster_id, + Identify.cluster_id, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + MANUFACTURER_SPECIFIC_CLUSTER_ID_2, + PollControl.cluster_id, + TemperatureMeasurement.cluster_id, + IasZone.cluster_id, + DIAGNOSTICS_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Identify.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfigurationCluster, + Identify.cluster_id, + MANUFACTURER_SPECIFIC_CLUSTER_ID, + MANUFACTURER_SPECIFIC_CLUSTER_ID_2, + PollControl.cluster_id, + TemperatureMeasurement.cluster_id, + IasZone.cluster_id, + DIAGNOSTICS_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [Ota.cluster_id, Identify.cluster_id], + }, + } + } From 1fd9bf2670bddfcf02aa47783a8ed9c124cfb07a Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 25 Nov 2020 01:13:22 +0100 Subject: [PATCH 107/113] Philips Hue Lightstrip (v4/Bluetooth) support, Hue GU10 Spots (Bluetooth) support (#579) --- zhaquirks/philips/zhaextendedcolorlight.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zhaquirks/philips/zhaextendedcolorlight.py b/zhaquirks/philips/zhaextendedcolorlight.py index d84b1073d8..fc744f9492 100644 --- a/zhaquirks/philips/zhaextendedcolorlight.py +++ b/zhaquirks/philips/zhaextendedcolorlight.py @@ -39,6 +39,7 @@ class ZHAExtendedColorLight(CustomDevice): (PHILIPS, "LCA003"), (PHILIPS, "LCB001"), (PHILIPS, "LCE002"), + (PHILIPS, "LCG002"), ], ENDPOINTS: { 11: { @@ -109,6 +110,7 @@ class ZHAExtendedColorLight2(CustomDevice): signature = { MODELS_INFO: [ + (PHILIPS, "LCL001"), (PHILIPS, "LCT026"), (PHILIPS, "929002376001"), (PHILIPS, "929002375901"), From d72854e4a6b823a75946124ae4453886e85f96dd Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 25 Nov 2020 10:47:48 -0500 Subject: [PATCH 108/113] Add tests to enforce signature/replacement content (#598) * Update tests to validate signature and replacement data * Cleanup quirks * Update quirk tests to accept None for model or manufacturer --- tests/test_quirks.py | 114 +++++++++++++++++++++++++++ zhaquirks/ikea/opencloseremote.py | 1 - zhaquirks/lutron/lzl4bwhl01remote.py | 18 +---- zhaquirks/salus/sp600.py | 1 - 4 files changed, 118 insertions(+), 16 deletions(-) diff --git a/tests/test_quirks.py b/tests/test_quirks.py index 76cf12864d..0f4c450c44 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -8,6 +8,8 @@ import zigpy.profiles import zigpy.quirks as zq import zigpy.types +import zigpy.zcl as zcl +import zigpy.zdo.types import zhaquirks # noqa: F401, E402 from zhaquirks.const import ( @@ -20,6 +22,7 @@ NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, + SKIP_CONFIGURATION, ) from zhaquirks.xiaomi import XIAOMI_NODE_DESC @@ -31,6 +34,30 @@ continue ALL_QUIRK_CLASSES.append(quirk) +del quirk, model_quirk_list, manufacturer + + +SIGNATURE_ALLOWED = { + ENDPOINTS, + MANUFACTURER, + MODEL, + MODELS_INFO, + NODE_DESCRIPTOR, +} +SIGNATURE_EP_ALLOWED = { + DEVICE_TYPE, + INPUT_CLUSTERS, + PROFILE_ID, + OUTPUT_CLUSTERS, +} +SIGNATURE_REPLACEMENT_ALLOWED = { + ENDPOINTS, + MANUFACTURER, + MODEL, + NODE_DESCRIPTOR, + SKIP_CONFIGURATION, +} + @pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES) def test_quirk_replacements(quirk): @@ -239,3 +266,90 @@ def test_quirk_quickinit(quirk): assert DEVICE_TYPE in ep_data assert isinstance(ep_data[INPUT_CLUSTERS], list) assert isinstance(ep_data[OUTPUT_CLUSTERS], list) + + +@pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES) +def test_signature(quirk): + """Make sure signature look sane for all custom devices.""" + + def _check_range(cluster): + for range in zcl.Cluster._registry_range.keys(): + if range[0] <= cluster <= range[1]: + return True + return False + + # enforce new style of signature + assert ENDPOINTS in quirk.signature + numeric = [eid for eid in quirk.signature if isinstance(eid, int)] + assert not numeric + assert set(quirk.signature).issubset(SIGNATURE_ALLOWED) + models_info = quirk.signature.get(MODELS_INFO) + if models_info is not None: + for manufacturer, model in models_info: + if manufacturer is not None: + assert isinstance(manufacturer, str) + assert manufacturer + if model is not None: + assert isinstance(model, str) + assert model + + for m_m in (MANUFACTURER, MODEL): + value = quirk.signature.get(m_m) + if value is not None: + assert isinstance(value, str) + assert value + + # Check that the signature data is OK + ep_signature = quirk.signature[ENDPOINTS] + for ep_id, ep_data in ep_signature.items(): + assert isinstance(ep_id, int) + assert 0x01 <= ep_id <= 0xFE + assert set(ep_data).issubset(SIGNATURE_EP_ALLOWED) + for sig_attr in (DEVICE_TYPE, PROFILE_ID): + value = ep_data.get(sig_attr) + if value is not None: + assert isinstance(value, int) + assert 0x0000 <= value <= 0xFFFF + for clusters_type in (INPUT_CLUSTERS, OUTPUT_CLUSTERS): + clusters = ep_data.get(clusters_type) + if clusters is not None: + assert all((isinstance(cluster_id, int) for cluster_id in clusters)) + assert all((0 <= cluster_id <= 0xFFFF for cluster_id in clusters)) + + for m_m in (MANUFACTURER, MODEL): + value = ep_data.get(m_m) + if value is not None: + assert isinstance(value, str) + assert value + + # Check that the replacement data is OK + assert set(quirk.replacement).issubset(SIGNATURE_REPLACEMENT_ALLOWED) + for ep_id, ep_data in quirk.replacement[ENDPOINTS].items(): + assert isinstance(ep_id, int) + assert 0x01 <= ep_id <= 0xFE + assert set(ep_data).issubset(SIGNATURE_EP_ALLOWED) + + for sig_attr in (DEVICE_TYPE, PROFILE_ID): + value = ep_data.get(sig_attr) + if value is not None: + assert isinstance(value, int) + assert 0x0000 <= value <= 0xFFFF + + for clusters_type in (INPUT_CLUSTERS, OUTPUT_CLUSTERS): + clusters = ep_data.get(clusters_type, []) + for cluster in clusters: + if clusters is not None: + if isinstance(cluster, int): + assert cluster in zcl.Cluster._registry or _check_range(cluster) + else: + assert issubclass(cluster, zcl.Cluster) + + for m_m in (MANUFACTURER, MODEL): + value = ep_data.get(m_m) + if value is not None: + assert isinstance(value, str) + assert value + + node_desc = ep_data.get(NODE_DESCRIPTOR) + if node_desc is not None: + assert isinstance(node_desc, zigpy.zdo.types.NodeDescriptor) diff --git a/zhaquirks/ikea/opencloseremote.py b/zhaquirks/ikea/opencloseremote.py index a5f21493d6..fa0834e2ce 100644 --- a/zhaquirks/ikea/opencloseremote.py +++ b/zhaquirks/ikea/opencloseremote.py @@ -108,7 +108,6 @@ class IkeaTradfriOpenCloseRemote(CustomDevice): } replacement = { - MODELS_INFO: [(IKEA, "TRADFRI open/close remote")], ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, diff --git a/zhaquirks/lutron/lzl4bwhl01remote.py b/zhaquirks/lutron/lzl4bwhl01remote.py index 679d5800c9..2d7e70d218 100644 --- a/zhaquirks/lutron/lzl4bwhl01remote.py +++ b/zhaquirks/lutron/lzl4bwhl01remote.py @@ -58,7 +58,10 @@ class LutronLZL4BWHL01Remote(CustomDevice): # device_version=2 # input_clusters=[0, 4096, 65280, 64580] # output_clusters=[4096, 3, 6, 8, 4, 5, 0, 65280]> - MODELS_INFO: [("Lutron", "LZL4BWHL01 Remote")], + MODELS_INFO: [ + ("Lutron", "LZL4BWHL01 Remote"), + (" Lutron", "LZL4BWHL01 Remote"), + ], ENDPOINTS: { 1: { PROFILE_ID: zll.PROFILE_ID, @@ -134,16 +137,3 @@ class LutronLZL4BWHL01Remote(CustomDevice): ARGS: [1, 30, 6], }, } - - -class LutronLZL4BWHL01Remote2(LutronLZL4BWHL01Remote): - """Custom device representing Lutron LZL4BWHL01 Remote.""" - - signature = { - ENDPOINTS: { - 1: { - **LutronLZL4BWHL01Remote.signature["endpoints"][1], - "manufacturer": " Lutron", # Some remotes report this - } - } - } diff --git a/zhaquirks/salus/sp600.py b/zhaquirks/salus/sp600.py index b31c9884ef..28ea9cdf36 100644 --- a/zhaquirks/salus/sp600.py +++ b/zhaquirks/salus/sp600.py @@ -92,5 +92,4 @@ class SP600(CustomDevice): OUTPUT_CLUSTERS: [Ota.cluster_id], } }, - MODELS_INFO: [(COMPUTIME, MODEL)], } From 0fe1ab9c41aaf1f343f2a9adbdad202e71b28bc1 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 25 Nov 2020 11:18:48 -0500 Subject: [PATCH 109/113] Add lumi.sensor_switch.aq2 to quick init (#601) --- tests/test_quirks.py | 4 +++- zhaquirks/xiaomi/aqara/switch_aq2.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/test_quirks.py b/tests/test_quirks.py index 0f4c450c44..2148b19ea0 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -250,7 +250,9 @@ class QuirkDevice(zhaquirks.QuickInitDevice): ] -@pytest.mark.parametrize("quirk", ALL_QUIRK_CLASSES) +@pytest.mark.parametrize( + "quirk", (q for q in ALL_QUIRK_CLASSES if issubclass(q, zhaquirks.QuickInitDevice)) +) def test_quirk_quickinit(quirk): """Make sure signature in QuickInit Devices have all required attributes.""" diff --git a/zhaquirks/xiaomi/aqara/switch_aq2.py b/zhaquirks/xiaomi/aqara/switch_aq2.py index b251fa940c..859e9e3180 100644 --- a/zhaquirks/xiaomi/aqara/switch_aq2.py +++ b/zhaquirks/xiaomi/aqara/switch_aq2.py @@ -4,7 +4,13 @@ from zigpy.profiles import zha from zigpy.zcl.clusters.general import Basic, Groups, OnOff -from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice +from .. import ( + LUMI, + XIAOMI_NODE_DESC, + BasicCluster, + PowerConfigurationCluster, + XiaomiQuickInitDevice, +) from ...const import ( ARGS, ATTRIBUTE_ID, @@ -19,6 +25,7 @@ ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, + NODE_DESCRIPTOR, OUTPUT_CLUSTERS, PROFILE_ID, QUADRUPLE_PRESS, @@ -36,7 +43,7 @@ _LOGGER = logging.getLogger(__name__) -class SwitchAQ2(XiaomiCustomDevice): +class SwitchAQ2(XiaomiQuickInitDevice): """Aqara button device.""" signature = { @@ -45,6 +52,7 @@ class SwitchAQ2(XiaomiCustomDevice): # input_clusters=[0, 6, 65535] # output_clusters=[0, 4, 65535]> MODELS_INFO: [(LUMI, "lumi.sensor_switch.aq2")], + NODE_DESCRIPTOR: XIAOMI_NODE_DESC, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, From c0d7895ba3c9f2ea37ca14ac3c3793c56e1dff8b Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 25 Nov 2020 11:36:16 -0500 Subject: [PATCH 110/113] Cleanup tests of pytest.mark.asyncio marks (#602) --- tests/test_konke.py | 1 - tests/test_orvibo.py | 1 - tests/test_xiaomi.py | 1 - 3 files changed, 3 deletions(-) diff --git a/tests/test_konke.py b/tests/test_konke.py index e1a2d2e2d7..cc05c873f9 100644 --- a/tests/test_konke.py +++ b/tests/test_konke.py @@ -11,7 +11,6 @@ from tests.common import ZCL_IAS_MOTION_COMMAND, ClusterListener -@pytest.mark.asyncio @pytest.mark.parametrize( "quirk", (zhaquirks.konke.motion.KonkeMotion, zhaquirks.konke.motion.KonkeMotionB) ) diff --git a/tests/test_orvibo.py b/tests/test_orvibo.py index e2a74ddd7e..9c870f0305 100644 --- a/tests/test_orvibo.py +++ b/tests/test_orvibo.py @@ -11,7 +11,6 @@ from tests.common import ZCL_IAS_MOTION_COMMAND, ClusterListener -@pytest.mark.asyncio @pytest.mark.parametrize("quirk", (zhaquirks.orvibo.motion.SN10ZW,)) async def test_konke_motion(zigpy_device_from_quirk, quirk): """Test Orvibo motion sensor.""" diff --git a/tests/test_xiaomi.py b/tests/test_xiaomi.py index 88cf77da32..07c4fc2f74 100644 --- a/tests/test_xiaomi.py +++ b/tests/test_xiaomi.py @@ -59,7 +59,6 @@ def test_basic_cluster_deserialize_wrong_len_2(): assert deserialized[1] -@pytest.mark.asyncio @pytest.mark.parametrize( "quirk", ( From ca9ce604a993be11be5393372cecc3ed3c3ef709 Mon Sep 17 00:00:00 2001 From: Ignacio Larrain Date: Thu, 26 Nov 2020 14:07:46 -0300 Subject: [PATCH 111/113] Terncy: Add command schemas and filter repeated click events (#580) Fixes command data deserialization and repeated click events generated by some devices (for single action). --- zhaquirks/terncy/__init__.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/zhaquirks/terncy/__init__.py b/zhaquirks/terncy/__init__.py index 606fbd6185..bbd29d197d 100644 --- a/zhaquirks/terncy/__init__.py +++ b/zhaquirks/terncy/__init__.py @@ -1,7 +1,9 @@ """Module for Terncy quirks.""" +from collections import deque import math from zigpy.quirks import CustomCluster +import zigpy.types as t from zigpy.zcl.clusters.measurement import ( IlluminanceMeasurement, TemperatureMeasurement, @@ -120,14 +122,26 @@ class TerncyRawCluster(CustomCluster): cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID name = "Terncy Raw cluster" - # ep_attribute = "accelerometer" + + manufacturer_client_commands = { + 0: ("click_event", (t.uint8_t, t.uint8_t), False), + 4: ("motion_event", (t.uint8_t, t.uint8_t, t.uint8_t), False), + } + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self._last_clicks = deque(maxlen=10) def handle_cluster_request(self, tsn, command_id, args): """Handle a cluster command received on this cluster.""" - args = list(args) if command_id == 0: # click event count = args[0] state = args[1] + if (state, count) in self._last_clicks: + return # ignore repeated event for single action. + else: + self._last_clicks.append((state, count)) if state > 5: state = 5 event_args = {PRESS_TYPE: CLICK_TYPES[state], "count": count, VALUE: state} From 17372452f6802c3ae846c36f82545254a4e5151d Mon Sep 17 00:00:00 2001 From: "Julien \"_FrnchFrgg_\" Rivaud" Date: Fri, 27 Nov 2020 21:14:09 +0100 Subject: [PATCH 112/113] Thermostat (#538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Prepare support for tuya-based thermostats * Implement support for tuya-based thermostatic valves * Siterwell GS361 * Revolt NX4914-944 * Moes HY638 Co-Authored-By: Loïc * Introduce support for Moes BHT-002GCLZB Co-authored-by: Loïc --- tests/test_tuya.py | 427 ++++++++++++++++++++++++++++- zhaquirks/tuya/__init__.py | 192 ++++++++++++- zhaquirks/tuya/electric_heating.py | 172 ++++++++++++ zhaquirks/tuya/valve.py | 207 ++++++++++++++ 4 files changed, 984 insertions(+), 14 deletions(-) create mode 100644 zhaquirks/tuya/electric_heating.py create mode 100644 zhaquirks/tuya/valve.py diff --git a/tests/test_tuya.py b/tests/test_tuya.py index 28dd2f43ab..8a809796f2 100644 --- a/tests/test_tuya.py +++ b/tests/test_tuya.py @@ -4,16 +4,45 @@ from unittest import mock import pytest +from zigpy.profiles import zha +from zigpy.quirks import CustomDevice +import zigpy.types as t from zigpy.zcl import foundation -from zhaquirks.const import OFF, ON, ZONE_STATE +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OFF, + ON, + OUTPUT_CLUSTERS, + PROFILE_ID, + ZONE_STATE, +) +from zhaquirks.tuya import Data, TuyaManufClusterAttributes +import zhaquirks.tuya.electric_heating import zhaquirks.tuya.motion +import zhaquirks.tuya.siren +import zhaquirks.tuya.valve from tests.common import ClusterListener ZCL_TUYA_MOTION = b"\tL\x01\x00\x05\x03\x04\x00\x01\x02" ZCL_TUYA_SWITCH_ON = b"\tQ\x02\x006\x01\x01\x00\x01\x01" ZCL_TUYA_SWITCH_OFF = b"\tQ\x02\x006\x01\x01\x00\x01\x00" +ZCL_TUYA_ATTRIBUTE_617_TO_179 = b"\tp\x02\x00\x02i\x02\x00\x04\x00\x00\x00\xb3" +ZCL_TUYA_SIREN_TEMPERATURE = ZCL_TUYA_ATTRIBUTE_617_TO_179 +ZCL_TUYA_SIREN_HUMIDITY = b"\tp\x02\x00\x02j\x02\x00\x04\x00\x00\x00U" +ZCL_TUYA_SIREN_ON = b"\t\t\x02\x00\x04h\x01\x00\x01\x01" +ZCL_TUYA_SIREN_OFF = b"\t\t\x02\x00\x04h\x01\x00\x01\x00" +ZCL_TUYA_VALVE_TEMPERATURE = b"\tp\x02\x00\x02\x03\x02\x00\x04\x00\x00\x00\xb3" +ZCL_TUYA_VALVE_TARGET_TEMP = b"\t3\x01\x03\x05\x02\x02\x00\x04\x00\x00\x002" +ZCL_TUYA_VALVE_OFF = b"\t2\x01\x03\x04\x04\x04\x00\x01\x00" +ZCL_TUYA_VALVE_SCHEDULE = b"\t2\x01\x03\x04\x04\x04\x00\x01\x01" +ZCL_TUYA_VALVE_MANUAL = b"\t2\x01\x03\x04\x04\x04\x00\x01\x02" +ZCL_TUYA_EHEAT_TEMPERATURE = b"\tp\x02\x00\x02\x18\x02\x00\x04\x00\x00\x00\xb3" +ZCL_TUYA_EHEAT_TARGET_TEMP = b"\t3\x01\x03\x05\x10\x02\x00\x04\x00\x00\x00\x15" @pytest.mark.parametrize("quirk", (zhaquirks.tuya.motion.TuyaMotion,)) @@ -103,3 +132,399 @@ async def test_singleswitch_requests(zigpy_device_from_quirk, quirk): status = switch_cluster.command(0x0002) assert status == foundation.Status.UNSUP_CLUSTER_COMMAND + + +async def test_tuya_data_conversion(): + """Test tuya conversion from Data to ztype and reverse.""" + assert Data([4, 0, 0, 1, 39]).to_value(t.uint32_t) == 295 + assert Data([4, 0, 0, 0, 220]).to_value(t.uint32_t) == 220 + assert Data([4, 255, 255, 255, 236]).to_value(t.int32s) == -20 + assert Data.from_value(t.uint32_t(295)) == [4, 0, 0, 1, 39] + assert Data.from_value(t.uint32_t(220)) == [4, 0, 0, 0, 220] + assert Data.from_value(t.int32s(-20)) == [4, 255, 255, 255, 236] + + +class TestManufCluster(TuyaManufClusterAttributes): + """Cluster for synthetic tests.""" + + manufacturer_attributes = {617: ("test_attribute", t.uint32_t)} + + +class TestDevice(CustomDevice): + """Device for synthetic tests.""" + + signature = { + MODELS_INFO: [("_test_manuf", "_test_device")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [TestManufCluster.cluster_id], + OUTPUT_CLUSTERS: [], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [TestManufCluster], + OUTPUT_CLUSTERS: [], + } + }, + } + + +@pytest.mark.parametrize("quirk", (TestDevice,)) +async def test_tuya_receive_attribute(zigpy_device_from_quirk, quirk): + """Test conversion of tuya commands to attributes.""" + + test_dev = zigpy_device_from_quirk(quirk) + tuya_cluster = test_dev.endpoints[1].tuya_manufacturer + listener = ClusterListener(tuya_cluster) + + hdr, args = tuya_cluster.deserialize(ZCL_TUYA_ATTRIBUTE_617_TO_179) + tuya_cluster.handle_message(hdr, args) + + assert len(listener.attribute_updates) == 1 + assert listener.attribute_updates[0][0] == 617 + assert listener.attribute_updates[0][1] == 179 + + +@pytest.mark.parametrize("quirk", (TestDevice,)) +async def test_tuya_send_attribute(zigpy_device_from_quirk, quirk): + """Test conversion of attributes to tuya commands.""" + + test_dev = zigpy_device_from_quirk(quirk) + tuya_cluster = test_dev.endpoints[1].tuya_manufacturer + + async def async_success(*args, **kwargs): + return foundation.Status.SUCCESS + + with mock.patch.object( + tuya_cluster.endpoint, "request", side_effect=async_success + ) as m1: + + status = await tuya_cluster.write_attributes({617: 179}) + m1.assert_called_with( + 61184, + 1, + b"\x01\x01\x00\x00\x01i\x02\x00\x04\x00\x00\x00\xb3", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.siren.TuyaSiren,)) +async def test_siren_state_report(zigpy_device_from_quirk, quirk): + """Test tuya siren standard state reporting from incoming commands.""" + + siren_dev = zigpy_device_from_quirk(quirk) + tuya_cluster = siren_dev.endpoints[1].tuya_manufacturer + + temp_listener = ClusterListener(siren_dev.endpoints[1].temperature) + humid_listener = ClusterListener(siren_dev.endpoints[1].humidity) + switch_listener = ClusterListener(siren_dev.endpoints[1].on_off) + + frames = ( + ZCL_TUYA_SIREN_TEMPERATURE, + ZCL_TUYA_SIREN_HUMIDITY, + ZCL_TUYA_SIREN_ON, + ZCL_TUYA_SIREN_OFF, + ) + for frame in frames: + hdr, args = tuya_cluster.deserialize(frame) + tuya_cluster.handle_message(hdr, args) + + assert len(temp_listener.cluster_commands) == 0 + assert len(temp_listener.attribute_updates) == 1 + assert temp_listener.attribute_updates[0][0] == 0x0000 + assert temp_listener.attribute_updates[0][1] == 1790 + + assert len(humid_listener.cluster_commands) == 0 + assert len(humid_listener.attribute_updates) == 1 + assert humid_listener.attribute_updates[0][0] == 0x0000 + assert humid_listener.attribute_updates[0][1] == 8500 + + assert len(switch_listener.cluster_commands) == 0 + assert len(switch_listener.attribute_updates) == 2 + assert switch_listener.attribute_updates[0][0] == 0x0000 + assert switch_listener.attribute_updates[0][1] == ON + assert switch_listener.attribute_updates[1][0] == 0x0000 + assert switch_listener.attribute_updates[1][1] == OFF + + +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.siren.TuyaSiren,)) +async def test_siren_send_attribute(zigpy_device_from_quirk, quirk): + """Test tuya siren outgoing commands.""" + + siren_dev = zigpy_device_from_quirk(quirk) + tuya_cluster = siren_dev.endpoints[1].tuya_manufacturer + switch_cluster = siren_dev.endpoints[1].on_off + + async def async_success(*args, **kwargs): + return foundation.Status.SUCCESS + + with mock.patch.object( + tuya_cluster.endpoint, "request", side_effect=async_success + ) as m1: + + status = await switch_cluster.command(0x0000) + m1.assert_called_with( + 61184, + 1, + b"\x01\x01\x00\x00\x01h\x01\x00\x01\x00", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + status = await switch_cluster.command(0x0001) + m1.assert_called_with( + 61184, + 2, + b"\x01\x02\x00\x00\x02h\x01\x00\x01\x01", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + status = switch_cluster.command(0x0003) + assert status == foundation.Status.UNSUP_CLUSTER_COMMAND + + +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.valve.SiterwellGS361,)) +async def test_valve_state_report(zigpy_device_from_quirk, quirk): + """Test thermostatic valves standard reporting from incoming commands.""" + + valve_dev = zigpy_device_from_quirk(quirk) + tuya_cluster = valve_dev.endpoints[1].tuya_manufacturer + + thermostat_listener = ClusterListener(valve_dev.endpoints[1].thermostat) + + frames = ( + ZCL_TUYA_VALVE_TEMPERATURE, + ZCL_TUYA_VALVE_TARGET_TEMP, + ZCL_TUYA_VALVE_OFF, + ZCL_TUYA_VALVE_SCHEDULE, + ZCL_TUYA_VALVE_MANUAL, + ) + for frame in frames: + hdr, args = tuya_cluster.deserialize(frame) + tuya_cluster.handle_message(hdr, args) + + assert len(thermostat_listener.cluster_commands) == 0 + assert len(thermostat_listener.attribute_updates) == 13 + assert thermostat_listener.attribute_updates[0][0] == 0x0000 # TEMP + assert thermostat_listener.attribute_updates[0][1] == 1790 + assert thermostat_listener.attribute_updates[1][0] == 0x0012 # TARGET + assert thermostat_listener.attribute_updates[1][1] == 500 + assert thermostat_listener.attribute_updates[2][0] == 0x001C # OFF + assert thermostat_listener.attribute_updates[2][1] == 0x00 + assert thermostat_listener.attribute_updates[3][0] == 0x001E + assert thermostat_listener.attribute_updates[3][1] == 0x00 + assert thermostat_listener.attribute_updates[4][0] == 0x0029 + assert thermostat_listener.attribute_updates[4][1] == 0x00 + assert thermostat_listener.attribute_updates[5][0] == 0x001C # SCHEDULE + assert thermostat_listener.attribute_updates[5][1] == 0x04 + assert thermostat_listener.attribute_updates[6][0] == 0x0025 + assert thermostat_listener.attribute_updates[6][1] == 0x01 + assert thermostat_listener.attribute_updates[7][0] == 0x001E + assert thermostat_listener.attribute_updates[7][1] == 0x04 + assert thermostat_listener.attribute_updates[8][0] == 0x0029 + assert thermostat_listener.attribute_updates[8][1] == 0x01 + assert thermostat_listener.attribute_updates[9][0] == 0x001C # MANUAL + assert thermostat_listener.attribute_updates[9][1] == 0x04 + assert thermostat_listener.attribute_updates[10][0] == 0x0025 + assert thermostat_listener.attribute_updates[10][1] == 0x00 + assert thermostat_listener.attribute_updates[11][0] == 0x001E + assert thermostat_listener.attribute_updates[11][1] == 0x04 + assert thermostat_listener.attribute_updates[12][0] == 0x0029 + assert thermostat_listener.attribute_updates[12][1] == 0x01 + + +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.valve.SiterwellGS361,)) +async def test_valve_send_attribute(zigpy_device_from_quirk, quirk): + """Test thermostatic valve outgoing commands.""" + + valve_dev = zigpy_device_from_quirk(quirk) + tuya_cluster = valve_dev.endpoints[1].tuya_manufacturer + thermostat_cluster = valve_dev.endpoints[1].thermostat + + async def async_success(*args, **kwargs): + return foundation.Status.SUCCESS + + with mock.patch.object( + tuya_cluster.endpoint, "request", side_effect=async_success + ) as m1: + + status = await thermostat_cluster.write_attributes( + { + "occupied_heating_setpoint": 2500, + } + ) + m1.assert_called_with( + 61184, + 1, + b"\x01\x01\x00\x00\x01\x02\x02\x00\x04\x00\x00\x00\xfa", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + status = await thermostat_cluster.write_attributes( + { + "system_mode": 0x00, + } + ) + m1.assert_called_with( + 61184, + 2, + b"\x01\x02\x00\x00\x02\x04\x04\x00\x01\x00", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + status = await thermostat_cluster.write_attributes( + { + "system_mode": 0x04, + } + ) + m1.assert_called_with( + 61184, + 3, + b"\x01\x03\x00\x00\x03\x04\x04\x00\x01\x02", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + status = await thermostat_cluster.write_attributes( + { + "programing_oper_mode": 0x01, + } + ) + m1.assert_called_with( + 61184, + 4, + b"\x01\x04\x00\x00\x04\x04\x04\x00\x01\x01", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + # simulate a target temp update so that relative changes can work + hdr, args = tuya_cluster.deserialize(ZCL_TUYA_VALVE_TARGET_TEMP) + tuya_cluster.handle_message(hdr, args) + status = await thermostat_cluster.command(0x0000, 0x00, 20) + m1.assert_called_with( + 61184, + 5, + b"\x01\x05\x00\x00\x05\x02\x02\x00\x04\x00\x00\x00F", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + status = await thermostat_cluster.command(0x0002) + assert status == foundation.Status.UNSUP_CLUSTER_COMMAND + + +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.electric_heating.MoesBHT,)) +async def test_eheating_state_report(zigpy_device_from_quirk, quirk): + """Test thermostatic valves standard reporting from incoming commands.""" + + electric_dev = zigpy_device_from_quirk(quirk) + tuya_cluster = electric_dev.endpoints[1].tuya_manufacturer + + thermostat_listener = ClusterListener(electric_dev.endpoints[1].thermostat) + + frames = (ZCL_TUYA_EHEAT_TEMPERATURE, ZCL_TUYA_EHEAT_TARGET_TEMP) + for frame in frames: + hdr, args = tuya_cluster.deserialize(frame) + tuya_cluster.handle_message(hdr, args) + + assert len(thermostat_listener.cluster_commands) == 0 + assert len(thermostat_listener.attribute_updates) == 2 + assert thermostat_listener.attribute_updates[0][0] == 0x0000 # TEMP + assert thermostat_listener.attribute_updates[0][1] == 1790 + assert thermostat_listener.attribute_updates[1][0] == 0x0012 # TARGET + assert thermostat_listener.attribute_updates[1][1] == 2100 + + +@pytest.mark.parametrize("quirk", (zhaquirks.tuya.electric_heating.MoesBHT,)) +async def test_eheat_send_attribute(zigpy_device_from_quirk, quirk): + """Test electric thermostat outgoing commands.""" + + eheat_dev = zigpy_device_from_quirk(quirk) + tuya_cluster = eheat_dev.endpoints[1].tuya_manufacturer + thermostat_cluster = eheat_dev.endpoints[1].thermostat + + async def async_success(*args, **kwargs): + return foundation.Status.SUCCESS + + with mock.patch.object( + tuya_cluster.endpoint, "request", side_effect=async_success + ) as m1: + + status = await thermostat_cluster.write_attributes( + { + "occupied_heating_setpoint": 2500, + } + ) + m1.assert_called_with( + 61184, + 1, + b"\x01\x01\x00\x00\x01\x10\x02\x00\x04\x00\x00\x00\x19", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + status = await thermostat_cluster.write_attributes( + { + "system_mode": 0x00, + } + ) + m1.assert_called_with( + 61184, + 2, + b"\x01\x02\x00\x00\x02\x01\x01\x00\x01\x00", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + status = await thermostat_cluster.write_attributes( + { + "system_mode": 0x04, + } + ) + m1.assert_called_with( + 61184, + 3, + b"\x01\x03\x00\x00\x03\x01\x01\x00\x01\x01", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + # simulate a target temp update so that relative changes can work + hdr, args = tuya_cluster.deserialize(ZCL_TUYA_EHEAT_TARGET_TEMP) + tuya_cluster.handle_message(hdr, args) + status = await thermostat_cluster.command(0x0000, 0x00, 20) + m1.assert_called_with( + 61184, + 4, + b"\x01\x04\x00\x00\x04\x10\x02\x00\x04\x00\x00\x00\x17", + expect_reply=False, + command_id=0, + ) + assert status == (foundation.Status.SUCCESS,) + + status = await thermostat_cluster.command(0x0002) + assert status == foundation.Status.UNSUP_CLUSTER_COMMAND diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py index 7a2980e1b0..d72cc1e53e 100644 --- a/zhaquirks/tuya/__init__.py +++ b/zhaquirks/tuya/__init__.py @@ -5,9 +5,10 @@ from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.zcl import foundation -from zigpy.zcl.clusters.general import OnOff +from zigpy.zcl.clusters.general import OnOff, PowerConfiguration +from zigpy.zcl.clusters.hvac import Thermostat, UserInterface -from .. import Bus +from .. import Bus, LocalDataCluster TUYA_CLUSTER_ID = 0xEF00 TUYA_SET_DATA = 0x0000 @@ -24,6 +25,23 @@ class Data(t.List, item_type=t.uint8_t): """list of uint8_t.""" + @classmethod + def from_value(cls, value): + """Convert from a zigpy typed value to a tuya data payload.""" + # serialized in little-endian by zigpy + data = cls(value.serialize()) + # we want big-endian, with length prepended + data.append(len(data)) + data.reverse() + return data + + def to_value(self, ztype): + """Convert from a tuya data payload to a zigpy typed value.""" + # first uint8_t is the length of the remaining data + # tuya data is in big endian whereas ztypes use little endian + value, _ = ztype.deserialize(bytes(reversed(self[1:]))) + return value + class TuyaManufCluster(CustomCluster): """Tuya manufacturer specific cluster.""" @@ -58,7 +76,7 @@ def handle_cluster_request(self, tsn: int, command_id: int, args: Tuple) -> None return super().handle_cluster_request(tsn, command_id, args) tuya_cmd = args[0].command_id - tuya_value = args[0].data[1:] # first uint8_t is length + tuya_data = args[0].data _LOGGER.debug( "[0x%04x:%s:0x%04x] Received value %s " @@ -66,7 +84,7 @@ def handle_cluster_request(self, tsn: int, command_id: int, args: Tuple) -> None self.endpoint.device.nwk, self.endpoint.endpoint_id, self.cluster_id, - repr(tuya_value), + repr(tuya_data[1:]), tuya_cmd, command_id, ) @@ -74,9 +92,8 @@ def handle_cluster_request(self, tsn: int, command_id: int, args: Tuple) -> None if tuya_cmd not in self.attributes: return - # tuya data is in big endian whereas ztypes use little endian ztype = self.attributes[tuya_cmd][1] - zvalue, _ = ztype.deserialize(bytes(reversed(tuya_value))) + zvalue = tuya_data.to_value(ztype) self._update_attribute(tuya_cmd, zvalue) def read_attributes( @@ -94,18 +111,12 @@ async def write_attributes(self, attributes, manufacturer=None): records = self._write_attr_records(attributes) for record in records: - # serialized in little-endian - data = list(record.value.value.serialize()) - # we want big-endian, with length prepended - data.append(len(data)) - data.reverse() - cmd_payload = TuyaManufCluster.Command() cmd_payload.status = 0 cmd_payload.tsn = self.endpoint.device.application.get_sequence() cmd_payload.command_id = record.attrid cmd_payload.function = 0 - cmd_payload.data = data + cmd_payload.data = Data.from_value(record.value.value) await super().command( TUYA_SET_DATA, @@ -185,3 +196,158 @@ def __init__(self, *args, **kwargs): """Init device.""" self.switch_bus = Bus() super().__init__(*args, **kwargs) + + +class TuyaThermostatCluster(LocalDataCluster, Thermostat): + """Thermostat cluster for Tuya thermostats.""" + + _CONSTANT_ATTRIBUTES = {0x001B: Thermostat.ControlSequenceOfOperation.Heating_Only} + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.thermostat_bus.add_listener(self) + + def temperature_change(self, attr, value): + """Local or target temperature change from device.""" + self._update_attribute(self.attridx[attr], value) + + def state_change(self, value): + """State update from device.""" + if value == 0: + mode = self.RunningMode.Off + state = self.RunningState.Idle + else: + mode = self.RunningMode.Heat + state = self.RunningState.Heat_State_On + self._update_attribute(self.attridx["running_mode"], mode) + self._update_attribute(self.attridx["running_state"], state) + + # pylint: disable=R0201 + def map_attribute(self, attribute, value): + """Map standardized attribute value to dict of manufacturer values.""" + return {} + + async def write_attributes(self, attributes, manufacturer=None): + """Implement writeable attributes.""" + + records = self._write_attr_records(attributes) + + if not records: + return (foundation.Status.SUCCESS,) + + manufacturer_attrs = {} + for record in records: + attr_name = self.attributes[record.attrid][0] + new_attrs = self.map_attribute(attr_name, record.value.value) + + _LOGGER.debug( + "[0x%04x:%s:0x%04x] Mapping standard %s (0x%04x) " + "with value %s to custom %s", + self.endpoint.device.nwk, + self.endpoint.endpoint_id, + self.cluster_id, + attr_name, + record.attrid, + repr(record.value.value), + repr(new_attrs), + ) + + manufacturer_attrs.update(new_attrs) + + if not manufacturer_attrs: + return (foundation.Status.FAILURE,) + + await self.endpoint.tuya_manufacturer.write_attributes( + manufacturer_attrs, manufacturer=manufacturer + ) + + return (foundation.Status.SUCCESS,) + + # pylint: disable=W0236 + async def command( + self, + command_id: Union[foundation.Command, int, t.uint8_t], + *args, + manufacturer: Optional[Union[int, t.uint16_t]] = None, + expect_reply: bool = True, + tsn: Optional[Union[int, t.uint8_t]] = None, + ): + """Implement thermostat commands.""" + + if command_id != 0x0000: + return foundation.Status.UNSUP_CLUSTER_COMMAND + + mode, offset = args + if mode not in (self.SetpointMode.Heat, self.SetpointMode.Both): + return foundation.Status.INVALID_VALUE + + attrid = self.attridx["occupied_heating_setpoint"] + + success, _ = await self.read_attributes((attrid,), manufacturer=manufacturer) + try: + current = success[attrid] + except KeyError: + return foundation.Status.FAILURE + + # offset is given in decidegrees, see Zigbee cluster specification + return await self.write_attributes( + {"occupied_heating_setpoint": current + offset * 10}, + manufacturer=manufacturer, + ) + + +class TuyaUserInterfaceCluster(LocalDataCluster, UserInterface): + """HVAC User interface cluster for tuya thermostats.""" + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.ui_bus.add_listener(self) + + def child_lock_change(self, mode): + """Change of child lock setting.""" + if mode == 0: + lockout = self.KeypadLockout.No_lockout + else: + lockout = self.KeypadLockout.Level_1_lockout + + self._update_attribute(self.attridx["keypad_lockout"], lockout) + + async def write_attributes(self, attributes, manufacturer=None): + """Defer the keypad_lockout attribute to child_lock.""" + + records = self._write_attr_records(attributes) + + for record in records: + if record.attrid == self.attridx["keypad_lockout"]: + lock = 0 if record.value.value == self.KeypadLockout.No_lockout else 1 + return await self.endpoint.tuya_manufacturer.write_attributes( + {self._CHILD_LOCK_ATTR: lock}, manufacturer=manufacturer + ) + + return (foundation.Status.FAILURE,) + + +class TuyaPowerConfigurationCluster(LocalDataCluster, PowerConfiguration): + """PowerConfiguration cluster for battery-operated thermostats.""" + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.battery_bus.add_listener(self) + + def battery_change(self, value): + """Change of reported battery percentage remaining.""" + self._update_attribute(self.attridx["battery_percentage_remaining"], value * 2) + + +class TuyaThermostat(CustomDevice): + """Generic Tuya thermostat device.""" + + def __init__(self, *args, **kwargs): + """Init device.""" + self.thermostat_bus = Bus() + self.ui_bus = Bus() + self.battery_bus = Bus() + super().__init__(*args, **kwargs) diff --git a/zhaquirks/tuya/electric_heating.py b/zhaquirks/tuya/electric_heating.py new file mode 100644 index 0000000000..1ee1b7009b --- /dev/null +++ b/zhaquirks/tuya/electric_heating.py @@ -0,0 +1,172 @@ +"""Map from manufacturer to standard clusters for electric heating thermostats.""" +import logging + +from zigpy.profiles import zha +import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time + +from . import ( + TuyaManufClusterAttributes, + TuyaThermostat, + TuyaThermostatCluster, + TuyaUserInterfaceCluster, +) +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +# info from https://github.com/zigpy/zha-device-handlers/pull/538#issuecomment-723334124 +# https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/fromZigbee.js#L239 +# and https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/common.js#L113 +MOESBHT_TARGET_TEMP_ATTR = 0x0210 # [0,0,0,21] target room temp (degree) +MOESBHT_TEMPERATURE_ATTR = 0x0218 # [0,0,0,200] current room temp (decidegree) +MOESBHT_SCHEDULE_MODE_ATTR = 0x0403 # [1] false [0] true /!\ inverted +MOESBHT_MANUAL_MODE_ATTR = 0x0402 # [1] false [0] true /!\ inverted +MOESBHT_ENABLED_ATTR = 0x0101 # [0] off [1] on +MOESBHT_RUNNING_MODE_ATTR = 0x0424 # [1] idle [0] heating /!\ inverted +MOESBHT_CHILD_LOCK_ATTR = 0x0128 # [0] unlocked [1] child-locked + +_LOGGER = logging.getLogger(__name__) + + +class MoesBHTManufCluster(TuyaManufClusterAttributes): + """Manufacturer Specific Cluster of some electric heating thermostats.""" + + manufacturer_attributes = { + MOESBHT_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t), + MOESBHT_TEMPERATURE_ATTR: ("temperature", t.uint32_t), + MOESBHT_SCHEDULE_MODE_ATTR: ("schedule_mode", t.uint8_t), + MOESBHT_MANUAL_MODE_ATTR: ("manual_mode", t.uint8_t), + MOESBHT_ENABLED_ATTR: ("enabled", t.uint8_t), + MOESBHT_RUNNING_MODE_ATTR: ("running_mode", t.uint8_t), + MOESBHT_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t), + } + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == MOESBHT_TARGET_TEMP_ATTR: + self.endpoint.device.thermostat_bus.listener_event( + "temperature_change", + "occupied_heating_setpoint", + value * 100, # degree to centidegree + ) + elif attrid == MOESBHT_TEMPERATURE_ATTR: + self.endpoint.device.thermostat_bus.listener_event( + "temperature_change", + "local_temp", + value * 10, # decidegree to centidegree + ) + elif attrid == MOESBHT_SCHEDULE_MODE_ATTR: + if value == 0: # value is inverted + self.endpoint.device.thermostat_bus.listener_event( + "program_change", "scheduled" + ) + elif attrid == MOESBHT_MANUAL_MODE_ATTR: + if value == 0: # value is inverted + self.endpoint.device.thermostat_bus.listener_event( + "program_change", "manual" + ) + elif attrid == MOESBHT_ENABLED_ATTR: + self.endpoint.device.thermostat_bus.listener_event("enabled_change", value) + elif attrid == MOESBHT_RUNNING_MODE_ATTR: + # value is inverted + self.endpoint.device.thermostat_bus.listener_event( + "state_change", 1 - value + ) + elif attrid == MOESBHT_CHILD_LOCK_ATTR: + self.endpoint.device.ui_bus.listener_event("child_lock_change", value) + + +class MoesBHTThermostat(TuyaThermostatCluster): + """Thermostat cluster for some electric heating controllers.""" + + def map_attribute(self, attribute, value): + """Map standardized attribute value to dict of manufacturer values.""" + + if attribute == "occupied_heating_setpoint": + # centidegree to degree + return {MOESBHT_TARGET_TEMP_ATTR: round(value / 100)} + if attribute == "system_mode": + if value == self.SystemMode.Off: + return {MOESBHT_ENABLED_ATTR: 0} + if value == self.SystemMode.Heat: + return {MOESBHT_ENABLED_ATTR: 1} + self.error("Unsupported value for SystemMode") + elif attribute == "programing_oper_mode": + # values are inverted + if value == self.ProgrammingOperationMode.Simple: + return {MOESBHT_MANUAL_MODE_ATTR: 0, MOESBHT_SCHEDULE_MODE_ATTR: 1} + if value == self.ProgrammingOperationMode.Schedule_programming_mode: + return {MOESBHT_MANUAL_MODE_ATTR: 1, MOESBHT_SCHEDULE_MODE_ATTR: 0} + self.error("Unsupported value for ProgrammingOperationMode") + + return super().map_attribute(attribute, value) + + def program_change(self, mode): + """Programming mode change.""" + if mode == "manual": + value = self.ProgrammingOperationMode.Simple + else: + value = self.ProgrammingOperationMode.Schedule_programming_mode + + self._update_attribute(self.attridx["programing_oper_mode"], value) + + def enabled_change(self, value): + """System mode change.""" + if value == 0: + mode = self.SystemMode.Off + else: + mode = self.SystemMode.Heat + self._update_attribute(self.attridx["system_mode"], mode) + + +class MoesBHTUserInterface(TuyaUserInterfaceCluster): + """HVAC User interface cluster for tuya electric heating thermostats.""" + + _CHILD_LOCK_ATTR = MOESBHT_CHILD_LOCK_ATTR + + +class MoesBHT(TuyaThermostat): + """Moes BHT-002GCLZB Thermostatic radiator valve.""" + + signature = { + # endpoint=1 profile=260 device_type=81 device_version=1 input_clusters=[0, 4, 5, 61184], + # output_clusters=[10, 25] + MODELS_INFO: [("_TZE200_aoclfnxz", "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, + TuyaManufClusterAttributes.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.THERMOSTAT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + MoesBHTManufCluster, + MoesBHTThermostat, + MoesBHTUserInterface, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + } + } diff --git a/zhaquirks/tuya/valve.py b/zhaquirks/tuya/valve.py new file mode 100644 index 0000000000..a32dc194aa --- /dev/null +++ b/zhaquirks/tuya/valve.py @@ -0,0 +1,207 @@ +"""Map from manufacturer to standard clusters for thermostatic valves.""" +import logging + +from zigpy.profiles import zha +import zigpy.types as t +from zigpy.zcl.clusters.general import Basic, Groups, Identify, Ota, Scenes, Time + +from . import ( + TuyaManufClusterAttributes, + TuyaPowerConfigurationCluster, + TuyaThermostat, + TuyaThermostatCluster, + TuyaUserInterfaceCluster, +) +from ..const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +# info from https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/common.js#L113 +# and https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/fromZigbee.js#L362 +SITERWELL_CHILD_LOCK_ATTR = 0x0107 # [0] unlocked [1] child-locked +SITERWELL_WINDOW_DETECT_ATTR = 0x0112 # [0] inactive [1] active +SITERWELL_VALVE_DETECT_ATTR = 0x0114 # [0] do not report [1] report +SITERWELL_VALVE_STATE_ATTR = 0x026D # [0,0,0,55] opening percentage +SITERWELL_TARGET_TEMP_ATTR = 0x0202 # [0,0,0,210] target room temp (decidegree) +SITERWELL_TEMPERATURE_ATTR = 0x0203 # [0,0,0,200] current room temp (decidegree) +SITERWELL_BATTERY_ATTR = 0x0215 # [0,0,0,98] battery charge +SITERWELL_MODE_ATTR = 0x0404 # [0] off [1] scheduled [2] manual + +_LOGGER = logging.getLogger(__name__) + + +class SiterwellManufCluster(TuyaManufClusterAttributes): + """Manufacturer Specific Cluster of some thermostatic valves.""" + + manufacturer_attributes = { + SITERWELL_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t), + SITERWELL_WINDOW_DETECT_ATTR: ("window_detection", t.uint8_t), + SITERWELL_VALVE_DETECT_ATTR: ("valve_detect", t.uint8_t), + SITERWELL_VALVE_STATE_ATTR: ("valve_state", t.uint32_t), + SITERWELL_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t), + SITERWELL_TEMPERATURE_ATTR: ("temperature", t.uint32_t), + SITERWELL_BATTERY_ATTR: ("battery", t.uint32_t), + SITERWELL_MODE_ATTR: ("mode", t.uint8_t), + } + + TEMPERATURE_ATTRS = { + SITERWELL_TEMPERATURE_ATTR: "local_temp", + SITERWELL_TARGET_TEMP_ATTR: "occupied_heating_setpoint", + } + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid in self.TEMPERATURE_ATTRS: + self.endpoint.device.thermostat_bus.listener_event( + "temperature_change", + self.TEMPERATURE_ATTRS[attrid], + value * 10, # decidegree to centidegree + ) + elif attrid == SITERWELL_MODE_ATTR: + self.endpoint.device.thermostat_bus.listener_event("mode_change", value) + self.endpoint.device.thermostat_bus.listener_event( + "state_change", value > 0 + ) + elif attrid == SITERWELL_VALVE_STATE_ATTR: + self.endpoint.device.thermostat_bus.listener_event("state_change", value) + elif attrid == SITERWELL_CHILD_LOCK_ATTR: + mode = 1 if value else 0 + self.endpoint.device.ui_bus.listener_event("child_lock_change", mode) + elif attrid == SITERWELL_BATTERY_ATTR: + self.endpoint.device.battery_bus.listener_event("battery_change", value) + + +class SiterwellThermostat(TuyaThermostatCluster): + """Thermostat cluster for some thermostatic valves.""" + + def map_attribute(self, attribute, value): + """Map standardized attribute value to dict of manufacturer values.""" + + if attribute == "occupied_heating_setpoint": + # centidegree to decidegree + return {SITERWELL_TARGET_TEMP_ATTR: round(value / 10)} + if attribute in ("system_mode", "programing_oper_mode"): + if attribute == "system_mode": + system_mode = value + oper_mode = self._attr_cache.get( + self.attridx["programing_oper_mode"], + self.ProgrammingOperationMode.Simple, + ) + else: + system_mode = self._attr_cache.get( + self.attridx["system_mode"], self.SystemMode.Heat + ) + oper_mode = value + if system_mode == self.SystemMode.Off: + return {SITERWELL_MODE_ATTR: 0} + if system_mode == self.SystemMode.Heat: + if oper_mode == self.ProgrammingOperationMode.Schedule_programming_mode: + return {SITERWELL_MODE_ATTR: 1} + if oper_mode == self.ProgrammingOperationMode.Simple: + return {SITERWELL_MODE_ATTR: 2} + self.error("Unsupported value for ProgrammingOperationMode") + else: + self.error("Unsupported value for SystemMode") + + def mode_change(self, value): + """System Mode change.""" + if value == 0: + self._update_attribute(self.attridx["system_mode"], self.SystemMode.Off) + return + + if value == 1: + mode = self.ProgrammingOperationMode.Schedule_programming_mode + else: + mode = self.ProgrammingOperationMode.Simple + + self._update_attribute(self.attridx["system_mode"], self.SystemMode.Heat) + self._update_attribute(self.attridx["programing_oper_mode"], mode) + + +class SiterwellUserInterface(TuyaUserInterfaceCluster): + """HVAC User interface cluster for tuya electric heating thermostats.""" + + _CHILD_LOCK_ATTR = SITERWELL_CHILD_LOCK_ATTR + + +class SiterwellGS361(TuyaThermostat): + """SiterwellGS361 Thermostatic radiator valve and clones.""" + + signature = { + # endpoint=1 profile=260 device_type=0 device_version=0 input_clusters=[0, 3] + # output_clusters=[3, 25]> + MODELS_INFO: [("_TYST11_jeaxp72v", "eaxp72v"), ("_TYST11_kfvq6avy", "fvq6avy")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [Basic.cluster_id, Identify.cluster_id], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.THERMOSTAT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + SiterwellManufCluster, + SiterwellThermostat, + SiterwellUserInterface, + TuyaPowerConfigurationCluster, + ], + OUTPUT_CLUSTERS: [Identify.cluster_id, Ota.cluster_id], + } + } + } + + +class MoesHY368(TuyaThermostat): + """MoesHY368 Thermostatic radiator valve.""" + + signature = { + # endpoint=1 profile=260 device_type=81 device_version=0 input_clusters=[0, 4, 5, 61184] + # output_clusters=[10, 25]> + MODELS_INFO: [("_TZE200_ckud7u2l", "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, + TuyaManufClusterAttributes.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.THERMOSTAT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + SiterwellManufCluster, + SiterwellThermostat, + SiterwellUserInterface, + TuyaPowerConfigurationCluster, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + } + } + } From b655c3ee6c84265dce61999ed99622081683db23 Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Fri, 27 Nov 2020 15:25:27 -0500 Subject: [PATCH 113/113] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9014132cac..872a99ebe9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.46" +VERSION = "0.0.47" def readme():