|
| 1 | +"""Map from manufacturer to standard clusters for thermostatic valves.""" |
| 2 | +import logging |
| 3 | + |
| 4 | +import zigpy.profiles.zha |
| 5 | +import zigpy.types as t |
| 6 | +from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time |
| 7 | + |
| 8 | +from zhaquirks.const import ( |
| 9 | + DEVICE_TYPE, |
| 10 | + ENDPOINTS, |
| 11 | + INPUT_CLUSTERS, |
| 12 | + MODELS_INFO, |
| 13 | + OUTPUT_CLUSTERS, |
| 14 | + PROFILE_ID, |
| 15 | +) |
| 16 | +from zhaquirks.tuya import ( |
| 17 | + TuyaManufClusterAttributes, |
| 18 | + TuyaThermostat, |
| 19 | + TuyaThermostatCluster, |
| 20 | + TuyaUserInterfaceCluster, |
| 21 | +) |
| 22 | + |
| 23 | +# from https://github.com/Koenkk/zigbee-herdsman-converters/pull/1694/ |
| 24 | +HAOZEE_SCHEDULE_WORKDAYS_AM_ATTR = ( |
| 25 | + 0x0077 # schedule for workdays [8, 0, 20, 8, 1, 15, 11, 30, 15] |
| 26 | +) |
| 27 | +HAOZEE_SCHEDULE_WORKDAYS_PM_ATTR = ( |
| 28 | + 0x0078 # schedule for workdays [13, 30, 15, 17, 0, 15, 150, 0, 15] |
| 29 | +) |
| 30 | +HAOZEE_SCHEDULE_WEEKEND_AM_ATTR = ( |
| 31 | + 0x0079 # schedule for weekend [6, 0, 20, 8, 0, 15, 11, 30, 15] |
| 32 | +) |
| 33 | +HAOZEE_SCHEDULE_WEEKEND_PM_ATTR = ( |
| 34 | + 0x007A # schedule for weekend [13, 30, 15, 17, 0, 15, 22, 0, 15] |
| 35 | +) |
| 36 | + |
| 37 | +HAOZEE_HEATING_ENABLED_ATTR = 0x0166 # [0] idle [1] heating |
| 38 | +HAOZEE_MAX_TEMP_PROTECTION_ENABLED_ATTR = ( |
| 39 | + 0x016A # minimal temp protection [0] disabled [1] enabled |
| 40 | +) |
| 41 | +HAOZEE_MIN_TEMP_PROTECTION_ENABLED_ATTR = ( |
| 42 | + 0x016B # maximal temp protection [0] disabled [1] enabled |
| 43 | +) |
| 44 | +HAOZEE_ENABLED_ATTR = 0x017D # device status [0] disabled [1] enabled |
| 45 | +HAOZEE_CHILD_LOCK_ATTR = 0x0181 # [0] unlocked [1] child-locked |
| 46 | +HAOZEE_EXT_TEMP_ATTR = 0x0267 # external NTC sensor temp (decidegree) |
| 47 | +HAOZEE_AWAY_DAYS_ATTR = 0x0268 # away mode duration (days) |
| 48 | +HAOZEE_AWAY_TEMP_ATTR = 0x0269 # away mode temperature (decidegree) |
| 49 | +HAOZEE_TEMP_CALIBRATION_ATTR = 0x026D # temperature calibration (decidegree) |
| 50 | +HAOZEE_TEMP_HYSTERESIS_ATTR = 0x026E # temperature hysteresis (decidegree) |
| 51 | +HAOZEE_TEMP_PROTECT_HYSTERESIS_ATTR = ( |
| 52 | + 0x026F # temperature hysteresis for protection (decidegree) |
| 53 | +) |
| 54 | +HAOZEE_MAX_PROTECT_TEMP_ATTR = ( |
| 55 | + 0x0270 # maximal temp for protection trigger (decidegree) |
| 56 | +) |
| 57 | +HAOZEE_MIN_PROTECT_TEMP_ATTR = ( |
| 58 | + 0x0271 # minimal temp for protection trigger (decidegree) |
| 59 | +) |
| 60 | +HAOZEE_MAX_TEMP_LIMIT_ATTR = 0x0272 # maximum limit of temperature setting (decidegree) |
| 61 | +HAOZEE_MIN_TEMP_LIMIT_ATTR = 0x0273 # minimum limit of temperature setting (decidegree) |
| 62 | +HAOZEE_TARGET_TEMP_ATTR = 0x027E # target temperature (decidegree) |
| 63 | +HAOZEE_CURRENT_ROOM_TEMP_ATTR = ( |
| 64 | + 0x027F # temperature reported by MCU. Depends on sensor type. (decidegree) |
| 65 | +) |
| 66 | +HAOZEE_SENSOR_TYPE_ATTR = 0x0474 # sensor type [0] internal [1] external [2] both |
| 67 | +HAOZEE_POWERON_BEHAVIOR_ATTR = 0x0475 # poweron behavior [0] restore [1] off [2] on |
| 68 | +HAOZEE_WEEKFORMAT = 0x0476 # [0] 5+2 [1] 6+1 [2] 7 (days) |
| 69 | + |
| 70 | +HAOZEE_CURRENT_MODE_ATTR = 0x0480 # [0] manual [1] auto [2] away |
| 71 | +HAOZEE_FAULT_ATTR = 0x0582 # Known fault codes: [4] E2 external sensor error |
| 72 | + |
| 73 | +# Unknown DP - descaling on/off, window detection, window detection settings |
| 74 | + |
| 75 | +_LOGGER = logging.getLogger(__name__) |
| 76 | + |
| 77 | + |
| 78 | +class HY08WEManufCluster(TuyaManufClusterAttributes): |
| 79 | + """Manufacturer Specific Cluster of some thermostatic valves.""" |
| 80 | + |
| 81 | + # Important! This device uses offset from 2000 year for UTC time and offset from 1970 for local time |
| 82 | + set_time_offset = 2000 |
| 83 | + set_time_local_offset = 1970 |
| 84 | + |
| 85 | + attributes = TuyaManufClusterAttributes.attributes.copy() |
| 86 | + attributes.update( |
| 87 | + { |
| 88 | + HAOZEE_HEATING_ENABLED_ATTR: ("heating_enabled", t.uint8_t), |
| 89 | + HAOZEE_MAX_TEMP_PROTECTION_ENABLED_ATTR: ( |
| 90 | + "max_temp_protection_enabled", |
| 91 | + t.uint8_t, |
| 92 | + ), |
| 93 | + HAOZEE_MIN_TEMP_PROTECTION_ENABLED_ATTR: ( |
| 94 | + "min_temp_protection_enabled", |
| 95 | + t.uint8_t, |
| 96 | + ), |
| 97 | + HAOZEE_ENABLED_ATTR: ("enabled", t.uint8_t), |
| 98 | + HAOZEE_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t), |
| 99 | + HAOZEE_EXT_TEMP_ATTR: ("external_temperature", t.uint32_t), |
| 100 | + HAOZEE_AWAY_DAYS_ATTR: ("away_duration_days", t.uint32_t), |
| 101 | + HAOZEE_AWAY_TEMP_ATTR: ("away_mode_temperature", t.uint32_t), |
| 102 | + HAOZEE_TEMP_CALIBRATION_ATTR: ("temperature_calibration", t.int32s), |
| 103 | + HAOZEE_TEMP_HYSTERESIS_ATTR: ("hysterisis_temperature", t.uint32_t), |
| 104 | + HAOZEE_TEMP_PROTECT_HYSTERESIS_ATTR: ( |
| 105 | + "hysterisis_protection_temperature", |
| 106 | + t.uint32_t, |
| 107 | + ), |
| 108 | + HAOZEE_MAX_PROTECT_TEMP_ATTR: ("max_protection_temperature", t.uint32_t), |
| 109 | + HAOZEE_MIN_PROTECT_TEMP_ATTR: ("min_protection_temperature", t.uint32_t), |
| 110 | + HAOZEE_MAX_TEMP_LIMIT_ATTR: ("max_temperature", t.uint32_t), |
| 111 | + HAOZEE_MIN_TEMP_LIMIT_ATTR: ("min_temperature", t.uint32_t), |
| 112 | + HAOZEE_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t), |
| 113 | + HAOZEE_CURRENT_ROOM_TEMP_ATTR: ("internal_temperature", t.uint32_t), |
| 114 | + HAOZEE_SENSOR_TYPE_ATTR: ("sensor_settings", t.uint8_t), |
| 115 | + HAOZEE_POWERON_BEHAVIOR_ATTR: ("poweron_behavior", t.uint8_t), |
| 116 | + HAOZEE_WEEKFORMAT: ("week_format", t.uint8_t), |
| 117 | + HAOZEE_CURRENT_MODE_ATTR: ("mode", t.uint8_t), |
| 118 | + HAOZEE_FAULT_ATTR: ("fault", t.uint8_t), |
| 119 | + } |
| 120 | + ) |
| 121 | + |
| 122 | + DIRECT_MAPPED_ATTRS = { |
| 123 | + HAOZEE_CURRENT_ROOM_TEMP_ATTR: ("local_temp", lambda value: value * 10), |
| 124 | + HAOZEE_TARGET_TEMP_ATTR: ( |
| 125 | + "occupied_heating_setpoint", |
| 126 | + lambda value: value * 10, |
| 127 | + ), |
| 128 | + HAOZEE_AWAY_TEMP_ATTR: ( |
| 129 | + "unoccupied_heating_setpoint", |
| 130 | + lambda value: value * 100, |
| 131 | + ), |
| 132 | + HAOZEE_TEMP_CALIBRATION_ATTR: ( |
| 133 | + "local_temperature_calibration", |
| 134 | + lambda value: value * 10, |
| 135 | + ), |
| 136 | + HAOZEE_MIN_TEMP_LIMIT_ATTR: ( |
| 137 | + "min_heat_setpoint_limit", |
| 138 | + lambda value: value * 100, |
| 139 | + ), |
| 140 | + HAOZEE_MAX_TEMP_LIMIT_ATTR: ( |
| 141 | + "max_heat_setpoint_limit", |
| 142 | + lambda value: value * 100, |
| 143 | + ), |
| 144 | + HAOZEE_AWAY_DAYS_ATTR: ("unoccupied_duration_days", None), |
| 145 | + } |
| 146 | + |
| 147 | + def _update_attribute(self, attrid, value): |
| 148 | + super()._update_attribute(attrid, value) |
| 149 | + if attrid in self.DIRECT_MAPPED_ATTRS: |
| 150 | + self.endpoint.device.thermostat_bus.listener_event( |
| 151 | + "temperature_change", |
| 152 | + self.DIRECT_MAPPED_ATTRS[attrid][0], |
| 153 | + value |
| 154 | + if self.DIRECT_MAPPED_ATTRS[attrid][1] is None |
| 155 | + else self.DIRECT_MAPPED_ATTRS[attrid][1]( |
| 156 | + value |
| 157 | + ), # decidegree to centidegree |
| 158 | + ) |
| 159 | + elif attrid == HAOZEE_ENABLED_ATTR: |
| 160 | + self.endpoint.device.thermostat_bus.listener_event("enabled_change", value) |
| 161 | + elif attrid == HAOZEE_HEATING_ENABLED_ATTR: |
| 162 | + self.endpoint.device.thermostat_bus.listener_event("state_change", value) |
| 163 | + elif attrid == HAOZEE_CURRENT_MODE_ATTR: |
| 164 | + self.endpoint.device.thermostat_bus.listener_event("mode_change", value) |
| 165 | + elif attrid == HAOZEE_CHILD_LOCK_ATTR: |
| 166 | + self.endpoint.device.ui_bus.listener_event("child_lock_change", value) |
| 167 | + |
| 168 | + |
| 169 | +class HY08WEThermostat(TuyaThermostatCluster): |
| 170 | + """Thermostat cluster for some thermostatic valves.""" |
| 171 | + |
| 172 | + DIRECT_MAPPING_ATTRS = { |
| 173 | + "occupied_heating_setpoint": ( |
| 174 | + HAOZEE_TARGET_TEMP_ATTR, |
| 175 | + lambda value: round(value / 10), |
| 176 | + ), |
| 177 | + "unoccupied_heating_setpoint": ( |
| 178 | + HAOZEE_AWAY_TEMP_ATTR, |
| 179 | + lambda value: round(value / 100), |
| 180 | + ), |
| 181 | + "min_heat_setpoint_limit": ( |
| 182 | + HAOZEE_MIN_TEMP_LIMIT_ATTR, |
| 183 | + lambda value: round(value / 100), |
| 184 | + ), |
| 185 | + "max_heat_setpoint_limit": ( |
| 186 | + HAOZEE_MAX_TEMP_LIMIT_ATTR, |
| 187 | + lambda value: round(value / 100), |
| 188 | + ), |
| 189 | + "local_temperature_calibration": ( |
| 190 | + HAOZEE_TEMP_CALIBRATION_ATTR, |
| 191 | + lambda value: round(value / 10), |
| 192 | + ), |
| 193 | + } |
| 194 | + |
| 195 | + def map_attribute(self, attribute, value): |
| 196 | + """Map standardized attribute value to dict of manufacturer values.""" |
| 197 | + |
| 198 | + if attribute in self.DIRECT_MAPPING_ATTRS: |
| 199 | + return { |
| 200 | + self.DIRECT_MAPPING_ATTRS[attribute][0]: value |
| 201 | + if self.DIRECT_MAPPING_ATTRS[attribute][1] is None |
| 202 | + else self.DIRECT_MAPPING_ATTRS[attribute][1](value) |
| 203 | + } |
| 204 | + |
| 205 | + if attribute == "system_mode": |
| 206 | + if value == self.SystemMode.Off: |
| 207 | + return {HAOZEE_ENABLED_ATTR: 0} |
| 208 | + if value == self.SystemMode.Heat: |
| 209 | + return {HAOZEE_ENABLED_ATTR: 1} |
| 210 | + self.error("Unsupported value for SystemMode") |
| 211 | + |
| 212 | + if attribute == "programing_oper_mode": |
| 213 | + if value == self.ProgrammingOperationMode.Simple: |
| 214 | + return {HAOZEE_CURRENT_MODE_ATTR: 0} |
| 215 | + if value == self.ProgrammingOperationMode.Schedule_programming_mode: |
| 216 | + return {HAOZEE_CURRENT_MODE_ATTR: 1} |
| 217 | + if value == self.ProgrammingOperationMode.Economy_mode: |
| 218 | + return {HAOZEE_CURRENT_MODE_ATTR: 2} |
| 219 | + self.error("Unsupported value for ProgrammingOperationMode") |
| 220 | + |
| 221 | + def mode_change(self, value): |
| 222 | + """System Mode change.""" |
| 223 | + occupancy = self.Occupancy.Occupied |
| 224 | + if value == 0: |
| 225 | + prog_mode = self.ProgrammingOperationMode.Simple |
| 226 | + elif value == 1: |
| 227 | + prog_mode = self.ProgrammingOperationMode.Schedule_programming_mode |
| 228 | + elif value == 2: |
| 229 | + prog_mode = self.ProgrammingOperationMode.Simple |
| 230 | + occupancy = self.Occupancy.Unoccupied |
| 231 | + self._update_attribute(self.attridx["programing_oper_mode"], prog_mode) |
| 232 | + self._update_attribute(self.attridx["occupancy"], occupancy) |
| 233 | + |
| 234 | + def enabled_change(self, value): |
| 235 | + """System mode change.""" |
| 236 | + if value == 0: |
| 237 | + mode = self.SystemMode.Off |
| 238 | + else: |
| 239 | + mode = self.SystemMode.Heat |
| 240 | + self._update_attribute(self.attridx["system_mode"], mode) |
| 241 | + |
| 242 | + |
| 243 | +class HY08WEUserInterface(TuyaUserInterfaceCluster): |
| 244 | + """HVAC User interface cluster for tuya electric heating thermostats.""" |
| 245 | + |
| 246 | + _CHILD_LOCK_ATTR = HAOZEE_CHILD_LOCK_ATTR |
| 247 | + |
| 248 | + |
| 249 | +class HY08WE(TuyaThermostat): |
| 250 | + """Haozee HY08WE Thermostatic radiator valve.""" |
| 251 | + |
| 252 | + signature = { |
| 253 | + # endpoint=1 profile=260 device_type=81 device_version=1 input_clusters=[0, 4, 5, 61184], |
| 254 | + # output_clusters=[25, 10] |
| 255 | + MODELS_INFO: [("_TZE200_znzs7yaw", "TS0601")], |
| 256 | + ENDPOINTS: { |
| 257 | + 1: { |
| 258 | + PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, |
| 259 | + DEVICE_TYPE: zigpy.profiles.zha.DeviceType.SMART_PLUG, |
| 260 | + INPUT_CLUSTERS: [ |
| 261 | + Basic.cluster_id, |
| 262 | + Groups.cluster_id, |
| 263 | + Scenes.cluster_id, |
| 264 | + TuyaManufClusterAttributes.cluster_id, |
| 265 | + ], |
| 266 | + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], |
| 267 | + } |
| 268 | + }, |
| 269 | + } |
| 270 | + |
| 271 | + replacement = { |
| 272 | + ENDPOINTS: { |
| 273 | + 1: { |
| 274 | + PROFILE_ID: zigpy.profiles.zha.PROFILE_ID, |
| 275 | + DEVICE_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, |
| 276 | + INPUT_CLUSTERS: [ |
| 277 | + Basic.cluster_id, |
| 278 | + Groups.cluster_id, |
| 279 | + Scenes.cluster_id, |
| 280 | + HY08WEManufCluster, |
| 281 | + HY08WEThermostat, |
| 282 | + HY08WEUserInterface, |
| 283 | + ], |
| 284 | + OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], |
| 285 | + } |
| 286 | + } |
| 287 | + } |
0 commit comments