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(): diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py index 2013e59271..08c4694421 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 = [ @@ -142,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) 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, + ], + } + } + } 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 = { + # + 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], + } + } + } diff --git a/zhaquirks/plaid/soil.py b/zhaquirks/plaid/soil.py index a85fc142bd..65a6720aff 100644 --- a/zhaquirks/plaid/soil.py +++ b/zhaquirks/plaid/soil.py @@ -25,7 +25,23 @@ 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)) + + 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): 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