From 9f13509fe9fc4e326d8f56223086998765dfd760 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Sun, 29 Dec 2019 00:26:53 +0300 Subject: [PATCH 1/7] Fix LocalDataCluster (#227) * Fix LocalDataCluster Fixed attribute read for LocalDataCluster. * Removed unused import * Cater for 0 value --- zhaquirks/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 5781874092..8503b8dc86 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -4,8 +4,8 @@ import pkgutil from zigpy.quirks import CustomCluster -import zigpy.types as types from zigpy.util import ListenableMixin +from zigpy.zcl import foundation from zigpy.zcl.clusters.general import PowerConfiguration from zigpy.zdo import types as zdotypes @@ -35,9 +35,17 @@ class LocalDataCluster(CustomCluster): async def read_attributes_raw(self, attributes, manufacturer=None): """Prevent remote reads.""" - attributes = [types.uint16_t(a) for a in attributes] - values = [self._attr_cache.get(attr) for attr in attributes] - return values + records = [ + foundation.ReadAttributeRecord( + attr, foundation.Status.UNSUPPORTED_ATTRIBUTE, foundation.TypeValue() + ) + for attr in attributes + ] + for record in records: + record.value.value = self._attr_cache.get(record.attrid) + if record.value.value is not None: + record.status = foundation.Status.SUCCESS + return [records] class EventableCluster(CustomCluster): From fa189ce1a069d039f0f6d7664c4ed906b38b8a51 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Mon, 30 Dec 2019 00:52:19 +0300 Subject: [PATCH 2/7] LocalDataCluster: prevent remote attribute writes (#229) * LocalDataCluster: prevent remote attribute writes * removed unused variable --- zhaquirks/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 8503b8dc86..8fcc04cb3a 100755 --- a/zhaquirks/__init__.py +++ b/zhaquirks/__init__.py @@ -45,7 +45,18 @@ async def read_attributes_raw(self, attributes, manufacturer=None): record.value.value = self._attr_cache.get(record.attrid) if record.value.value is not None: record.status = foundation.Status.SUCCESS - return [records] + return (records,) + + async def write_attributes(self, attributes, manufacturer=None): + """Prevent remote writes.""" + for attrid, value in attributes.items(): + if isinstance(attrid, str): + attrid = self._attridx[attrid] + if attrid not in self.attributes: + self.error("%d is not a valid attribute id", attrid) + continue + self._update_attribute(attrid, value) + return (foundation.Status.SUCCESS,) class EventableCluster(CustomCluster): From 9d53f5ae93f18181f868b4636523aabc42ab0906 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Mon, 13 Jan 2020 16:29:07 +0300 Subject: [PATCH 3/7] XBee PWM support (#230) --- README.md | 1 + zhaquirks/xbee/__init__.py | 84 ++++++++++++++++++++++++++++++-------- zhaquirks/xbee/xbee3_io.py | 6 +-- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ab8abbdb74..7557b04730 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ If you are looking to make your first code contribution to this project then we - 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. diff --git a/zhaquirks/xbee/__init__.py b/zhaquirks/xbee/__init__.py index 45786c013a..0bfd5a692c 100644 --- a/zhaquirks/xbee/__init__.py +++ b/zhaquirks/xbee/__init__.py @@ -22,7 +22,13 @@ from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.zcl import foundation -from zigpy.zcl.clusters.general import AnalogInput, BinaryInput, LevelControl, OnOff +from zigpy.zcl.clusters.general import ( + AnalogInput, + AnalogOutput, + BinaryInput, + LevelControl, + OnOff, +) from .. import EventableCluster, LocalDataCluster from ..const import ENDPOINTS, INPUT_CLUSTERS, OUTPUT_CLUSTERS @@ -40,6 +46,8 @@ XBEE_PROFILE_ID = 0xC105 XBEE_REMOTE_AT = 0x17 XBEE_SRC_ENDPOINT = 0xE8 +ATTR_PRESENT_VALUE = 0x0055 +PIN_ANALOG_OUTPUT = 2 class IOSample(bytes): @@ -125,26 +133,27 @@ def deserialize(cls, data): } +ENDPOINT_TO_AT = { + 0xD0: "D0", + 0xD1: "D1", + 0xD2: "D2", + 0xD3: "D3", + 0xD4: "D4", + 0xD5: "D5", + 0xD8: "D8", + 0xD9: "D9", + 0xDA: "P0", + 0xDB: "P1", + 0xDC: "P2", +} + + class XBeeOnOff(CustomCluster, OnOff): """XBee on/off cluster.""" - ep_id_2_pin = { - 0xD0: "D0", - 0xD1: "D1", - 0xD2: "D2", - 0xD3: "D3", - 0xD4: "D4", - 0xD5: "D5", - 0xD8: "D8", - 0xD9: "D9", - 0xDA: "P0", - 0xDB: "P1", - 0xDC: "P2", - } - async def command(self, command, *args, manufacturer=None, expect_reply=True): """Xbee change pin state command, requires zigpy_xbee.""" - pin_name = self.ep_id_2_pin.get(self._endpoint.endpoint_id) + 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: @@ -155,6 +164,47 @@ async def command(self, command, *args, manufacturer=None, expect_reply=True): return 0, foundation.Status.SUCCESS +class XBeePWM(LocalDataCluster, AnalogOutput): + """XBee PWM Cluster.""" + + ep_id_2_pwm = {0xDA: "M0", 0xDB: "M1"} + + def __init__(self, endpoint, is_server=True): + """Set known attributes and store them in cache.""" + super().__init__(endpoint, is_server) + self._update_attribute(0x0041, float(0x03FF)) # max_present_value + self._update_attribute(0x0045, 0.0) # min_present_value + self._update_attribute(0x0051, 0) # out_of_service + self._update_attribute(0x006A, 1.0) # resolution + self._update_attribute(0x006F, 0x00) # status_flags + + async def write_attributes(self, attributes, manufacturer=None): + """Intercept present_value attribute write.""" + if ATTR_PRESENT_VALUE in attributes: + duty_cycle = int(round(float(attributes.pop(ATTR_PRESENT_VALUE)))) + at_command = self.ep_id_2_pwm.get(self._endpoint.endpoint_id) + result = await self._endpoint.device.remote_at(at_command, duty_cycle) + if result != foundation.Status.SUCCESS: + return result + + at_command = ENDPOINT_TO_AT.get(self._endpoint.endpoint_id) + result = await self._endpoint.device.remote_at( + at_command, PIN_ANALOG_OUTPUT + ) + if result != foundation.Status.SUCCESS or not attributes: + return result + + return await super().write_attributes(attributes, manufacturer) + + async def read_attributes_raw(self, attributes, manufacturer=None): + """Intercept present_value attribute read.""" + if ATTR_PRESENT_VALUE in attributes: + at_command = self.ep_id_2_pwm.get(self._endpoint.endpoint_id) + result = await self._endpoint.device.remote_at(at_command) + self._update_attribute(ATTR_PRESENT_VALUE, float(result)) + return await super().read_attributes_raw(attributes, manufacturer) + + class XBeeCommon(CustomDevice): """XBee common class.""" @@ -200,7 +250,7 @@ def handle_cluster_request(self, tsn, command_id, args): self._endpoint.device.__getitem__( ENDPOINT_MAP[pin] ).__getattr__(AnalogInput.ep_attribute)._update_attribute( - 0x0055, # "present_value" + ATTR_PRESENT_VALUE, values["analog_samples"][pin] / (10.23 if pin != 7 else 1000), # supply voltage is in mV ) diff --git a/zhaquirks/xbee/xbee3_io.py b/zhaquirks/xbee/xbee3_io.py index 4fbf77f2ca..110911f036 100644 --- a/zhaquirks/xbee/xbee3_io.py +++ b/zhaquirks/xbee/xbee3_io.py @@ -3,7 +3,7 @@ from zigpy.profiles import zha from zigpy.zcl.clusters.general import AnalogInput -from . import XBEE_PROFILE_ID, XBeeCommon, XBeeOnOff +from . import XBEE_PROFILE_ID, XBeeCommon, XBeeOnOff, XBeePWM from ..const import DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, OUTPUT_CLUSTERS, PROFILE_ID @@ -91,7 +91,7 @@ def __init__(self, application, ieee, nwk, replaces): "model": "DIO10/PWM0", DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, - INPUT_CLUSTERS: [XBeeOnOff], + INPUT_CLUSTERS: [XBeeOnOff, XBeePWM], OUTPUT_CLUSTERS: [], }, 0xDB: { @@ -99,7 +99,7 @@ def __init__(self, application, ieee, nwk, replaces): "model": "DIO11/PWM1", DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, PROFILE_ID: XBEE_PROFILE_ID, - INPUT_CLUSTERS: [XBeeOnOff], + INPUT_CLUSTERS: [XBeeOnOff, XBeePWM], OUTPUT_CLUSTERS: [], }, 0xDC: { From a567e19dcae3e4c17485ab9f628b8684592ab83d Mon Sep 17 00:00:00 2001 From: h3ndrik Date: Mon, 13 Jan 2020 14:29:33 +0100 Subject: [PATCH 4/7] Add new IKEA motion sensor E1745 (#232) --- zhaquirks/ikea/motionzha.py | 80 ++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/zhaquirks/ikea/motionzha.py b/zhaquirks/ikea/motionzha.py index 25ba42cbf1..7ebd33e7f2 100644 --- a/zhaquirks/ikea/motionzha.py +++ b/zhaquirks/ikea/motionzha.py @@ -1,13 +1,15 @@ """Device handler for IKEA of Sweden TRADFRI remote control.""" from zigpy.profiles import zha -from zigpy.quirks import CustomDevice +from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( Alarms, Basic, Groups, Identify, + LevelControl, OnOff, Ota, + PollControl, PowerConfiguration, ) from zigpy.zcl.clusters.lightlink import LightLink @@ -23,9 +25,24 @@ ) from . import IKEA, LightLinkCluster +IKEA_CLUSTER_ID = 0xFC7C # decimal = 64636 DIAGNOSTICS_CLUSTER_ID = 0x0B05 # decimal = 2821 +class LightLinkClusterNew(CustomCluster, LightLink): + """Ikea LightLink cluster.""" + + async def bind(self): + """Bind LightLink cluster to coordinator.""" + application = self._endpoint.device.application + try: + coordinator = application.get_device(application.ieee) + except KeyError: + return + status = await coordinator.add_to_group(0x0000) + return [status] + + class IkeaTradfriMotion(CustomDevice): """Custom device representing IKEA of Sweden TRADFRI remote control.""" @@ -81,3 +98,64 @@ class IkeaTradfriMotion(CustomDevice): } } } + + +class IkeaTradfriMotionE1745(CustomDevice): + """Custom device representing IKEA of Sweden TRADFRI motion sensor E1745.""" + + signature = { + # + MODELS_INFO: [(IKEA, "TRADFRI motion sensor")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SENSOR, + 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, + LightLink.cluster_id, + ], + } + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SENSOR, + INPUT_CLUSTERS: [ + Basic.cluster_id, + DoublingPowerConfigurationCluster, + Identify.cluster_id, + Alarms.cluster_id, + PollControl.cluster_id, + LightLinkClusterNew, + IKEA_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Ota.cluster_id, + LightLink.cluster_id, + ], + } + } + } From d175ec1f3f1a1b74a987c6ab37cded7cbe268d95 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 13 Jan 2020 08:35:28 -0500 Subject: [PATCH 5/7] Add support for IKEA 2 button remote with ZLL profile (#231) * add 2btn remote with zll profile * copy models info * fix signature --- zhaquirks/ikea/twobtnremote.py | 50 +++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/zhaquirks/ikea/twobtnremote.py b/zhaquirks/ikea/twobtnremote.py index 44d84d7dde..eaa4ad0e1e 100644 --- a/zhaquirks/ikea/twobtnremote.py +++ b/zhaquirks/ikea/twobtnremote.py @@ -1,5 +1,5 @@ """Device handler for IKEA of Sweden TRADFRI remote control.""" -from zigpy.profiles import zha +from zigpy.profiles import zha, zll from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.closures import WindowCovering from zigpy.zcl.clusters.general import ( @@ -135,3 +135,51 @@ class IkeaTradfriRemote2Btn(CustomDevice): ARGS: [1, 83], }, } + + +class IkeaTradfriRemote2BtnZLL(CustomDevice): + """Custom device representing IKEA of Sweden TRADFRI remote control.""" + + signature = { + # + MODELS_INFO: IkeaTradfriRemote2Btn.signature[MODELS_INFO].copy(), + ENDPOINTS: { + 1: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.CONTROLLER, + INPUT_CLUSTERS: [ + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + Alarms.cluster_id, + WindowCovering.cluster_id, + LightLink.cluster_id, + IKEA_CLUSTER_ID, + ], + OUTPUT_CLUSTERS: IkeaTradfriRemote2Btn.signature[ENDPOINTS][1][ + OUTPUT_CLUSTERS + ].copy(), + } + }, + } + signature[ENDPOINTS][1][INPUT_CLUSTERS].append(WindowCovering.cluster_id) + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zll.PROFILE_ID, + DEVICE_TYPE: zll.DeviceType.CONTROLLER, + INPUT_CLUSTERS: IkeaTradfriRemote2Btn.replacement[ENDPOINTS][1][ + INPUT_CLUSTERS + ].copy(), + OUTPUT_CLUSTERS: IkeaTradfriRemote2Btn.replacement[ENDPOINTS][1][ + OUTPUT_CLUSTERS + ].copy(), + } + } + } + + device_automation_triggers = IkeaTradfriRemote2Btn.device_automation_triggers.copy() From f6341e224ee29de8c518f20d53790fcc95cc7c47 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 13 Jan 2020 11:57:36 -0500 Subject: [PATCH 6/7] adjust battery top end (#238) --- zhaquirks/xiaomi/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py index b9ab7bec44..53d62d2398 100644 --- a/zhaquirks/xiaomi/__init__.py +++ b/zhaquirks/xiaomi/__init__.py @@ -264,9 +264,8 @@ def _parse_mija_attributes(self, value): @staticmethod def _calculate_remaining_battery_percentage(voltage): """Calculate percentage.""" - # Min/Max values from https://github.com/louisZL/lumi-gateway-local-api min_voltage = 2800 - max_voltage = 3300 + max_voltage = 3000 percent = (voltage - min_voltage) / (max_voltage - min_voltage) * 200 return min(200, percent) From 3c59dcb95fabe41cb36321523e36315a932c16bb Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Mon, 13 Jan 2020 12:01:16 -0500 Subject: [PATCH 7/7] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f6355a646b..4b26b34f74 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import find_packages, setup -VERSION = "0.0.30" +VERSION = "0.0.31" def readme():