"""Map from manufacturer to standard clusters for thermostatic valves.""" import datetime from typing import Final import zigpy.profiles.zha import zigpy.types as t from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time from zigpy.zcl.foundation import ZCLAttributeDef from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, ) from zhaquirks.tuya import ( TuyaManufClusterAttributes, TuyaThermostat, TuyaThermostatCluster, TuyaUserInterfaceCluster, ) # from https://github.com/Koenkk/zigbee-herdsman-converters/pull/1694/ HAOZEE_SCHEDULE_WORKDAYS_AM_ATTR = ( 0x0077 # schedule for workdays [8, 0, 20, 8, 1, 15, 11, 30, 15] ) HAOZEE_SCHEDULE_WORKDAYS_PM_ATTR = ( 0x0078 # schedule for workdays [13, 30, 15, 17, 0, 15, 150, 0, 15] ) HAOZEE_SCHEDULE_WEEKEND_AM_ATTR = ( 0x0079 # schedule for weekend [6, 0, 20, 8, 0, 15, 11, 30, 15] ) HAOZEE_SCHEDULE_WEEKEND_PM_ATTR = ( 0x007A # schedule for weekend [13, 30, 15, 17, 0, 15, 22, 0, 15] ) HAOZEE_HEATING_ENABLED_ATTR = 0x0166 # [0] idle [1] heating HAOZEE_MAX_TEMP_PROTECTION_ENABLED_ATTR = ( 0x016A # minimal temp protection [0] disabled [1] enabled ) HAOZEE_MIN_TEMP_PROTECTION_ENABLED_ATTR = ( 0x016B # maximal temp protection [0] disabled [1] enabled ) HAOZEE_ENABLED_ATTR = 0x017D # device status [0] disabled [1] enabled HAOZEE_CHILD_LOCK_ATTR = 0x0181 # [0] unlocked [1] child-locked HAOZEE_EXT_TEMP_ATTR = 0x0267 # external NTC sensor temp (decidegree) HAOZEE_AWAY_DAYS_ATTR = 0x0268 # away mode duration (days) HAOZEE_AWAY_TEMP_ATTR = 0x0269 # away mode temperature (decidegree) HAOZEE_TEMP_CALIBRATION_ATTR = 0x026D # temperature calibration (decidegree) HAOZEE_TEMP_HYSTERESIS_ATTR = 0x026E # temperature hysteresis (decidegree) HAOZEE_TEMP_PROTECT_HYSTERESIS_ATTR = ( 0x026F # temperature hysteresis for protection (decidegree) ) HAOZEE_MAX_PROTECT_TEMP_ATTR = ( 0x0270 # maximal temp for protection trigger (decidegree) ) HAOZEE_MIN_PROTECT_TEMP_ATTR = ( 0x0271 # minimal temp for protection trigger (decidegree) ) HAOZEE_MAX_TEMP_LIMIT_ATTR = 0x0272 # maximum limit of temperature setting (decidegree) HAOZEE_MIN_TEMP_LIMIT_ATTR = 0x0273 # minimum limit of temperature setting (decidegree) HAOZEE_TARGET_TEMP_ATTR = 0x027E # target temperature (decidegree) HAOZEE_CURRENT_ROOM_TEMP_ATTR = ( 0x027F # temperature reported by MCU. Depends on sensor type. (decidegree) ) HAOZEE_SENSOR_TYPE_ATTR = 0x0474 # sensor type [0] internal [1] external [2] both HAOZEE_POWERON_BEHAVIOR_ATTR = 0x0475 # poweron behavior [0] restore [1] off [2] on HAOZEE_WEEKFORMAT = 0x0476 # [0] 5+2 [1] 6+1 [2] 7 (days) HAOZEE_CURRENT_MODE_ATTR = 0x0480 # [0] manual [1] auto [2] away HAOZEE_FAULT_ATTR = 0x0582 # Known fault codes: [4] E2 external sensor error # Unknown DP - descaling on/off, window detection, window detection settings class HY08WEManufCluster(TuyaManufClusterAttributes): """Manufacturer Specific Cluster of some thermostatic valves.""" # Important! This device uses offset from 2000 year for UTC time and offset from 1970 for local time set_time_offset = datetime.datetime(2000, 1, 1, tzinfo=datetime.UTC) set_time_local_offset = datetime.datetime(1970, 1, 1) class AttributeDefs(TuyaManufClusterAttributes.AttributeDefs): """Attribute definitions.""" heating_enabled: Final = ZCLAttributeDef( id=HAOZEE_HEATING_ENABLED_ATTR, type=t.uint8_t ) max_temp_protection_enabled: Final = ZCLAttributeDef( id=HAOZEE_MAX_TEMP_PROTECTION_ENABLED_ATTR, type=t.uint8_t ) min_temp_protection_enabled: Final = ZCLAttributeDef( id=HAOZEE_MIN_TEMP_PROTECTION_ENABLED_ATTR, type=t.uint8_t ) enabled: Final = ZCLAttributeDef(id=HAOZEE_ENABLED_ATTR, type=t.uint8_t) child_lock: Final = ZCLAttributeDef(id=HAOZEE_CHILD_LOCK_ATTR, type=t.uint8_t) external_temperature: Final = ZCLAttributeDef( id=HAOZEE_EXT_TEMP_ATTR, type=t.uint32_t ) away_duration_days: Final = ZCLAttributeDef( id=HAOZEE_AWAY_DAYS_ATTR, type=t.uint32_t ) away_mode_temperature: Final = ZCLAttributeDef( id=HAOZEE_AWAY_TEMP_ATTR, type=t.uint32_t ) temperature_calibration: Final = ZCLAttributeDef( id=HAOZEE_TEMP_CALIBRATION_ATTR, type=t.int32s ) hysterisis_temperature: Final = ZCLAttributeDef( id=HAOZEE_TEMP_HYSTERESIS_ATTR, type=t.uint32_t ) hysterisis_protection_temperature: Final = ZCLAttributeDef( id=HAOZEE_TEMP_PROTECT_HYSTERESIS_ATTR, type=t.uint32_t ) max_protection_temperature: Final = ZCLAttributeDef( id=HAOZEE_MAX_PROTECT_TEMP_ATTR, type=t.uint32_t ) min_protection_temperature: Final = ZCLAttributeDef( id=HAOZEE_MIN_PROTECT_TEMP_ATTR, type=t.uint32_t ) max_temperature: Final = ZCLAttributeDef( id=HAOZEE_MAX_TEMP_LIMIT_ATTR, type=t.uint32_t ) min_temperature: Final = ZCLAttributeDef( id=HAOZEE_MIN_TEMP_LIMIT_ATTR, type=t.uint32_t ) target_temperature: Final = ZCLAttributeDef( id=HAOZEE_TARGET_TEMP_ATTR, type=t.uint32_t ) internal_temperature: Final = ZCLAttributeDef( id=HAOZEE_CURRENT_ROOM_TEMP_ATTR, type=t.uint32_t ) sensor_settings: Final = ZCLAttributeDef( id=HAOZEE_SENSOR_TYPE_ATTR, type=t.uint8_t ) poweron_behavior: Final = ZCLAttributeDef( id=HAOZEE_POWERON_BEHAVIOR_ATTR, type=t.uint8_t ) week_format: Final = ZCLAttributeDef(id=HAOZEE_WEEKFORMAT, type=t.uint8_t) mode: Final = ZCLAttributeDef(id=HAOZEE_CURRENT_MODE_ATTR, type=t.uint8_t) fault: Final = ZCLAttributeDef(id=HAOZEE_FAULT_ATTR, type=t.uint8_t) DIRECT_MAPPED_ATTRS = { HAOZEE_CURRENT_ROOM_TEMP_ATTR: ("local_temp", lambda value: value * 10), HAOZEE_TARGET_TEMP_ATTR: ( "occupied_heating_setpoint", lambda value: value * 10, ), HAOZEE_AWAY_TEMP_ATTR: ( "unoccupied_heating_setpoint", lambda value: value * 100, ), HAOZEE_TEMP_CALIBRATION_ATTR: ( "local_temperature_calibration", lambda value: value * 10, ), HAOZEE_MIN_TEMP_LIMIT_ATTR: ( "min_heat_setpoint_limit", lambda value: value * 100, ), HAOZEE_MAX_TEMP_LIMIT_ATTR: ( "max_heat_setpoint_limit", lambda value: value * 100, ), HAOZEE_AWAY_DAYS_ATTR: ("unoccupied_duration_days", None), } def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) if attrid in self.DIRECT_MAPPED_ATTRS: self.endpoint.device.thermostat_bus.listener_event( "temperature_change", self.DIRECT_MAPPED_ATTRS[attrid][0], value if self.DIRECT_MAPPED_ATTRS[attrid][1] is None else self.DIRECT_MAPPED_ATTRS[attrid][1]( value ), # decidegree to centidegree ) elif attrid == HAOZEE_ENABLED_ATTR: self.endpoint.device.thermostat_bus.listener_event("enabled_change", value) elif attrid == HAOZEE_HEATING_ENABLED_ATTR: self.endpoint.device.thermostat_bus.listener_event("state_change", value) elif attrid == HAOZEE_CURRENT_MODE_ATTR: self.endpoint.device.thermostat_bus.listener_event("mode_change", value) elif attrid == HAOZEE_CHILD_LOCK_ATTR: self.endpoint.device.ui_bus.listener_event("child_lock_change", value) class HY08WEThermostat(TuyaThermostatCluster): """Thermostat cluster for some thermostatic valves.""" DIRECT_MAPPING_ATTRS = { "occupied_heating_setpoint": ( HAOZEE_TARGET_TEMP_ATTR, lambda value: round(value / 10), ), "unoccupied_heating_setpoint": ( HAOZEE_AWAY_TEMP_ATTR, lambda value: round(value / 100), ), "min_heat_setpoint_limit": ( HAOZEE_MIN_TEMP_LIMIT_ATTR, lambda value: round(value / 100), ), "max_heat_setpoint_limit": ( HAOZEE_MAX_TEMP_LIMIT_ATTR, lambda value: round(value / 100), ), "local_temperature_calibration": ( HAOZEE_TEMP_CALIBRATION_ATTR, lambda value: round(value / 10), ), } def map_attribute(self, attribute, value): """Map standardized attribute value to dict of manufacturer values.""" if attribute in self.DIRECT_MAPPING_ATTRS: return { self.DIRECT_MAPPING_ATTRS[attribute][0]: value if self.DIRECT_MAPPING_ATTRS[attribute][1] is None else self.DIRECT_MAPPING_ATTRS[attribute][1](value) } if attribute == "system_mode": if value == self.SystemMode.Off: return {HAOZEE_ENABLED_ATTR: 0} if value == self.SystemMode.Heat: return {HAOZEE_ENABLED_ATTR: 1} self.error("Unsupported value for SystemMode") if attribute == "programing_oper_mode": if value == self.ProgrammingOperationMode.Simple: return {HAOZEE_CURRENT_MODE_ATTR: 0} if value == self.ProgrammingOperationMode.Schedule_programming_mode: return {HAOZEE_CURRENT_MODE_ATTR: 1} if value == self.ProgrammingOperationMode.Economy_mode: return {HAOZEE_CURRENT_MODE_ATTR: 2} self.error("Unsupported value for ProgrammingOperationMode") def mode_change(self, value): """System Mode change.""" occupancy = self.Occupancy.Occupied if value == 0: prog_mode = self.ProgrammingOperationMode.Simple elif value == 1: prog_mode = self.ProgrammingOperationMode.Schedule_programming_mode elif value == 2: prog_mode = self.ProgrammingOperationMode.Simple occupancy = self.Occupancy.Unoccupied self._update_attribute( self.attributes_by_name["programing_oper_mode"].id, prog_mode ) self._update_attribute(self.attributes_by_name["occupancy"].id, occupancy) def enabled_change(self, value): """System mode change.""" if value == 0: mode = self.SystemMode.Off else: mode = self.SystemMode.Heat self._update_attribute(self.attributes_by_name["system_mode"].id, mode) class HY08WEUserInterface(TuyaUserInterfaceCluster): """HVAC User interface cluster for tuya electric heating thermostats.""" _CHILD_LOCK_ATTR = HAOZEE_CHILD_LOCK_ATTR class HY08WE(TuyaThermostat): """Haozee HY08WE Thermostatic radiator valve.""" signature = { # endpoint=1 profile=260 device_type=81 device_version=1 input_clusters=[0, 4, 5, 61184], # output_clusters=[25, 10] MODELS_INFO: [("_TZE200_znzs7yaw", "TS0601")], ENDPOINTS: { 1: { PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, DEVICE_TYPE: zigpy.profiles.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: zigpy.profiles.zha.PROFILE_ID, DEVICE_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, INPUT_CLUSTERS: [ Basic.cluster_id, Groups.cluster_id, Scenes.cluster_id, HY08WEManufCluster, HY08WEThermostat, HY08WEUserInterface, ], OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], } } }