From 301dfc6ef7c2ed909faa99e6bab24524df235a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Clau=C3=9Fen?= Date: Thu, 9 Nov 2023 09:37:33 +0100 Subject: [PATCH 01/40] Add Bosch Thermostat 2 quirk --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 161 +++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 zhaquirks/bosch/rbsh_trv0_zb_eu.py diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py new file mode 100644 index 0000000000..2094da5209 --- /dev/null +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -0,0 +1,161 @@ +"""Device handler for Bosch RBSH-TRV0-ZB-EU thermostat.""" + +from zigpy.profiles import zha +import zigpy.types as t +from zigpy.quirks import CustomDevice +from zigpy.zcl.clusters import general, hvac, homeautomation + +from zigpy.zcl.clusters.general import ( + Basic, + Identify, + Ota, + PollControl, + Groups, + Time, + PowerConfiguration, + ZCLAttributeDef, +) +from zigpy.zcl.clusters.hvac import Thermostat, UserInterface +from zigpy.zcl.clusters.homeautomation import Diagnostic + +from zhaquirks import CustomCluster +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + + +class BoschOperatingMode(t.enum8): + Schedule = 0x00 + Manual = 0x01 + Pause = 0x05 + + +class State(t.enum8): + Off = 0x00 + On = 0x01 + + +class BoschDisplayOrientation(t.enum8): + Normal = 0x00 + Flipped = 0x01 + + +class BoschDisplayedTemperature(t.enum8): + Target = 0x00 + Measured = 0x01 + + +class BoschThermostatCluster(CustomCluster, Thermostat): + """Bosch thermostat cluster.""" + + class AttributeDefs(Thermostat.AttributeDefs): + operating_mode = ZCLAttributeDef( + id=t.uint16_t(0x4007), + type=BoschOperatingMode, + is_manufacturer_specific=True, + ) + + pi_heating_demand = ZCLAttributeDef( + id=t.uint16_t(0x4020), + # Values range from 0-100 + type=t.enum8, + is_manufacturer_specific=True, + ) + + window_open = ZCLAttributeDef( + id=t.uint16_t(0x4042), + type=State, + is_manufacturer_specific=True, + ) + + boost = ZCLAttributeDef( + id=t.uint16_t(0x4043), + type=State, + is_manufacturer_specific=True, + ) + + +class BoschUserInterfaceCluster(CustomCluster, UserInterface): + """Bosch UserInterface cluster.""" + + class AttributeDefs(UserInterface.AttributeDefs): + display_orientation = ZCLAttributeDef( + id=t.uint16_t(0x400B), + type=BoschDisplayOrientation, + is_manufacturer_specific=True, + ) + + display_ontime = ZCLAttributeDef( + id=t.uint16_t(0x403A), + # Usable values range from 2-30 + type=t.enum8, + is_manufacturer_specific=True, + ) + + display_brightness = ZCLAttributeDef( + id=t.uint16_t(0x403B), + # Values range from 0-10 + type=t.enum8, + is_manufacturer_specific=True, + ) + + displayed_temperature = ZCLAttributeDef( + id=t.uint16_t(0x4039), + type=BoschDisplayedTemperature, + is_manufacturer_specific=True, + ) + + +class BoschThermostat(CustomDevice): + """Bosch thermostat custom device.""" + + signature = { + MODELS_INFO: [("BOSCH", "RBSH-TRV0-ZB-EU")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.THERMOSTAT, + INPUT_CLUSTERS: [ + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.Groups.cluster_id, + general.PollControl.cluster_id, + hvac.Thermostat.cluster_id, + hvac.UserInterface.cluster_id, + homeautomation.Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [general.Ota.cluster_id, + general.Time.cluster_id], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + INPUT_CLUSTERS: [ + Basic, + BoschThermostatCluster, + BoschUserInterfaceCluster, + Diagnostic, + Groups, + Identify, + Ota, + PollControl, + PowerConfiguration, + Time, + ], + OUTPUT_CLUSTERS: [Ota, Time], + }, + }, + } From 4ce4c3fad73678d231c03d0cca4c75a797d8adf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20N=C3=B6thlich?= Date: Sat, 23 Dec 2023 21:04:03 +0100 Subject: [PATCH 02/40] Format code for CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adrian Nöthlich --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 2094da5209..1a398c9453 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -1,22 +1,21 @@ """Device handler for Bosch RBSH-TRV0-ZB-EU thermostat.""" from zigpy.profiles import zha -import zigpy.types as t from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters import general, hvac, homeautomation - +import zigpy.types as t +from zigpy.zcl.clusters import general, homeautomation, hvac from zigpy.zcl.clusters.general import ( Basic, + Groups, Identify, Ota, PollControl, - Groups, - Time, PowerConfiguration, + Time, ZCLAttributeDef, ) -from zigpy.zcl.clusters.hvac import Thermostat, UserInterface from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.zcl.clusters.hvac import Thermostat, UserInterface from zhaquirks import CustomCluster from zhaquirks.const import ( @@ -134,8 +133,7 @@ class BoschThermostat(CustomDevice): hvac.UserInterface.cluster_id, homeautomation.Diagnostic.cluster_id, ], - OUTPUT_CLUSTERS: [general.Ota.cluster_id, - general.Time.cluster_id], + OUTPUT_CLUSTERS: [general.Ota.cluster_id, general.Time.cluster_id], }, }, } From 8e78cdf5c9b734eb907670d7916de1ff6f01cad4 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 2 May 2024 22:41:53 +0200 Subject: [PATCH 03/40] Update to using Quirks V2 API. Support for Bosh Room Thermostat II 230v 1. Bosch Room Thermostat II 230V: RBSH-RTH0-ZB-EU - exposes thermostat attributes: operating mode, window open, boost, pi heating demand - exposes user interface attributes: display on-time, display brightness 2. Bosch Radiator Thermostat II: RBSH-TRV0-ZB-EU - exposes thermostat attributes: operating mode, window open, boost, pi heating demand, remote temperature - exposes user interface attributes: display orientation, display on-time, display brightness, displayed temperature - fixes HVAC mode setting and reporting by linking system mode attribute to the operating mode attribute TBD: All attributes are exposed to the HA UI with zha ready available strings which might not match their intended purpose. To be determined why the Quirks V2 API support implementation in ZHA doesn't fallback to using the attribute name when no or unknown translation key is provided. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 142 +++++++++++ zhaquirks/bosch/rbsh_trv0_zb_eu.py | 363 ++++++++++++++++++++++------- 2 files changed, 425 insertions(+), 80 deletions(-) create mode 100644 zhaquirks/bosch/rbsh_rth0_zb_eu.py diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py new file mode 100644 index 0000000000..ad51f52579 --- /dev/null +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -0,0 +1,142 @@ + """Device handler for Bosch RBSH-RTH0-ZB-EU thermostat.""" + +from typing import Any, Final + +from zigpy.device import Device +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster +from zigpy.quirks.registry import DeviceRegistry +from zigpy.quirks.v2 import ( + CustomDeviceV2, + add_to_registry_v2, +) +from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass +import zigpy.types as t +from zigpy.zcl import ClusterType +from zigpy.zcl.clusters.hvac import Thermostat, UserInterface +from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef, ZCLCommandDef + +"""Bosch specific thermostat attribute ids.""" +OPERATING_MODE_ATTR_ID = 0x4007 +VALVE_POSITION_ATTR_ID = 0x4020 +WINDOW_OPEN_ATTR_ID = 0x4042 +BOOST_ATTR_ID = 0x4043 + +"""Bosch specific user interface attribute ids.""" +SCREEN_TIMEOUT_ATTR_ID = 0x403a +SCREEN_BRIGHTNESS_ATTR_ID = 0x403b + +"""Bosh operating mode attribute values.""" +class BoschOperatingMode(t.enum8): + Schedule = 0x00 + Manual = 0x01 + Pause = 0x05 + +"""Bosch thermostat preset.""" +class BoschPreset(t.enum8): + Normal = 0x00 + Boost = 0x01 + +"""Binary attribute (window open) value.""" +class State(t.enum8): + Off = 0x00 + On = 0x01 + +class BoschThermostatCluster(CustomCluster, Thermostat): + """Bosch thermostat cluster.""" + + class AttributeDefs(Thermostat.AttributeDefs): + operating_mode = ZCLAttributeDef( + id=t.uint16_t(OPERATING_MODE_ATTR_ID), + type=BoschOperatingMode, + is_manufacturer_specific=True, + ) + + pi_heating_demand = ZCLAttributeDef( + id=t.uint16_t(VALVE_POSITION_ATTR_ID), + # Values range from 0-100 + type=t.enum8, + is_manufacturer_specific=True, + ) + + window_open = ZCLAttributeDef( + id=t.uint16_t(WINDOW_OPEN_ATTR_ID), + type=State, + is_manufacturer_specific=True, + ) + + boost = ZCLAttributeDef( + id=t.uint16_t(BOOST_ATTR_ID), + type=BoschPreset, + is_manufacturer_specific=True, + ) + + +class BoschUserInterfaceCluster(CustomCluster, UserInterface): + """Bosch UserInterface cluster.""" + + class AttributeDefs(UserInterface.AttributeDefs): + display_ontime = ZCLAttributeDef( + id=t.uint16_t(SCREEN_TIMEOUT_ATTR_ID), + # Usable values range from 5-30 + type=t.enum8, + is_manufacturer_specific=True, + ) + + display_brightness = ZCLAttributeDef( + id=t.uint16_t(SCREEN_BRIGHTNESS_ATTR_ID), + # Values range from 0-10 + type=t.enum8, + is_manufacturer_specific=True, + ) + + +class BoschThermostat(CustomDeviceV2): + """Bosch thermostat custom device.""" + +( + add_to_registry_v2( + "Bosch", "RBSH-RTH0-ZB-EU" + ) + .device_class(BoschThermostat) + .replaces(BoschThermostatCluster) + .replaces(BoschUserInterfaceCluster) + # Operating mode: controlled automatically through Thermostat.system_mode (HAVC mode). + .enum( + BoschThermostatCluster.AttributeDefs.operating_mode.name, + BoschOperatingMode, + BoschThermostatCluster.cluster_id, + translation_key="switch_mode" + ) + # Preset - normal/boost. + .enum( + BoschThermostatCluster.AttributeDefs.boost.name, + BoschPreset, + BoschThermostatCluster.cluster_id, + translation_key="preset" + ) + # Window open switch: manually set or through an automation. + .switch( + BoschThermostatCluster.AttributeDefs.window_open.name, + BoschThermostatCluster.cluster_id, + translation_key="window_detection" + ) + # Display time-out + .number( + BoschUserInterfaceCluster.AttributeDefs.display_ontime.name, + BoschUserInterfaceCluster.cluster_id, + min_value=5, + max_value=30, + step=1, + translation_key="on_off_transition_time" + ) + # Display brightness + .number( + BoschUserInterfaceCluster.AttributeDefs.display_brightness.name, + BoschUserInterfaceCluster.cluster_id, + min_value=0, + max_value=10, + step=1, + translation_key="backlight_mode" + ) +) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 1a398c9453..31e9cfc2db 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -1,159 +1,362 @@ """Device handler for Bosch RBSH-TRV0-ZB-EU thermostat.""" +from typing import Any, Final + +from zigpy.device import Device from zigpy.profiles import zha -from zigpy.quirks import CustomDevice -import zigpy.types as t -from zigpy.zcl.clusters import general, homeautomation, hvac -from zigpy.zcl.clusters.general import ( - Basic, - Groups, - Identify, - Ota, - PollControl, - PowerConfiguration, - Time, - ZCLAttributeDef, +from zigpy.quirks import CustomCluster +from zigpy.quirks.registry import DeviceRegistry +from zigpy.quirks.v2 import ( + CustomDeviceV2, + add_to_registry_v2, ) -from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass +import zigpy.types as t +from zigpy.zcl import ClusterType from zigpy.zcl.clusters.hvac import Thermostat, UserInterface +from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef, ZCLCommandDef -from zhaquirks import CustomCluster -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) +"""Bosch specific thermostat attribute ids.""" +OPERATING_MODE_ATTR_ID = 0x4007 +VALVE_POSITION_ATTR_ID = 0x4020 +REMOTE_TEMPERATURE_ATTR_ID = 0x4040 +WINDOW_OPEN_ATTR_ID = 0x4042 +BOOST_ATTR_ID = 0x4043 +"""Bosch specific user interface attribute ids.""" +SCREEN_ORIENTATION_ATTR_ID = 0x400b +DISPLAY_MODE_ATTR_ID = 0x4039 +SCREEN_TIMEOUT_ATTR_ID = 0x403a +SCREEN_BRIGHTNESS_ATTR_ID = 0x403b +"""Bosh operating mode attribute values.""" class BoschOperatingMode(t.enum8): Schedule = 0x00 Manual = 0x01 Pause = 0x05 +"""Bosch thermostat preset.""" +class BoschPreset(t.enum8): + Normal = 0x00 + Boost = 0x01 +"""Binary attribute (window open) value.""" class State(t.enum8): Off = 0x00 On = 0x01 - +"""Bosch display orientation attribute values.""" class BoschDisplayOrientation(t.enum8): Normal = 0x00 Flipped = 0x01 - +"""Bosch displayed temperature attribute values.""" class BoschDisplayedTemperature(t.enum8): Target = 0x00 Measured = 0x01 +"""HA thermostat attribute that needs special handling in the Bosch thermostat entity.""" +SYSTEM_MODE_ATTR = Thermostat.AttributeDefs.system_mode + +"""Bosch operating mode to HA system mode mapping.""" +OPERATING_MODE_TO_SYSTEM_MODE_MAP = { + BoschOperatingMode.Schedule: Thermostat.SystemMode.Auto, + BoschOperatingMode.Manual: Thermostat.SystemMode.Heat, + BoschOperatingMode.Pause: Thermostat.SystemMode.Off, + "BoschOperatingMode.Schedule": Thermostat.SystemMode.Auto, + "BoschOperatingMode.Manual": Thermostat.SystemMode.Heat, + "BoschOperatingMode.Pause": Thermostat.SystemMode.Off +} + +"""HA system mode to Bosch operating mode mapping.""" +SYSTEM_MODE_TO_OPERATING_MODE_MAP = { + Thermostat.SystemMode.Off: BoschOperatingMode.Pause, + Thermostat.SystemMode.Heat: BoschOperatingMode.Manual, + Thermostat.SystemMode.Auto: BoschOperatingMode.Schedule, + "SystemMode.Off": BoschOperatingMode.Pause, + "SystemMode.Heat": BoschOperatingMode.Manual, + "SystemMode.Auto": BoschOperatingMode.Schedule +} + +DISPLAY_ORIENTATION_ENUM_TO_INT_MAP = { + 0x00: 0x00, + 0x01: 0x01, + "BoschDisplayOrientation.Normal": 0x00, + "BoschDisplayOrientation.Flipped": 0x01 +} class BoschThermostatCluster(CustomCluster, Thermostat): """Bosch thermostat cluster.""" class AttributeDefs(Thermostat.AttributeDefs): operating_mode = ZCLAttributeDef( - id=t.uint16_t(0x4007), + id=t.uint16_t(OPERATING_MODE_ATTR_ID), type=BoschOperatingMode, is_manufacturer_specific=True, ) pi_heating_demand = ZCLAttributeDef( - id=t.uint16_t(0x4020), + id=t.uint16_t(VALVE_POSITION_ATTR_ID), # Values range from 0-100 type=t.enum8, is_manufacturer_specific=True, ) window_open = ZCLAttributeDef( - id=t.uint16_t(0x4042), + id=t.uint16_t(WINDOW_OPEN_ATTR_ID), type=State, is_manufacturer_specific=True, ) boost = ZCLAttributeDef( - id=t.uint16_t(0x4043), - type=State, + id=t.uint16_t(BOOST_ATTR_ID), + type=BoschPreset, is_manufacturer_specific=True, ) + remote_temperature = ZCLAttributeDef( + id=t.uint16_t(REMOTE_TEMPERATURE_ATTR_ID), + type=t.int16s, + is_manufacturer_specific=True, + ) + + async def write_attributes( + self, + attributes: dict[str | int, Any], + manufacturer: int | None = None + ) -> list: + """system_mode special handling: + - turn off by setting operating_mode to Pause + - turn on by setting operating_mode to Manual + - add new system_mode value to the internal zigpy Cluster cache + """ + + operating_mode_attr = self.AttributeDefs.operating_mode + + result = [] + remaining_attributes = attributes.copy() + system_mode_value = None + operating_mode_value = None + + """Check if SYSTEM_MODE_ATTR is being written (can be numeric or string): + - do not write it to the device since it is not supported + - keep the value to be converted to the supported operating_mode + """ + if SYSTEM_MODE_ATTR.id in attributes: + remaining_attributes.pop(SYSTEM_MODE_ATTR.id) + system_mode_value = attributes.get(SYSTEM_MODE_ATTR.id) + elif SYSTEM_MODE_ATTR.name in attributes: + remaining_attributes.pop(SYSTEM_MODE_ATTR.name) + system_mode_value = attributes.get(SYSTEM_MODE_ATTR.name) + + """Check if operating_mode_attr is being written (can be numeric or string). + - ignore incoming operating_mode when system_mode is also written + - system_mode has priority and its value would be converted to operating_mode + - add resulting system_mode to the internal zigpy Cluster cache + """ + operating_mode_attribute_id = None + if operating_mode_attr.id in attributes: + operating_mode_attribute_id = operating_mode_attr.id + elif operating_mode_attr.name in attributes: + operating_mode_attribute_id = operating_mode_attr.name + + if operating_mode_attribute_id is not None: + if system_mode_value is not None: + operating_mode_value = remaining_attributes.pop(operating_mode_attribute_id) + else: + operating_mode_value = attributes.get(operating_mode_attribute_id) + + if system_mode_value is not None: + """Write operating_mode (from system_mode value).""" + new_operating_mode_value = SYSTEM_MODE_TO_OPERATING_MODE_MAP[system_mode_value] + result += await super().write_attributes({operating_mode_attr.id: new_operating_mode_value}, manufacturer) + self._update_attribute(SYSTEM_MODE_ATTR.id, system_mode_value) + elif operating_mode_value is not None: + new_system_mode_value = OPERATING_MODE_TO_SYSTEM_MODE_MAP[operating_mode_value] + self._update_attribute(SYSTEM_MODE_ATTR.id, new_system_mode_value) + + """Write the remaining attributes to thermostat cluster.""" + if remaining_attributes: + result += await super().write_attributes(remaining_attributes, manufacturer) + return result + + + async def read_attributes( + self, + attributes: list[int | str], + allow_cache: bool = False, + only_cache: bool = False, + manufacturer: int | t.uint16_t | None = None, + ): + """system_mode special handling: + - read and convert operating_mode to system_mode. + """ + + operating_mode_attr = self.AttributeDefs.operating_mode + + successful_r, failed_r = {}, {} + remaining_attributes = attributes.copy() + system_mode_attribute_id = None + + """Check if SYSTEM_MODE_ATTR is being read (can be numeric or string).""" + if SYSTEM_MODE_ATTR.id in attributes: + system_mode_attribute_id = SYSTEM_MODE_ATTR.id + elif SYSTEM_MODE_ATTR.name in attributes: + system_mode_attribute_id = SYSTEM_MODE_ATTR.name + + """Read operating_mode instead and convert it to system_mode.""" + if system_mode_attribute_id is not None: + remaining_attributes.remove(system_mode_attribute_id) + successful_r, failed_r = await super().read_attributes( + [operating_mode_attr.name], allow_cache, only_cache, manufacturer + ) + if operating_mode_attr.name in successful_r: + operating_mode_value = successful_r.pop(operating_mode_attr.name) + system_mode_value = OPERATING_MODE_TO_SYSTEM_MODE_MAP[operating_mode_value] + successful_r[system_mode_attribute_id] = system_mode_value + self._update_attribute(SYSTEM_MODE_ATTR.id, system_mode_value) + + """Read remaining attributes from thermostat cluster.""" + if remaining_attributes: + remaining_result = await super().read_attributes( + remaining_attributes, allow_cache, only_cache, manufacturer + ) + + successful_r.update(remaining_result[0]) + failed_r.update(remaining_result[1]) + + return successful_r, failed_r + class BoschUserInterfaceCluster(CustomCluster, UserInterface): """Bosch UserInterface cluster.""" class AttributeDefs(UserInterface.AttributeDefs): display_orientation = ZCLAttributeDef( - id=t.uint16_t(0x400B), - type=BoschDisplayOrientation, + id=t.uint16_t(SCREEN_ORIENTATION_ATTR_ID), + # To be matched to BoschDisplayOrientation enum. + type=t.uint8_t, is_manufacturer_specific=True, ) display_ontime = ZCLAttributeDef( - id=t.uint16_t(0x403A), - # Usable values range from 2-30 + id=t.uint16_t(SCREEN_TIMEOUT_ATTR_ID), + # Usable values range from 5-30 type=t.enum8, is_manufacturer_specific=True, ) display_brightness = ZCLAttributeDef( - id=t.uint16_t(0x403B), + id=t.uint16_t(SCREEN_BRIGHTNESS_ATTR_ID), # Values range from 0-10 type=t.enum8, is_manufacturer_specific=True, ) displayed_temperature = ZCLAttributeDef( - id=t.uint16_t(0x4039), + id=t.uint16_t(DISPLAY_MODE_ATTR_ID), type=BoschDisplayedTemperature, is_manufacturer_specific=True, ) + async def write_attributes( + self, + attributes: dict[str | int, Any], + manufacturer: int | None = None + ) -> list: + """display_orientation special handling: + - convert from enum to uint8_t + """ + display_orientation_attr = self.AttributeDefs.display_orientation + + remaining_attributes = attributes.copy() + display_orientation_attribute_id = None + + """Check if display_orientation is being written (can be numeric or string).""" + if display_orientation_attr.id in attributes: + display_orientation_attribute_id = display_orientation_attr.id + elif display_orientation_attr.name in attributes: + display_orientation_attribute_id = display_orientation_attr.name -class BoschThermostat(CustomDevice): + if display_orientation_attribute_id is not None: + display_orientation_value = remaining_attributes.pop(display_orientation_attr.id) + new_display_orientation_value = DISPLAY_ORIENTATION_ENUM_TO_INT_MAP[display_orientation_value] + remaining_attributes[display_orientation_attribute_id] = new_display_orientation_value + + return await super().write_attributes(remaining_attributes, manufacturer) + + +class BoschThermostat(CustomDeviceV2): """Bosch thermostat custom device.""" - signature = { - MODELS_INFO: [("BOSCH", "RBSH-TRV0-ZB-EU")], - ENDPOINTS: { - # - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.THERMOSTAT, - INPUT_CLUSTERS: [ - general.Basic.cluster_id, - general.PowerConfiguration.cluster_id, - general.Identify.cluster_id, - general.Groups.cluster_id, - general.PollControl.cluster_id, - hvac.Thermostat.cluster_id, - hvac.UserInterface.cluster_id, - homeautomation.Diagnostic.cluster_id, - ], - OUTPUT_CLUSTERS: [general.Ota.cluster_id, general.Time.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - INPUT_CLUSTERS: [ - Basic, - BoschThermostatCluster, - BoschUserInterfaceCluster, - Diagnostic, - Groups, - Identify, - Ota, - PollControl, - PowerConfiguration, - Time, - ], - OUTPUT_CLUSTERS: [Ota, Time], - }, - }, - } +( + add_to_registry_v2( + "BOSCH", "RBSH-TRV0-ZB-EU" + ) + .device_class(BoschThermostat) + .replaces(BoschThermostatCluster) + .replaces(BoschUserInterfaceCluster) + # Operating mode: controlled automatically through Thermostat.system_mode (HAVC mode). + .enum( + BoschThermostatCluster.AttributeDefs.operating_mode.name, + BoschOperatingMode, + BoschThermostatCluster.cluster_id, + translation_key="switch_mode" + ) + # Preset - normal/boost. + .enum( + BoschThermostatCluster.AttributeDefs.boost.name, + BoschPreset, + BoschThermostatCluster.cluster_id, + translation_key="preset" + ) + # Window open switch: manually set or through an automation. + .switch( + BoschThermostatCluster.AttributeDefs.window_open.name, + BoschThermostatCluster.cluster_id, + translation_key="window_detection" + ) + # Remote temperature + .number( + BoschThermostatCluster.AttributeDefs.remote_temperature.name, + BoschThermostatCluster.cluster_id, + min_value=5, + max_value=30, + step=0.1, + multiplier=100, + device_class=NumberDeviceClass.TEMPERATURE, + #translation_key="external_sensor" + ) + # Display temperature. + .enum( + BoschUserInterfaceCluster.AttributeDefs.displayed_temperature.name, + BoschDisplayedTemperature, + BoschUserInterfaceCluster.cluster_id, + translation_key="device_temperature" + ) + # Display orientation + .enum( + BoschUserInterfaceCluster.AttributeDefs.display_orientation.name, + BoschDisplayOrientation, + BoschUserInterfaceCluster.cluster_id, + translation_key="inverted" + ) + # Display time-out + .number( + BoschUserInterfaceCluster.AttributeDefs.display_ontime.name, + BoschUserInterfaceCluster.cluster_id, + min_value=5, + max_value=30, + step=1, + translation_key="on_off_transition_time" + ) + # Display brightness + .number( + BoschUserInterfaceCluster.AttributeDefs.display_brightness.name, + BoschUserInterfaceCluster.cluster_id, + min_value=0, + max_value=10, + step=1, + translation_key="backlight_mode" + ) +) From 69f07bc803b78cec7524fd9efd85cf884a15b1b3 Mon Sep 17 00:00:00 2001 From: mrrstux <131166996+mrrstux@users.noreply.github.com> Date: Thu, 2 May 2024 22:51:49 +0200 Subject: [PATCH 04/40] Update rbsh_rth0_zb_eu.py --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index ad51f52579..8c2b24e775 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -1,4 +1,4 @@ - """Device handler for Bosch RBSH-RTH0-ZB-EU thermostat.""" +"""Device handler for Bosch RBSH-RTH0-ZB-EU thermostat.""" from typing import Any, Final From f179645e5853e4fda258d0b7c3eff49f6a3c2479 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 2 May 2024 23:23:59 +0200 Subject: [PATCH 05/40] Code formating using black. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 28 +++++--- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 100 ++++++++++++++++++----------- 2 files changed, 81 insertions(+), 47 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 8c2b24e775..f677fd1faf 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -23,25 +23,34 @@ BOOST_ATTR_ID = 0x4043 """Bosch specific user interface attribute ids.""" -SCREEN_TIMEOUT_ATTR_ID = 0x403a -SCREEN_BRIGHTNESS_ATTR_ID = 0x403b +SCREEN_TIMEOUT_ATTR_ID = 0x403A +SCREEN_BRIGHTNESS_ATTR_ID = 0x403B """Bosh operating mode attribute values.""" + + class BoschOperatingMode(t.enum8): Schedule = 0x00 Manual = 0x01 Pause = 0x05 + """Bosch thermostat preset.""" + + class BoschPreset(t.enum8): Normal = 0x00 Boost = 0x01 + """Binary attribute (window open) value.""" + + class State(t.enum8): Off = 0x00 On = 0x01 + class BoschThermostatCluster(CustomCluster, Thermostat): """Bosch thermostat cluster.""" @@ -94,10 +103,9 @@ class AttributeDefs(UserInterface.AttributeDefs): class BoschThermostat(CustomDeviceV2): """Bosch thermostat custom device.""" + ( - add_to_registry_v2( - "Bosch", "RBSH-RTH0-ZB-EU" - ) + add_to_registry_v2("Bosch", "RBSH-RTH0-ZB-EU") .device_class(BoschThermostat) .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) @@ -106,20 +114,20 @@ class BoschThermostat(CustomDeviceV2): BoschThermostatCluster.AttributeDefs.operating_mode.name, BoschOperatingMode, BoschThermostatCluster.cluster_id, - translation_key="switch_mode" + translation_key="switch_mode", ) # Preset - normal/boost. .enum( BoschThermostatCluster.AttributeDefs.boost.name, BoschPreset, BoschThermostatCluster.cluster_id, - translation_key="preset" + translation_key="preset", ) # Window open switch: manually set or through an automation. .switch( BoschThermostatCluster.AttributeDefs.window_open.name, BoschThermostatCluster.cluster_id, - translation_key="window_detection" + translation_key="window_detection", ) # Display time-out .number( @@ -128,7 +136,7 @@ class BoschThermostat(CustomDeviceV2): min_value=5, max_value=30, step=1, - translation_key="on_off_transition_time" + translation_key="on_off_transition_time", ) # Display brightness .number( @@ -137,6 +145,6 @@ class BoschThermostat(CustomDeviceV2): min_value=0, max_value=10, step=1, - translation_key="backlight_mode" + translation_key="backlight_mode", ) ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 31e9cfc2db..654f108daa 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -24,37 +24,52 @@ BOOST_ATTR_ID = 0x4043 """Bosch specific user interface attribute ids.""" -SCREEN_ORIENTATION_ATTR_ID = 0x400b +SCREEN_ORIENTATION_ATTR_ID = 0x400B DISPLAY_MODE_ATTR_ID = 0x4039 -SCREEN_TIMEOUT_ATTR_ID = 0x403a -SCREEN_BRIGHTNESS_ATTR_ID = 0x403b +SCREEN_TIMEOUT_ATTR_ID = 0x403A +SCREEN_BRIGHTNESS_ATTR_ID = 0x403B """Bosh operating mode attribute values.""" + + class BoschOperatingMode(t.enum8): Schedule = 0x00 Manual = 0x01 Pause = 0x05 + """Bosch thermostat preset.""" + + class BoschPreset(t.enum8): Normal = 0x00 Boost = 0x01 + """Binary attribute (window open) value.""" + + class State(t.enum8): Off = 0x00 On = 0x01 + """Bosch display orientation attribute values.""" + + class BoschDisplayOrientation(t.enum8): Normal = 0x00 Flipped = 0x01 + """Bosch displayed temperature attribute values.""" + + class BoschDisplayedTemperature(t.enum8): Target = 0x00 Measured = 0x01 + """HA thermostat attribute that needs special handling in the Bosch thermostat entity.""" SYSTEM_MODE_ATTR = Thermostat.AttributeDefs.system_mode @@ -65,7 +80,7 @@ class BoschDisplayedTemperature(t.enum8): BoschOperatingMode.Pause: Thermostat.SystemMode.Off, "BoschOperatingMode.Schedule": Thermostat.SystemMode.Auto, "BoschOperatingMode.Manual": Thermostat.SystemMode.Heat, - "BoschOperatingMode.Pause": Thermostat.SystemMode.Off + "BoschOperatingMode.Pause": Thermostat.SystemMode.Off, } """HA system mode to Bosch operating mode mapping.""" @@ -75,16 +90,17 @@ class BoschDisplayedTemperature(t.enum8): Thermostat.SystemMode.Auto: BoschOperatingMode.Schedule, "SystemMode.Off": BoschOperatingMode.Pause, "SystemMode.Heat": BoschOperatingMode.Manual, - "SystemMode.Auto": BoschOperatingMode.Schedule + "SystemMode.Auto": BoschOperatingMode.Schedule, } DISPLAY_ORIENTATION_ENUM_TO_INT_MAP = { 0x00: 0x00, 0x01: 0x01, "BoschDisplayOrientation.Normal": 0x00, - "BoschDisplayOrientation.Flipped": 0x01 + "BoschDisplayOrientation.Flipped": 0x01, } + class BoschThermostatCluster(CustomCluster, Thermostat): """Bosch thermostat cluster.""" @@ -121,14 +137,12 @@ class AttributeDefs(Thermostat.AttributeDefs): ) async def write_attributes( - self, - attributes: dict[str | int, Any], - manufacturer: int | None = None + self, attributes: dict[str | int, Any], manufacturer: int | None = None ) -> list: """system_mode special handling: - - turn off by setting operating_mode to Pause - - turn on by setting operating_mode to Manual - - add new system_mode value to the internal zigpy Cluster cache + - turn off by setting operating_mode to Pause + - turn on by setting operating_mode to Manual + - add new system_mode value to the internal zigpy Cluster cache """ operating_mode_attr = self.AttributeDefs.operating_mode @@ -162,17 +176,25 @@ async def write_attributes( if operating_mode_attribute_id is not None: if system_mode_value is not None: - operating_mode_value = remaining_attributes.pop(operating_mode_attribute_id) + operating_mode_value = remaining_attributes.pop( + operating_mode_attribute_id + ) else: operating_mode_value = attributes.get(operating_mode_attribute_id) if system_mode_value is not None: """Write operating_mode (from system_mode value).""" - new_operating_mode_value = SYSTEM_MODE_TO_OPERATING_MODE_MAP[system_mode_value] - result += await super().write_attributes({operating_mode_attr.id: new_operating_mode_value}, manufacturer) + new_operating_mode_value = SYSTEM_MODE_TO_OPERATING_MODE_MAP[ + system_mode_value + ] + result += await super().write_attributes( + {operating_mode_attr.id: new_operating_mode_value}, manufacturer + ) self._update_attribute(SYSTEM_MODE_ATTR.id, system_mode_value) elif operating_mode_value is not None: - new_system_mode_value = OPERATING_MODE_TO_SYSTEM_MODE_MAP[operating_mode_value] + new_system_mode_value = OPERATING_MODE_TO_SYSTEM_MODE_MAP[ + operating_mode_value + ] self._update_attribute(SYSTEM_MODE_ATTR.id, new_system_mode_value) """Write the remaining attributes to thermostat cluster.""" @@ -180,7 +202,6 @@ async def write_attributes( result += await super().write_attributes(remaining_attributes, manufacturer) return result - async def read_attributes( self, attributes: list[int | str], @@ -189,7 +210,7 @@ async def read_attributes( manufacturer: int | t.uint16_t | None = None, ): """system_mode special handling: - - read and convert operating_mode to system_mode. + - read and convert operating_mode to system_mode. """ operating_mode_attr = self.AttributeDefs.operating_mode @@ -212,7 +233,9 @@ async def read_attributes( ) if operating_mode_attr.name in successful_r: operating_mode_value = successful_r.pop(operating_mode_attr.name) - system_mode_value = OPERATING_MODE_TO_SYSTEM_MODE_MAP[operating_mode_value] + system_mode_value = OPERATING_MODE_TO_SYSTEM_MODE_MAP[ + operating_mode_value + ] successful_r[system_mode_attribute_id] = system_mode_value self._update_attribute(SYSTEM_MODE_ATTR.id, system_mode_value) @@ -260,12 +283,10 @@ class AttributeDefs(UserInterface.AttributeDefs): ) async def write_attributes( - self, - attributes: dict[str | int, Any], - manufacturer: int | None = None + self, attributes: dict[str | int, Any], manufacturer: int | None = None ) -> list: """display_orientation special handling: - - convert from enum to uint8_t + - convert from enum to uint8_t """ display_orientation_attr = self.AttributeDefs.display_orientation @@ -279,9 +300,15 @@ async def write_attributes( display_orientation_attribute_id = display_orientation_attr.name if display_orientation_attribute_id is not None: - display_orientation_value = remaining_attributes.pop(display_orientation_attr.id) - new_display_orientation_value = DISPLAY_ORIENTATION_ENUM_TO_INT_MAP[display_orientation_value] - remaining_attributes[display_orientation_attribute_id] = new_display_orientation_value + display_orientation_value = remaining_attributes.pop( + display_orientation_attr.id + ) + new_display_orientation_value = DISPLAY_ORIENTATION_ENUM_TO_INT_MAP[ + display_orientation_value + ] + remaining_attributes[display_orientation_attribute_id] = ( + new_display_orientation_value + ) return await super().write_attributes(remaining_attributes, manufacturer) @@ -289,10 +316,9 @@ async def write_attributes( class BoschThermostat(CustomDeviceV2): """Bosch thermostat custom device.""" + ( - add_to_registry_v2( - "BOSCH", "RBSH-TRV0-ZB-EU" - ) + add_to_registry_v2("BOSCH", "RBSH-TRV0-ZB-EU") .device_class(BoschThermostat) .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) @@ -301,20 +327,20 @@ class BoschThermostat(CustomDeviceV2): BoschThermostatCluster.AttributeDefs.operating_mode.name, BoschOperatingMode, BoschThermostatCluster.cluster_id, - translation_key="switch_mode" + translation_key="switch_mode", ) # Preset - normal/boost. .enum( BoschThermostatCluster.AttributeDefs.boost.name, BoschPreset, BoschThermostatCluster.cluster_id, - translation_key="preset" + translation_key="preset", ) # Window open switch: manually set or through an automation. .switch( BoschThermostatCluster.AttributeDefs.window_open.name, BoschThermostatCluster.cluster_id, - translation_key="window_detection" + translation_key="window_detection", ) # Remote temperature .number( @@ -325,21 +351,21 @@ class BoschThermostat(CustomDeviceV2): step=0.1, multiplier=100, device_class=NumberDeviceClass.TEMPERATURE, - #translation_key="external_sensor" + # translation_key="external_sensor" ) # Display temperature. .enum( BoschUserInterfaceCluster.AttributeDefs.displayed_temperature.name, BoschDisplayedTemperature, BoschUserInterfaceCluster.cluster_id, - translation_key="device_temperature" + translation_key="device_temperature", ) # Display orientation .enum( BoschUserInterfaceCluster.AttributeDefs.display_orientation.name, BoschDisplayOrientation, BoschUserInterfaceCluster.cluster_id, - translation_key="inverted" + translation_key="inverted", ) # Display time-out .number( @@ -348,7 +374,7 @@ class BoschThermostat(CustomDeviceV2): min_value=5, max_value=30, step=1, - translation_key="on_off_transition_time" + translation_key="on_off_transition_time", ) # Display brightness .number( @@ -357,6 +383,6 @@ class BoschThermostat(CustomDeviceV2): min_value=0, max_value=10, step=1, - translation_key="backlight_mode" + translation_key="backlight_mode", ) ) From fa98ea47b6a129312f688bbdfb0f5d0dd3814d41 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 2 May 2024 23:41:36 +0200 Subject: [PATCH 06/40] Rework code comments. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 30 ++++++++++++------ zhaquirks/bosch/rbsh_trv0_zb_eu.py | 50 +++++++++++++++++++----------- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index f677fd1faf..7986933161 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -17,36 +17,46 @@ from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef, ZCLCommandDef """Bosch specific thermostat attribute ids.""" + +# Mode of operation with values BoschOperatingMode. OPERATING_MODE_ATTR_ID = 0x4007 + +# Valve position: 0% - 100% VALVE_POSITION_ATTR_ID = 0x4020 + +# Window open switch (changes to a lower target temperature when on). WINDOW_OPEN_ATTR_ID = 0x4042 + +# Boost preset mode. BOOST_ATTR_ID = 0x4043 """Bosch specific user interface attribute ids.""" + +# Display on-time (5s - 30s). SCREEN_TIMEOUT_ATTR_ID = 0x403A -SCREEN_BRIGHTNESS_ATTR_ID = 0x403B -"""Bosh operating mode attribute values.""" +# Display brightness (0 - 10). +SCREEN_BRIGHTNESS_ATTR_ID = 0x403B class BoschOperatingMode(t.enum8): + """Bosh operating mode attribute values.""" + Schedule = 0x00 Manual = 0x01 Pause = 0x05 -"""Bosch thermostat preset.""" - - class BoschPreset(t.enum8): + """Bosch thermostat preset.""" + Normal = 0x00 Boost = 0x01 -"""Binary attribute (window open) value.""" - - class State(t.enum8): + """Binary attribute (window open) value.""" + Off = 0x00 On = 0x01 @@ -129,7 +139,7 @@ class BoschThermostat(CustomDeviceV2): BoschThermostatCluster.cluster_id, translation_key="window_detection", ) - # Display time-out + # Display time-out. .number( BoschUserInterfaceCluster.AttributeDefs.display_ontime.name, BoschUserInterfaceCluster.cluster_id, @@ -138,7 +148,7 @@ class BoschThermostat(CustomDeviceV2): step=1, translation_key="on_off_transition_time", ) - # Display brightness + # Display brightness. .number( BoschUserInterfaceCluster.AttributeDefs.display_brightness.name, BoschUserInterfaceCluster.cluster_id, diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 654f108daa..ef0dfc7a6f 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -17,55 +17,69 @@ from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef, ZCLCommandDef """Bosch specific thermostat attribute ids.""" + +# Mode of operation with values BoschOperatingMode. OPERATING_MODE_ATTR_ID = 0x4007 + +# Valve position: 0% - 100% VALVE_POSITION_ATTR_ID = 0x4020 + +# Remote measured temperature. REMOTE_TEMPERATURE_ATTR_ID = 0x4040 + +# Window open switch (changes to a lower target temperature when on). WINDOW_OPEN_ATTR_ID = 0x4042 + +# Boost preset mode. BOOST_ATTR_ID = 0x4043 """Bosch specific user interface attribute ids.""" + +# Display orientation with values BoschDisplayOrientation. SCREEN_ORIENTATION_ATTR_ID = 0x400B + +# Displayed temperature with values BoschDisplayedTemperature. DISPLAY_MODE_ATTR_ID = 0x4039 + +# Display on-time (5s - 30s). SCREEN_TIMEOUT_ATTR_ID = 0x403A -SCREEN_BRIGHTNESS_ATTR_ID = 0x403B -"""Bosh operating mode attribute values.""" +# Display brightness (0 - 10). +SCREEN_BRIGHTNESS_ATTR_ID = 0x403B class BoschOperatingMode(t.enum8): + """Bosh operating mode attribute values.""" + Schedule = 0x00 Manual = 0x01 Pause = 0x05 -"""Bosch thermostat preset.""" - - class BoschPreset(t.enum8): + """Bosch thermostat preset.""" + Normal = 0x00 Boost = 0x01 -"""Binary attribute (window open) value.""" - - class State(t.enum8): + """Binary attribute (window open) value.""" + Off = 0x00 On = 0x01 -"""Bosch display orientation attribute values.""" - - class BoschDisplayOrientation(t.enum8): + """Bosch display orientation attribute values.""" + Normal = 0x00 Flipped = 0x01 -"""Bosch displayed temperature attribute values.""" - - class BoschDisplayedTemperature(t.enum8): + """Bosch displayed temperature attribute values.""" + Target = 0x00 Measured = 0x01 @@ -342,7 +356,7 @@ class BoschThermostat(CustomDeviceV2): BoschThermostatCluster.cluster_id, translation_key="window_detection", ) - # Remote temperature + # Remote temperature. .number( BoschThermostatCluster.AttributeDefs.remote_temperature.name, BoschThermostatCluster.cluster_id, @@ -360,14 +374,14 @@ class BoschThermostat(CustomDeviceV2): BoschUserInterfaceCluster.cluster_id, translation_key="device_temperature", ) - # Display orientation + # Display orientation. .enum( BoschUserInterfaceCluster.AttributeDefs.display_orientation.name, BoschDisplayOrientation, BoschUserInterfaceCluster.cluster_id, translation_key="inverted", ) - # Display time-out + # Display time-out. .number( BoschUserInterfaceCluster.AttributeDefs.display_ontime.name, BoschUserInterfaceCluster.cluster_id, @@ -376,7 +390,7 @@ class BoschThermostat(CustomDeviceV2): step=1, translation_key="on_off_transition_time", ) - # Display brightness + # Display brightness. .number( BoschUserInterfaceCluster.AttributeDefs.display_brightness.name, BoschUserInterfaceCluster.cluster_id, From 5f9c720b25857902274b8faefd555da4a3af660e Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 7 May 2024 23:20:27 +0200 Subject: [PATCH 07/40] Switch exposed "boost" entity from enum to switch. Use exposed attribute names as entity translation keys. Expected list of translations: ------------------------------------------------ | | | ------------------------------------------------- | operating_mode | Operating mode | | boost | Boost | | window_open | Window open | | remote_temperature | Remote temperature | | displayed_temperature | Displayed temperature | | display_orientation | Display orientation | | display_on_time | Display ON time | | display_brightness | Display brightness | ------------------------------------------------- --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 28 +++++++++--------------- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 34 ++++++++++++------------------ 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 7986933161..dd06937d4d 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -47,13 +47,6 @@ class BoschOperatingMode(t.enum8): Pause = 0x05 -class BoschPreset(t.enum8): - """Bosch thermostat preset.""" - - Normal = 0x00 - Boost = 0x01 - - class State(t.enum8): """Binary attribute (window open) value.""" @@ -86,7 +79,7 @@ class AttributeDefs(Thermostat.AttributeDefs): boost = ZCLAttributeDef( id=t.uint16_t(BOOST_ATTR_ID), - type=BoschPreset, + type=State, is_manufacturer_specific=True, ) @@ -95,7 +88,7 @@ class BoschUserInterfaceCluster(CustomCluster, UserInterface): """Bosch UserInterface cluster.""" class AttributeDefs(UserInterface.AttributeDefs): - display_ontime = ZCLAttributeDef( + display_on_time = ZCLAttributeDef( id=t.uint16_t(SCREEN_TIMEOUT_ATTR_ID), # Usable values range from 5-30 type=t.enum8, @@ -124,29 +117,28 @@ class BoschThermostat(CustomDeviceV2): BoschThermostatCluster.AttributeDefs.operating_mode.name, BoschOperatingMode, BoschThermostatCluster.cluster_id, - translation_key="switch_mode", + translation_key="operating_mode", ) - # Preset - normal/boost. - .enum( + # Fast heating/boost. + .switch( BoschThermostatCluster.AttributeDefs.boost.name, - BoschPreset, BoschThermostatCluster.cluster_id, - translation_key="preset", + translation_key="boost", ) # Window open switch: manually set or through an automation. .switch( BoschThermostatCluster.AttributeDefs.window_open.name, BoschThermostatCluster.cluster_id, - translation_key="window_detection", + translation_key="window_open", ) # Display time-out. .number( - BoschUserInterfaceCluster.AttributeDefs.display_ontime.name, + BoschUserInterfaceCluster.AttributeDefs.display_on_time.name, BoschUserInterfaceCluster.cluster_id, min_value=5, max_value=30, step=1, - translation_key="on_off_transition_time", + translation_key="display_on_time", ) # Display brightness. .number( @@ -155,6 +147,6 @@ class BoschThermostat(CustomDeviceV2): min_value=0, max_value=10, step=1, - translation_key="backlight_mode", + translation_key="display_brightness", ) ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index ef0dfc7a6f..5cbf2c88a9 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -56,13 +56,6 @@ class BoschOperatingMode(t.enum8): Pause = 0x05 -class BoschPreset(t.enum8): - """Bosch thermostat preset.""" - - Normal = 0x00 - Boost = 0x01 - - class State(t.enum8): """Binary attribute (window open) value.""" @@ -140,7 +133,7 @@ class AttributeDefs(Thermostat.AttributeDefs): boost = ZCLAttributeDef( id=t.uint16_t(BOOST_ATTR_ID), - type=BoschPreset, + type=State, is_manufacturer_specific=True, ) @@ -276,7 +269,7 @@ class AttributeDefs(UserInterface.AttributeDefs): is_manufacturer_specific=True, ) - display_ontime = ZCLAttributeDef( + display_on_time = ZCLAttributeDef( id=t.uint16_t(SCREEN_TIMEOUT_ATTR_ID), # Usable values range from 5-30 type=t.enum8, @@ -341,20 +334,19 @@ class BoschThermostat(CustomDeviceV2): BoschThermostatCluster.AttributeDefs.operating_mode.name, BoschOperatingMode, BoschThermostatCluster.cluster_id, - translation_key="switch_mode", + translation_key="operating_mode", ) - # Preset - normal/boost. - .enum( + # Fast heating/boost. + .switch( BoschThermostatCluster.AttributeDefs.boost.name, - BoschPreset, BoschThermostatCluster.cluster_id, - translation_key="preset", + translation_key="boost", ) # Window open switch: manually set or through an automation. .switch( BoschThermostatCluster.AttributeDefs.window_open.name, BoschThermostatCluster.cluster_id, - translation_key="window_detection", + translation_key="window_open", ) # Remote temperature. .number( @@ -365,30 +357,30 @@ class BoschThermostat(CustomDeviceV2): step=0.1, multiplier=100, device_class=NumberDeviceClass.TEMPERATURE, - # translation_key="external_sensor" + # translation_key="remote_temperature" ) # Display temperature. .enum( BoschUserInterfaceCluster.AttributeDefs.displayed_temperature.name, BoschDisplayedTemperature, BoschUserInterfaceCluster.cluster_id, - translation_key="device_temperature", + translation_key="displayed_temperature", ) # Display orientation. .enum( BoschUserInterfaceCluster.AttributeDefs.display_orientation.name, BoschDisplayOrientation, BoschUserInterfaceCluster.cluster_id, - translation_key="inverted", + translation_key="display_orientation", ) # Display time-out. .number( - BoschUserInterfaceCluster.AttributeDefs.display_ontime.name, + BoschUserInterfaceCluster.AttributeDefs.display_on_time.name, BoschUserInterfaceCluster.cluster_id, min_value=5, max_value=30, step=1, - translation_key="on_off_transition_time", + translation_key="display_on_time", ) # Display brightness. .number( @@ -397,6 +389,6 @@ class BoschThermostat(CustomDeviceV2): min_value=0, max_value=10, step=1, - translation_key="backlight_mode", + translation_key="display_brightness", ) ) From 2840a7f86a5a6dfdff9fe2706f35310b3e642180 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 7 May 2024 23:39:11 +0200 Subject: [PATCH 08/40] Format code for CI. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 17 ++++++----------- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 30 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index dd06937d4d..9b119cf1e6 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -1,20 +1,11 @@ """Device handler for Bosch RBSH-RTH0-ZB-EU thermostat.""" -from typing import Any, Final -from zigpy.device import Device -from zigpy.profiles import zha from zigpy.quirks import CustomCluster -from zigpy.quirks.registry import DeviceRegistry -from zigpy.quirks.v2 import ( - CustomDeviceV2, - add_to_registry_v2, -) -from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass +from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 import zigpy.types as t -from zigpy.zcl import ClusterType from zigpy.zcl.clusters.hvac import Thermostat, UserInterface -from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef, ZCLCommandDef +from zigpy.zcl.foundation import ZCLAttributeDef """Bosch specific thermostat attribute ids.""" @@ -58,6 +49,8 @@ class BoschThermostatCluster(CustomCluster, Thermostat): """Bosch thermostat cluster.""" class AttributeDefs(Thermostat.AttributeDefs): + """Bosch thermostat manufacturer specific attributes.""" + operating_mode = ZCLAttributeDef( id=t.uint16_t(OPERATING_MODE_ATTR_ID), type=BoschOperatingMode, @@ -88,6 +81,8 @@ class BoschUserInterfaceCluster(CustomCluster, UserInterface): """Bosch UserInterface cluster.""" class AttributeDefs(UserInterface.AttributeDefs): + """Bosch user interface manufacturer specific attributes.""" + display_on_time = ZCLAttributeDef( id=t.uint16_t(SCREEN_TIMEOUT_ATTR_ID), # Usable values range from 5-30 diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 5cbf2c88a9..6ef47f20ff 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -1,20 +1,13 @@ """Device handler for Bosch RBSH-TRV0-ZB-EU thermostat.""" -from typing import Any, Final +from typing import Any -from zigpy.device import Device -from zigpy.profiles import zha from zigpy.quirks import CustomCluster -from zigpy.quirks.registry import DeviceRegistry -from zigpy.quirks.v2 import ( - CustomDeviceV2, - add_to_registry_v2, -) +from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass import zigpy.types as t -from zigpy.zcl import ClusterType from zigpy.zcl.clusters.hvac import Thermostat, UserInterface -from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef, ZCLCommandDef +from zigpy.zcl.foundation import ZCLAttributeDef """Bosch specific thermostat attribute ids.""" @@ -112,6 +105,8 @@ class BoschThermostatCluster(CustomCluster, Thermostat): """Bosch thermostat cluster.""" class AttributeDefs(Thermostat.AttributeDefs): + """Bosch thermostat manufacturer specific attributes.""" + operating_mode = ZCLAttributeDef( id=t.uint16_t(OPERATING_MODE_ATTR_ID), type=BoschOperatingMode, @@ -146,7 +141,8 @@ class AttributeDefs(Thermostat.AttributeDefs): async def write_attributes( self, attributes: dict[str | int, Any], manufacturer: int | None = None ) -> list: - """system_mode special handling: + """system_mode special handling. + - turn off by setting operating_mode to Pause - turn on by setting operating_mode to Manual - add new system_mode value to the internal zigpy Cluster cache @@ -161,7 +157,7 @@ async def write_attributes( """Check if SYSTEM_MODE_ATTR is being written (can be numeric or string): - do not write it to the device since it is not supported - - keep the value to be converted to the supported operating_mode + - keep the value to be converted to the supported operating_mode """ if SYSTEM_MODE_ATTR.id in attributes: remaining_attributes.pop(SYSTEM_MODE_ATTR.id) @@ -171,7 +167,7 @@ async def write_attributes( system_mode_value = attributes.get(SYSTEM_MODE_ATTR.name) """Check if operating_mode_attr is being written (can be numeric or string). - - ignore incoming operating_mode when system_mode is also written + - ignore incoming operating_mode when system_mode is also written - system_mode has priority and its value would be converted to operating_mode - add resulting system_mode to the internal zigpy Cluster cache """ @@ -216,7 +212,8 @@ async def read_attributes( only_cache: bool = False, manufacturer: int | t.uint16_t | None = None, ): - """system_mode special handling: + """system_mode special handling. + - read and convert operating_mode to system_mode. """ @@ -262,6 +259,8 @@ class BoschUserInterfaceCluster(CustomCluster, UserInterface): """Bosch UserInterface cluster.""" class AttributeDefs(UserInterface.AttributeDefs): + """Bosch user interface manufacturer specific attributes.""" + display_orientation = ZCLAttributeDef( id=t.uint16_t(SCREEN_ORIENTATION_ATTR_ID), # To be matched to BoschDisplayOrientation enum. @@ -292,7 +291,8 @@ class AttributeDefs(UserInterface.AttributeDefs): async def write_attributes( self, attributes: dict[str | int, Any], manufacturer: int | None = None ) -> list: - """display_orientation special handling: + """display_orientation special handling. + - convert from enum to uint8_t """ display_orientation_attr = self.AttributeDefs.display_orientation From 6130a864c35dcf6b909e7fa127fe80c55f6b11e6 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 7 May 2024 23:51:33 +0200 Subject: [PATCH 09/40] Remove empty custom device definition. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 7 +------ zhaquirks/bosch/rbsh_trv0_zb_eu.py | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 9b119cf1e6..64628b39d6 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -2,7 +2,7 @@ from zigpy.quirks import CustomCluster -from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 +from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t from zigpy.zcl.clusters.hvac import Thermostat, UserInterface from zigpy.zcl.foundation import ZCLAttributeDef @@ -98,13 +98,8 @@ class AttributeDefs(UserInterface.AttributeDefs): ) -class BoschThermostat(CustomDeviceV2): - """Bosch thermostat custom device.""" - - ( add_to_registry_v2("Bosch", "RBSH-RTH0-ZB-EU") - .device_class(BoschThermostat) .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) # Operating mode: controlled automatically through Thermostat.system_mode (HAVC mode). diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 6ef47f20ff..980d2a9e64 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -3,7 +3,7 @@ from typing import Any from zigpy.quirks import CustomCluster -from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 +from zigpy.quirks.v2 import add_to_registry_v2 from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass import zigpy.types as t from zigpy.zcl.clusters.hvac import Thermostat, UserInterface @@ -320,13 +320,8 @@ async def write_attributes( return await super().write_attributes(remaining_attributes, manufacturer) -class BoschThermostat(CustomDeviceV2): - """Bosch thermostat custom device.""" - - ( add_to_registry_v2("BOSCH", "RBSH-TRV0-ZB-EU") - .device_class(BoschThermostat) .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) # Operating mode: controlled automatically through Thermostat.system_mode (HAVC mode). From 4665bf84e1db6fb2ecc7e735609074f7d88ad86d Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 12 May 2024 17:26:16 +0200 Subject: [PATCH 10/40] Integrated @lgraf work on exposing ctrl_sequence_of_oper. New translation: ctrl_sequence_of_oper = "Control sequence of operation". --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 21 ++++++++++++++++++++- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 20 +++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 64628b39d6..996ba52962 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -4,7 +4,11 @@ from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t -from zigpy.zcl.clusters.hvac import Thermostat, UserInterface +from zigpy.zcl.clusters.hvac import ( + ControlSequenceOfOperation, + Thermostat, + UserInterface, +) from zigpy.zcl.foundation import ZCLAttributeDef """Bosch specific thermostat attribute ids.""" @@ -29,6 +33,8 @@ # Display brightness (0 - 10). SCREEN_BRIGHTNESS_ATTR_ID = 0x403B +# Control sequence of operation (heating/cooling) +CTRL_SEQUENCE_OF_OPERATION_ID = 0x001B class BoschOperatingMode(t.enum8): """Bosh operating mode attribute values.""" @@ -45,6 +51,13 @@ class State(t.enum8): On = 0x01 +class BoschControlSequenceOfOperation(t.enum8): + """Supported ControlSequenceOfOperation modes.""" + + Cooling = ControlSequenceOfOperation.Cooling_Only + Heating = ControlSequenceOfOperation.Heating_Only + + class BoschThermostatCluster(CustomCluster, Thermostat): """Bosch thermostat cluster.""" @@ -139,4 +152,10 @@ class AttributeDefs(UserInterface.AttributeDefs): step=1, translation_key="display_brightness", ) + .enum( + Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, + BoschControlSequenceOfOperation, + BoschThermostatCluster.cluster_id, + translation_key="ctrl_sequence_of_oper", + ) ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 980d2a9e64..9098f8dd0d 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -6,7 +6,11 @@ from zigpy.quirks.v2 import add_to_registry_v2 from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass import zigpy.types as t -from zigpy.zcl.clusters.hvac import Thermostat, UserInterface +from zigpy.zcl.clusters.hvac import ( + ControlSequenceOfOperation, + Thermostat, + UserInterface, +) from zigpy.zcl.foundation import ZCLAttributeDef """Bosch specific thermostat attribute ids.""" @@ -40,6 +44,8 @@ # Display brightness (0 - 10). SCREEN_BRIGHTNESS_ATTR_ID = 0x403B +# Control sequence of operation (heating/cooling) +CTRL_SEQUENCE_OF_OPERATION_ID = 0x001B class BoschOperatingMode(t.enum8): """Bosh operating mode attribute values.""" @@ -70,6 +76,13 @@ class BoschDisplayedTemperature(t.enum8): Measured = 0x01 +class BoschControlSequenceOfOperation(t.enum8): + """Supported ControlSequenceOfOperation modes.""" + + Cooling = ControlSequenceOfOperation.Cooling_Only + Heating = ControlSequenceOfOperation.Heating_Only + + """HA thermostat attribute that needs special handling in the Bosch thermostat entity.""" SYSTEM_MODE_ATTR = Thermostat.AttributeDefs.system_mode @@ -385,5 +398,10 @@ async def write_attributes( max_value=10, step=1, translation_key="display_brightness", + ).enum( + Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, + BoschControlSequenceOfOperation, + BoschThermostatCluster.cluster_id, + translation_key="ctrl_sequence_of_oper", ) ) From 6944c381afbf7eacddf6eaf7aff0955154f1b4d3 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 12 May 2024 20:19:37 +0200 Subject: [PATCH 11/40] Fix cooling mode support for radiator thermostat. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 1 + zhaquirks/bosch/rbsh_trv0_zb_eu.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 996ba52962..7c623764de 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -152,6 +152,7 @@ class AttributeDefs(UserInterface.AttributeDefs): step=1, translation_key="display_brightness", ) + # Heating vs Cooling. .enum( Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, BoschControlSequenceOfOperation, diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 9098f8dd0d..65d0e7ef48 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -100,9 +100,11 @@ class BoschControlSequenceOfOperation(t.enum8): SYSTEM_MODE_TO_OPERATING_MODE_MAP = { Thermostat.SystemMode.Off: BoschOperatingMode.Pause, Thermostat.SystemMode.Heat: BoschOperatingMode.Manual, + Thermostat.SystemMode.Cool: BoschOperatingMode.Manual, Thermostat.SystemMode.Auto: BoschOperatingMode.Schedule, "SystemMode.Off": BoschOperatingMode.Pause, "SystemMode.Heat": BoschOperatingMode.Manual, + "SystemMode.Cool": BoschOperatingMode.Manual, "SystemMode.Auto": BoschOperatingMode.Schedule, } @@ -245,14 +247,25 @@ async def read_attributes( """Read operating_mode instead and convert it to system_mode.""" if system_mode_attribute_id is not None: remaining_attributes.remove(system_mode_attribute_id) + + ctrl_sequence_of_oper_attr = Thermostat.AttributeDefs.ctrl_sequence_of_oper + successful_r, failed_r = await super().read_attributes( - [operating_mode_attr.name], allow_cache, only_cache, manufacturer + [operating_mode_attr.name, ctrl_sequence_of_oper_attr.name], allow_cache, only_cache, manufacturer ) if operating_mode_attr.name in successful_r: operating_mode_value = successful_r.pop(operating_mode_attr.name) system_mode_value = OPERATING_MODE_TO_SYSTEM_MODE_MAP[ operating_mode_value ] + + """Heating or cooling? Depends on both operating_mode and ctrl_sequence_of_operation.""" + ctrl_sequence_of_oper_value = None + if ctrl_sequence_of_oper_attr.name in successful_r: + ctrl_sequence_of_oper_value = successful_r.pop(ctrl_sequence_of_oper_attr.name) + if ctrl_sequence_of_oper_value == BoschControlSequenceOfOperation.Cooling and system_mode_value == Thermostat.SystemMode.Heat: + system_mode_value = Thermostat.SystemMode.Cool + successful_r[system_mode_attribute_id] = system_mode_value self._update_attribute(SYSTEM_MODE_ATTR.id, system_mode_value) @@ -398,6 +411,7 @@ async def write_attributes( max_value=10, step=1, translation_key="display_brightness", + # Heating vs Cooling. ).enum( Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, BoschControlSequenceOfOperation, From 841bebb992f9937dbd6590c40fa746ea8e7ec428 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 12 May 2024 20:46:34 +0200 Subject: [PATCH 12/40] Fix display orientation handling for radiator thermostat. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 65d0e7ef48..feeccd4c09 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -334,7 +334,7 @@ async def write_attributes( if display_orientation_attribute_id is not None: display_orientation_value = remaining_attributes.pop( - display_orientation_attr.id + display_orientation_attribute_id ) new_display_orientation_value = DISPLAY_ORIENTATION_ENUM_TO_INT_MAP[ display_orientation_value From 036ba191855ed4bea7bca54643f6c1675b00ef5b Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 14 May 2024 21:51:14 +0200 Subject: [PATCH 13/40] Write-attributes tests. --- tests/test_bosch.py | 188 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 tests/test_bosch.py diff --git a/tests/test_bosch.py b/tests/test_bosch.py new file mode 100644 index 0000000000..4b3275f735 --- /dev/null +++ b/tests/test_bosch.py @@ -0,0 +1,188 @@ +"""Tests the Bosch thermostats quirk.""" +from unittest import mock + +from zigpy.zcl import foundation +from zigpy.zcl.clusters.hvac import ControlSequenceOfOperation, Thermostat +from zigpy.zcl.foundation import WriteAttributesStatusRecord + +import zhaquirks +from zhaquirks.bosch.rbsh_trv0_zb_eu import ( + BoschOperatingMode, + BoschThermostatCluster as BoschTrvThermostatCluster, +) + +zhaquirks.setup() + +async def test_bosch_radiator_thermostat_II_write_attributes(zigpy_device_from_v2_quirk): + """Test the Radiator Thermostat II writes behaving correctly.""" + + device = zigpy_device_from_v2_quirk(manufacturer="BOSCH", model="RBSH-TRV0-ZB-EU") + + bosch_thermostat_cluster = device.endpoints[1].thermostat + + def mock_write(attributes, manufacturer=None): + records = [ + WriteAttributesStatusRecord(foundation.Status.SUCCESS) + for _ in attributes + ] + return [records, []] + + # data is written to trv + patch_bosch_trv_write = mock.patch.object( + bosch_thermostat_cluster, + "_write_attributes", + mock.AsyncMock(side_effect=mock_write), + ) + + # check that system_mode ends-up writing operating_mode: + with patch_bosch_trv_write: + # - Heating operation + success, fail = await bosch_thermostat_cluster.write_attributes( + {"ctrl_sequence_of_oper": ControlSequenceOfOperation.Heating_Only} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + + # -- Off + success, fail = await bosch_thermostat_cluster.write_attributes( + {"system_mode": Thermostat.SystemMode.Off} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off + assert bosch_thermostat_cluster._attr_cache[BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + + # -- Heat + success, fail = await bosch_thermostat_cluster.write_attributes( + {"system_mode": Thermostat.SystemMode.Heat} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Heat + assert bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + + # - Cooling operation + success, fail = await bosch_thermostat_cluster.write_attributes( + {"ctrl_sequence_of_oper": ControlSequenceOfOperation.Cooling_Only} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + + # -- Off + success, fail = await bosch_thermostat_cluster.write_attributes( + {"system_mode": Thermostat.SystemMode.Off} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off + assert bosch_thermostat_cluster._attr_cache[BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + + # -- Cool + success, fail = await bosch_thermostat_cluster.write_attributes( + {"system_mode": Thermostat.SystemMode.Cool} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Cool + assert bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + + +async def test_bosch_room_thermostat_II_230v_write_attributes(zigpy_device_from_v2_quirk): + """Test the Room Thermostat II 230v system_mode writes behaving correctly.""" + + device = zigpy_device_from_v2_quirk(manufacturer="Bosch", model="RBSH-RTH0-ZB-EU") + + bosch_thermostat_cluster = device.endpoints[1].thermostat + + def mock_write(attributes, manufacturer=None): + records = [ + WriteAttributesStatusRecord(foundation.Status.SUCCESS) + for _ in attributes + ] + return [records, []] + + # data is written to trv + patch_bosch_trv_write = mock.patch.object( + bosch_thermostat_cluster, + "_write_attributes", + mock.AsyncMock(side_effect=mock_write), + ) + + with patch_bosch_trv_write: + # check that system_mode ends-up writing operating_mode: + + # - Heating operation + success, fail = await bosch_thermostat_cluster.write_attributes( + {"ctrl_sequence_of_oper": ControlSequenceOfOperation.Heating_Only} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + + # -- Off + success, fail = await bosch_thermostat_cluster.write_attributes( + {"system_mode": Thermostat.SystemMode.Off} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + + # -- Heat + success, fail = await bosch_thermostat_cluster.write_attributes( + {"system_mode": Thermostat.SystemMode.Heat} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Heat + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + + # - Cooling operation + success, fail = await bosch_thermostat_cluster.write_attributes( + {"ctrl_sequence_of_oper": ControlSequenceOfOperation.Cooling_Only} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + + # -- Off + success, fail = await bosch_thermostat_cluster.write_attributes( + {"system_mode": Thermostat.SystemMode.Off} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + + # -- Cool + success, fail = await bosch_thermostat_cluster.write_attributes( + {"system_mode": Thermostat.SystemMode.Cool} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Cool + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only From 41ae0d4aa2060c19bcd7e90552fd1d3f97ba3767 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 14 May 2024 22:52:51 +0200 Subject: [PATCH 14/40] Thermostat UI write attributes tests. --- tests/test_bosch.py | 180 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 168 insertions(+), 12 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 4b3275f735..9b46e75d14 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -7,8 +7,10 @@ import zhaquirks from zhaquirks.bosch.rbsh_trv0_zb_eu import ( + BoschDisplayOrientation, BoschOperatingMode, BoschThermostatCluster as BoschTrvThermostatCluster, + BoschUserInterfaceCluster as BoschTrvUserInterfaceCluster, ) zhaquirks.setup() @@ -45,9 +47,9 @@ def mock_write(attributes, manufacturer=None): assert bosch_thermostat_cluster._attr_cache[ Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only - # -- Off + # -- Off (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( - {"system_mode": Thermostat.SystemMode.Off} + {Thermostat.AttributeDefs.system_mode.name: Thermostat.SystemMode.Off} ) assert success assert not fail @@ -56,9 +58,9 @@ def mock_write(attributes, manufacturer=None): assert bosch_thermostat_cluster._attr_cache[ Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only - # -- Heat + # -- Heat (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( - {"system_mode": Thermostat.SystemMode.Heat} + {Thermostat.AttributeDefs.system_mode.name: Thermostat.SystemMode.Heat} ) assert success assert not fail @@ -69,6 +71,54 @@ def mock_write(attributes, manufacturer=None): assert bosch_thermostat_cluster._attr_cache[ Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + # -- Off (by-id) + success, fail = await bosch_thermostat_cluster.write_attributes( + {Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Off} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off + assert bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + + # -- Heat (by-id) + success, fail = await bosch_thermostat_cluster.write_attributes( + {Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Heat} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Heat + assert bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + + # -- operating_mode (by-id) changes system_mode + success, fail = await bosch_thermostat_cluster.write_attributes( + {BoschTrvThermostatCluster.AttributeDefs.operating_mode.id: BoschOperatingMode.Pause} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off + assert bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause + + # -- operating_mode (by-name) changes system_mode + success, fail = await bosch_thermostat_cluster.write_attributes( + {BoschTrvThermostatCluster.AttributeDefs.operating_mode.name: BoschOperatingMode.Manual} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Heat + assert bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual + # - Cooling operation success, fail = await bosch_thermostat_cluster.write_attributes( {"ctrl_sequence_of_oper": ControlSequenceOfOperation.Cooling_Only} @@ -78,9 +128,9 @@ def mock_write(attributes, manufacturer=None): assert bosch_thermostat_cluster._attr_cache[ Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only - # -- Off + # -- Off (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( - {"system_mode": Thermostat.SystemMode.Off} + {Thermostat.AttributeDefs.system_mode.name: Thermostat.SystemMode.Off} ) assert success assert not fail @@ -89,9 +139,9 @@ def mock_write(attributes, manufacturer=None): assert bosch_thermostat_cluster._attr_cache[ Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only - # -- Cool + # -- Cool (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( - {"system_mode": Thermostat.SystemMode.Cool} + {Thermostat.AttributeDefs.system_mode.name: Thermostat.SystemMode.Cool} ) assert success assert not fail @@ -102,6 +152,53 @@ def mock_write(attributes, manufacturer=None): assert bosch_thermostat_cluster._attr_cache[ Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + # -- Off (by-id) + success, fail = await bosch_thermostat_cluster.write_attributes( + {Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Off} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off + assert bosch_thermostat_cluster._attr_cache[BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + + # -- Cool (by-id) + success, fail = await bosch_thermostat_cluster.write_attributes( + {Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Cool} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Cool + assert bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + + # -- operating_mode (by-id) gets ignored when system_mode is written + success, fail = await bosch_thermostat_cluster.write_attributes( + {Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Off, + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id: BoschOperatingMode.Manual} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off + assert bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause + + # -- operating_mode (by-name) gets ignored when system_mode is written + success, fail = await bosch_thermostat_cluster.write_attributes( + {Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Cool, + BoschTrvThermostatCluster.AttributeDefs.operating_mode.name: BoschOperatingMode.Pause} + ) + assert success + assert not fail + assert bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Cool + assert bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual async def test_bosch_room_thermostat_II_230v_write_attributes(zigpy_device_from_v2_quirk): """Test the Room Thermostat II 230v system_mode writes behaving correctly.""" @@ -138,7 +235,7 @@ def mock_write(attributes, manufacturer=None): # -- Off success, fail = await bosch_thermostat_cluster.write_attributes( - {"system_mode": Thermostat.SystemMode.Off} + {Thermostat.AttributeDefs.system_mode.name: Thermostat.SystemMode.Off} ) assert success assert not fail @@ -148,7 +245,7 @@ def mock_write(attributes, manufacturer=None): # -- Heat success, fail = await bosch_thermostat_cluster.write_attributes( - {"system_mode": Thermostat.SystemMode.Heat} + {Thermostat.AttributeDefs.system_mode.name: Thermostat.SystemMode.Heat} ) assert success assert not fail @@ -168,7 +265,7 @@ def mock_write(attributes, manufacturer=None): # -- Off success, fail = await bosch_thermostat_cluster.write_attributes( - {"system_mode": Thermostat.SystemMode.Off} + {Thermostat.AttributeDefs.system_mode.name: Thermostat.SystemMode.Off} ) assert success assert not fail @@ -178,7 +275,7 @@ def mock_write(attributes, manufacturer=None): # -- Cool success, fail = await bosch_thermostat_cluster.write_attributes( - {"system_mode": Thermostat.SystemMode.Cool} + {Thermostat.AttributeDefs.system_mode.name: Thermostat.SystemMode.Cool} ) assert success assert not fail @@ -186,3 +283,62 @@ def mock_write(attributes, manufacturer=None): Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Cool assert bosch_thermostat_cluster._attr_cache[ Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + +async def test_bosch_radiator_thermostat_II_user_interface_write_attributes(zigpy_device_from_v2_quirk): + """Test the Radiator Thermostat II user-interface writes behaving correctly.""" + + device = zigpy_device_from_v2_quirk(manufacturer="BOSCH", model="RBSH-TRV0-ZB-EU") + + bosch_thermostat_ui_cluster = device.endpoints[1].thermostat_ui + + def mock_write(attributes, manufacturer=None): + records = [ + WriteAttributesStatusRecord(foundation.Status.SUCCESS) + for _ in attributes + ] + return [records, []] + + # data is written to trv ui + patch_bosch_trv_ui_write = mock.patch.object( + bosch_thermostat_ui_cluster, + "_write_attributes", + mock.AsyncMock(side_effect=mock_write), + ) + + # check that display_orientation gets converted to supported value type: + with patch_bosch_trv_ui_write: + # - orientation (by-id) normal + success, fail = await bosch_thermostat_ui_cluster.write_attributes( + {BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id: BoschDisplayOrientation.Normal} + ) + assert success + assert not fail + assert bosch_thermostat_ui_cluster._attr_cache[ + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id] == 0 + + # - orientation (by-id) flipped + success, fail = await bosch_thermostat_ui_cluster.write_attributes( + {BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id: BoschDisplayOrientation.Flipped} + ) + assert success + assert not fail + assert bosch_thermostat_ui_cluster._attr_cache[ + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id] == 1 + + # - orientation (by-name) normal + success, fail = await bosch_thermostat_ui_cluster.write_attributes( + {BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.name: BoschDisplayOrientation.Normal} + ) + assert success + assert not fail + assert bosch_thermostat_ui_cluster._attr_cache[ + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id] == 0 + + # - orientation (by-name) flipped + success, fail = await bosch_thermostat_ui_cluster.write_attributes( + {BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.name: BoschDisplayOrientation.Flipped} + ) + assert success + assert not fail + assert bosch_thermostat_ui_cluster._attr_cache[ + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id] == 1 From e9e22d3a7c0516d737b39f9321d141221412bf61 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Wed, 15 May 2024 18:20:22 +0200 Subject: [PATCH 15/40] Read attributes tests. --- tests/test_bosch.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 9b46e75d14..2febc3af42 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -200,6 +200,48 @@ def mock_write(attributes, manufacturer=None): assert bosch_thermostat_cluster._attr_cache[ BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual +async def test_bosch_radiator_thermostat_II_read_attributes(zigpy_device_from_v2_quirk): + """Test the Radiator Thermostat II reads behaving correctly.""" + + device = zigpy_device_from_v2_quirk(manufacturer="BOSCH", model="RBSH-TRV0-ZB-EU") + + bosch_thermostat_cluster = device.endpoints[1].thermostat + + # fake read response for attributes: return BoschOperatingMode.Pause for all attributes + def mock_read(attributes, manufacturer=None): + records = [ + foundation.ReadAttributeRecord( + attr, foundation.Status.SUCCESS, foundation.TypeValue(None, BoschOperatingMode.Pause) + ) + for attr in attributes + ] + return (records,) + + # data is read from trv + patch_bosch_trv_read = mock.patch.object( + bosch_thermostat_cluster, + "_read_attributes", + mock.AsyncMock(side_effect=mock_read), + ) + + # check that system_mode ends-up reading operating_mode: + with patch_bosch_trv_read: + # - system_mode by id + success, fail = await bosch_thermostat_cluster.read_attributes( + [Thermostat.AttributeDefs.system_mode.id] + ) + assert success + assert not fail + assert Thermostat.SystemMode.Off in success.values() + + # - system_mode by name + success, fail = await bosch_thermostat_cluster.read_attributes( + [Thermostat.AttributeDefs.system_mode.name] + ) + assert success + assert not fail + assert Thermostat.SystemMode.Off in success.values() + async def test_bosch_room_thermostat_II_230v_write_attributes(zigpy_device_from_v2_quirk): """Test the Room Thermostat II 230v system_mode writes behaving correctly.""" From 4bb635059f120bf63ac19f8c4e10b108ec78d349 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 16 May 2024 13:40:07 +0200 Subject: [PATCH 16/40] Handle cooling mode when writting only operating_mode. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index feeccd4c09..88959649e6 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -213,6 +213,18 @@ async def write_attributes( new_system_mode_value = OPERATING_MODE_TO_SYSTEM_MODE_MAP[ operating_mode_value ] + + if new_system_mode_value == Thermostat.SystemMode.Heat: + """Heating or cooling? Depends on both operating_mode and ctrl_sequence_of_operation.""" + ctrl_sequence_of_oper_attr = Thermostat.AttributeDefs.ctrl_sequence_of_oper + successful_r, failed_r = await super().read_attributes( + [ctrl_sequence_of_oper_attr.name], True, False, manufacturer + ) + if ctrl_sequence_of_oper_attr.name in successful_r: + ctrl_sequence_of_oper_value = successful_r.pop(ctrl_sequence_of_oper_attr.name) + if ctrl_sequence_of_oper_value == BoschControlSequenceOfOperation.Cooling: + new_system_mode_value = Thermostat.SystemMode.Cool + self._update_attribute(SYSTEM_MODE_ATTR.id, new_system_mode_value) """Write the remaining attributes to thermostat cluster.""" @@ -260,7 +272,6 @@ async def read_attributes( ] """Heating or cooling? Depends on both operating_mode and ctrl_sequence_of_operation.""" - ctrl_sequence_of_oper_value = None if ctrl_sequence_of_oper_attr.name in successful_r: ctrl_sequence_of_oper_value = successful_r.pop(ctrl_sequence_of_oper_attr.name) if ctrl_sequence_of_oper_value == BoschControlSequenceOfOperation.Cooling and system_mode_value == Thermostat.SystemMode.Heat: From 56261937d4ec2ff82806a2f9660ed1a3f8d26b82 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 18 May 2024 23:44:55 +0200 Subject: [PATCH 17/40] Sync ctrl_sequence_of_oper to system_mode for Radiator Thermostat II. --- tests/test_bosch.py | 22 +++++++++++++++++++++- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 2febc3af42..3d5d49eaa6 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -36,8 +36,28 @@ def mock_write(attributes, manufacturer=None): mock.AsyncMock(side_effect=mock_write), ) + # fake read response for attributes: return BoschOperatingMode.Manual for all attributes + def mock_read(attributes, manufacturer=None): + records = [ + foundation.ReadAttributeRecord( + attr, foundation.Status.SUCCESS, foundation.TypeValue(None, BoschOperatingMode.Manual) + ) + for attr in attributes + ] + return (records,) + + # data is read from trv + patch_bosch_trv_read = mock.patch.object( + bosch_thermostat_cluster, + "_read_attributes", + mock.AsyncMock(side_effect=mock_read), + ) + # check that system_mode ends-up writing operating_mode: - with patch_bosch_trv_write: + with ( + patch_bosch_trv_write, + patch_bosch_trv_read + ): # - Heating operation success, fail = await bosch_thermostat_cluster.write_attributes( {"ctrl_sequence_of_oper": ControlSequenceOfOperation.Heating_Only} diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 88959649e6..d149fce1ea 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -226,6 +226,28 @@ async def write_attributes( new_system_mode_value = Thermostat.SystemMode.Cool self._update_attribute(SYSTEM_MODE_ATTR.id, new_system_mode_value) + else: + """Sync system_mode with ctrl_sequence_of_oper.""" + ctrl_sequence_of_oper_attr = Thermostat.AttributeDefs.ctrl_sequence_of_oper + + ctrl_sequence_of_oper_value = None + if ctrl_sequence_of_oper_attr.id in attributes: + ctrl_sequence_of_oper_value = attributes.get(ctrl_sequence_of_oper_attr.id) + elif ctrl_sequence_of_oper_attr.name in attributes: + ctrl_sequence_of_oper_value = attributes.get(ctrl_sequence_of_oper_attr.name) + + if ctrl_sequence_of_oper_value is not None: + successful_r, failed_r = await super().read_attributes( + [operating_mode_attr.name], True, False, manufacturer + ) + if operating_mode_attr.name in successful_r: + operating_mode_attr_value = successful_r.pop(operating_mode_attr.name) + if operating_mode_attr_value == BoschOperatingMode.Manual: + new_system_mode_value = Thermostat.SystemMode.Heat + if ctrl_sequence_of_oper_value == BoschControlSequenceOfOperation.Cooling: + new_system_mode_value = Thermostat.SystemMode.Cool + + self._update_attribute(SYSTEM_MODE_ATTR.id, new_system_mode_value) """Write the remaining attributes to thermostat cluster.""" if remaining_attributes: From c882f9630fa2a219aff3aa50aacb150b1b58e90c Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 19 May 2024 11:45:05 +0200 Subject: [PATCH 18/40] Remove type conversion for attribute ids. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 12 ++++++------ zhaquirks/bosch/rbsh_trv0_zb_eu.py | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 7c623764de..a900316ac2 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -65,26 +65,26 @@ class AttributeDefs(Thermostat.AttributeDefs): """Bosch thermostat manufacturer specific attributes.""" operating_mode = ZCLAttributeDef( - id=t.uint16_t(OPERATING_MODE_ATTR_ID), + id=OPERATING_MODE_ATTR_ID, type=BoschOperatingMode, is_manufacturer_specific=True, ) pi_heating_demand = ZCLAttributeDef( - id=t.uint16_t(VALVE_POSITION_ATTR_ID), + id=VALVE_POSITION_ATTR_ID, # Values range from 0-100 type=t.enum8, is_manufacturer_specific=True, ) window_open = ZCLAttributeDef( - id=t.uint16_t(WINDOW_OPEN_ATTR_ID), + id=WINDOW_OPEN_ATTR_ID, type=State, is_manufacturer_specific=True, ) boost = ZCLAttributeDef( - id=t.uint16_t(BOOST_ATTR_ID), + id=BOOST_ATTR_ID, type=State, is_manufacturer_specific=True, ) @@ -97,14 +97,14 @@ class AttributeDefs(UserInterface.AttributeDefs): """Bosch user interface manufacturer specific attributes.""" display_on_time = ZCLAttributeDef( - id=t.uint16_t(SCREEN_TIMEOUT_ATTR_ID), + id=SCREEN_TIMEOUT_ATTR_ID, # Usable values range from 5-30 type=t.enum8, is_manufacturer_specific=True, ) display_brightness = ZCLAttributeDef( - id=t.uint16_t(SCREEN_BRIGHTNESS_ATTR_ID), + id=SCREEN_BRIGHTNESS_ATTR_ID, # Values range from 0-10 type=t.enum8, is_manufacturer_specific=True, diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index d149fce1ea..82f5f4b107 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -123,32 +123,32 @@ class AttributeDefs(Thermostat.AttributeDefs): """Bosch thermostat manufacturer specific attributes.""" operating_mode = ZCLAttributeDef( - id=t.uint16_t(OPERATING_MODE_ATTR_ID), + id=OPERATING_MODE_ATTR_ID, type=BoschOperatingMode, is_manufacturer_specific=True, ) pi_heating_demand = ZCLAttributeDef( - id=t.uint16_t(VALVE_POSITION_ATTR_ID), + id=VALVE_POSITION_ATTR_ID, # Values range from 0-100 type=t.enum8, is_manufacturer_specific=True, ) window_open = ZCLAttributeDef( - id=t.uint16_t(WINDOW_OPEN_ATTR_ID), + id=WINDOW_OPEN_ATTR_ID, type=State, is_manufacturer_specific=True, ) boost = ZCLAttributeDef( - id=t.uint16_t(BOOST_ATTR_ID), + id=BOOST_ATTR_ID, type=State, is_manufacturer_specific=True, ) remote_temperature = ZCLAttributeDef( - id=t.uint16_t(REMOTE_TEMPERATURE_ATTR_ID), + id=REMOTE_TEMPERATURE_ATTR_ID, type=t.int16s, is_manufacturer_specific=True, ) @@ -321,28 +321,28 @@ class AttributeDefs(UserInterface.AttributeDefs): """Bosch user interface manufacturer specific attributes.""" display_orientation = ZCLAttributeDef( - id=t.uint16_t(SCREEN_ORIENTATION_ATTR_ID), + id=SCREEN_ORIENTATION_ATTR_ID, # To be matched to BoschDisplayOrientation enum. type=t.uint8_t, is_manufacturer_specific=True, ) display_on_time = ZCLAttributeDef( - id=t.uint16_t(SCREEN_TIMEOUT_ATTR_ID), + id=SCREEN_TIMEOUT_ATTR_ID, # Usable values range from 5-30 type=t.enum8, is_manufacturer_specific=True, ) display_brightness = ZCLAttributeDef( - id=t.uint16_t(SCREEN_BRIGHTNESS_ATTR_ID), + id=SCREEN_BRIGHTNESS_ATTR_ID, # Values range from 0-10 type=t.enum8, is_manufacturer_specific=True, ) displayed_temperature = ZCLAttributeDef( - id=t.uint16_t(DISPLAY_MODE_ATTR_ID), + id=DISPLAY_MODE_ATTR_ID, type=BoschDisplayedTemperature, is_manufacturer_specific=True, ) From 16d9083d088b28b638a4f28c9d292a2e84c4142c Mon Sep 17 00:00:00 2001 From: mrrstux Date: Mon, 10 Jun 2024 01:40:15 +0200 Subject: [PATCH 19/40] Make operating-mode read-only. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 5 ++++- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index a900316ac2..eb5ac4ca58 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -3,6 +3,7 @@ from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import add_to_registry_v2 +from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType import zigpy.types as t from zigpy.zcl.clusters.hvac import ( ControlSequenceOfOperation, @@ -115,12 +116,14 @@ class AttributeDefs(UserInterface.AttributeDefs): add_to_registry_v2("Bosch", "RBSH-RTH0-ZB-EU") .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) - # Operating mode: controlled automatically through Thermostat.system_mode (HAVC mode). + # Operating mode - read-only: controlled automatically through Thermostat.system_mode (HAVC mode). .enum( BoschThermostatCluster.AttributeDefs.operating_mode.name, BoschOperatingMode, BoschThermostatCluster.cluster_id, translation_key="operating_mode", + entity_platform=EntityPlatform.SENSOR, + entity_type=EntityType.DIAGNOSTIC, ) # Fast heating/boost. .switch( diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 82f5f4b107..cd41ebed36 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -4,6 +4,7 @@ from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import add_to_registry_v2 +from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass import zigpy.types as t from zigpy.zcl.clusters.hvac import ( @@ -383,12 +384,14 @@ async def write_attributes( add_to_registry_v2("BOSCH", "RBSH-TRV0-ZB-EU") .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) - # Operating mode: controlled automatically through Thermostat.system_mode (HAVC mode). + # Operating mode - read-only: controlled automatically through Thermostat.system_mode (HAVC mode). .enum( BoschThermostatCluster.AttributeDefs.operating_mode.name, BoschOperatingMode, BoschThermostatCluster.cluster_id, translation_key="operating_mode", + entity_platform=EntityPlatform.SENSOR, + entity_type=EntityType.DIAGNOSTIC, ) # Fast heating/boost. .switch( From f08962a28ea3f2a0dafb4d96a680cec1d8167c4e Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 30 Jun 2024 11:50:08 +0200 Subject: [PATCH 20/40] Remove strings from conversion maps. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index cd41ebed36..0be60e27b6 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -92,9 +92,6 @@ class BoschControlSequenceOfOperation(t.enum8): BoschOperatingMode.Schedule: Thermostat.SystemMode.Auto, BoschOperatingMode.Manual: Thermostat.SystemMode.Heat, BoschOperatingMode.Pause: Thermostat.SystemMode.Off, - "BoschOperatingMode.Schedule": Thermostat.SystemMode.Auto, - "BoschOperatingMode.Manual": Thermostat.SystemMode.Heat, - "BoschOperatingMode.Pause": Thermostat.SystemMode.Off, } """HA system mode to Bosch operating mode mapping.""" @@ -103,17 +100,12 @@ class BoschControlSequenceOfOperation(t.enum8): Thermostat.SystemMode.Heat: BoschOperatingMode.Manual, Thermostat.SystemMode.Cool: BoschOperatingMode.Manual, Thermostat.SystemMode.Auto: BoschOperatingMode.Schedule, - "SystemMode.Off": BoschOperatingMode.Pause, - "SystemMode.Heat": BoschOperatingMode.Manual, - "SystemMode.Cool": BoschOperatingMode.Manual, - "SystemMode.Auto": BoschOperatingMode.Schedule, } +"""Bosch display orientation enum to uint8_t mapping.""" DISPLAY_ORIENTATION_ENUM_TO_INT_MAP = { - 0x00: 0x00, - 0x01: 0x01, - "BoschDisplayOrientation.Normal": 0x00, - "BoschDisplayOrientation.Flipped": 0x01, + BoschDisplayOrientation.Normal: 0x00, + BoschDisplayOrientation.Flipped: 0x01, } From 872e810450e87987d84882db4fc6d623fc02c0e7 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 30 Jun 2024 23:02:59 +0200 Subject: [PATCH 21/40] Explicit attribute names - might fix the incorrect entity names. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 6 ++++++ zhaquirks/bosch/rbsh_trv0_zb_eu.py | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index eb5ac4ca58..8751ac2cc2 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -69,6 +69,7 @@ class AttributeDefs(Thermostat.AttributeDefs): id=OPERATING_MODE_ATTR_ID, type=BoschOperatingMode, is_manufacturer_specific=True, + name="operating_mode", ) pi_heating_demand = ZCLAttributeDef( @@ -76,18 +77,21 @@ class AttributeDefs(Thermostat.AttributeDefs): # Values range from 0-100 type=t.enum8, is_manufacturer_specific=True, + name="pi_heating_demand", ) window_open = ZCLAttributeDef( id=WINDOW_OPEN_ATTR_ID, type=State, is_manufacturer_specific=True, + name="window_open", ) boost = ZCLAttributeDef( id=BOOST_ATTR_ID, type=State, is_manufacturer_specific=True, + name="boost", ) @@ -102,6 +106,7 @@ class AttributeDefs(UserInterface.AttributeDefs): # Usable values range from 5-30 type=t.enum8, is_manufacturer_specific=True, + name="display_on_time", ) display_brightness = ZCLAttributeDef( @@ -109,6 +114,7 @@ class AttributeDefs(UserInterface.AttributeDefs): # Values range from 0-10 type=t.enum8, is_manufacturer_specific=True, + name="display_brightness", ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 0be60e27b6..485f0de4e1 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -119,6 +119,7 @@ class AttributeDefs(Thermostat.AttributeDefs): id=OPERATING_MODE_ATTR_ID, type=BoschOperatingMode, is_manufacturer_specific=True, + name="operating_mode", ) pi_heating_demand = ZCLAttributeDef( @@ -126,24 +127,28 @@ class AttributeDefs(Thermostat.AttributeDefs): # Values range from 0-100 type=t.enum8, is_manufacturer_specific=True, + name="pi_heating_demand", ) window_open = ZCLAttributeDef( id=WINDOW_OPEN_ATTR_ID, type=State, is_manufacturer_specific=True, + name="window_open", ) boost = ZCLAttributeDef( id=BOOST_ATTR_ID, type=State, is_manufacturer_specific=True, + name="boost", ) remote_temperature = ZCLAttributeDef( id=REMOTE_TEMPERATURE_ATTR_ID, type=t.int16s, is_manufacturer_specific=True, + name="remote_temperature", ) async def write_attributes( @@ -318,6 +323,7 @@ class AttributeDefs(UserInterface.AttributeDefs): # To be matched to BoschDisplayOrientation enum. type=t.uint8_t, is_manufacturer_specific=True, + name="display_orientation", ) display_on_time = ZCLAttributeDef( @@ -325,6 +331,7 @@ class AttributeDefs(UserInterface.AttributeDefs): # Usable values range from 5-30 type=t.enum8, is_manufacturer_specific=True, + name="display_on_time", ) display_brightness = ZCLAttributeDef( @@ -332,12 +339,14 @@ class AttributeDefs(UserInterface.AttributeDefs): # Values range from 0-10 type=t.enum8, is_manufacturer_specific=True, + name="display_brightness", ) displayed_temperature = ZCLAttributeDef( id=DISPLAY_MODE_ATTR_ID, type=BoschDisplayedTemperature, is_manufacturer_specific=True, + name="displayed_temperature", ) async def write_attributes( From 60f487463d0f9bc0438f3e25cbd62b7371100c6c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 19:24:07 +0000 Subject: [PATCH 22/40] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_bosch.py | 451 +++++++++++++++++++++-------- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 2 +- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 55 +++- 3 files changed, 381 insertions(+), 127 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 3d5d49eaa6..58c0202b31 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -1,4 +1,5 @@ """Tests the Bosch thermostats quirk.""" + from unittest import mock from zigpy.zcl import foundation @@ -15,7 +16,10 @@ zhaquirks.setup() -async def test_bosch_radiator_thermostat_II_write_attributes(zigpy_device_from_v2_quirk): + +async def test_bosch_radiator_thermostat_II_write_attributes( + zigpy_device_from_v2_quirk, +): """Test the Radiator Thermostat II writes behaving correctly.""" device = zigpy_device_from_v2_quirk(manufacturer="BOSCH", model="RBSH-TRV0-ZB-EU") @@ -24,8 +28,7 @@ async def test_bosch_radiator_thermostat_II_write_attributes(zigpy_device_from_v def mock_write(attributes, manufacturer=None): records = [ - WriteAttributesStatusRecord(foundation.Status.SUCCESS) - for _ in attributes + WriteAttributesStatusRecord(foundation.Status.SUCCESS) for _ in attributes ] return [records, []] @@ -40,7 +43,9 @@ def mock_write(attributes, manufacturer=None): def mock_read(attributes, manufacturer=None): records = [ foundation.ReadAttributeRecord( - attr, foundation.Status.SUCCESS, foundation.TypeValue(None, BoschOperatingMode.Manual) + attr, + foundation.Status.SUCCESS, + foundation.TypeValue(None, BoschOperatingMode.Manual), ) for attr in attributes ] @@ -54,18 +59,19 @@ def mock_read(attributes, manufacturer=None): ) # check that system_mode ends-up writing operating_mode: - with ( - patch_bosch_trv_write, - patch_bosch_trv_read - ): + with patch_bosch_trv_write, patch_bosch_trv_read: # - Heating operation success, fail = await bosch_thermostat_cluster.write_attributes( {"ctrl_sequence_of_oper": ControlSequenceOfOperation.Heating_Only} ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Heating_Only + ) # -- Off (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( @@ -73,10 +79,24 @@ def mock_read(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off - assert bosch_thermostat_cluster._attr_cache[BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Off + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Pause + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Heating_Only + ) # -- Heat (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( @@ -84,12 +104,24 @@ def mock_read(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Heat - assert bosch_thermostat_cluster._attr_cache[ - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Heat + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Manual + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Heating_Only + ) # -- Off (by-id) success, fail = await bosch_thermostat_cluster.write_attributes( @@ -97,12 +129,24 @@ def mock_read(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off - assert bosch_thermostat_cluster._attr_cache[ - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Off + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Pause + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Heating_Only + ) # -- Heat (by-id) success, fail = await bosch_thermostat_cluster.write_attributes( @@ -110,34 +154,66 @@ def mock_read(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Heat - assert bosch_thermostat_cluster._attr_cache[ - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Heat + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Manual + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Heating_Only + ) # -- operating_mode (by-id) changes system_mode success, fail = await bosch_thermostat_cluster.write_attributes( - {BoschTrvThermostatCluster.AttributeDefs.operating_mode.id: BoschOperatingMode.Pause} + { + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id: BoschOperatingMode.Pause + } ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off - assert bosch_thermostat_cluster._attr_cache[ - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Off + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Pause + ) # -- operating_mode (by-name) changes system_mode success, fail = await bosch_thermostat_cluster.write_attributes( - {BoschTrvThermostatCluster.AttributeDefs.operating_mode.name: BoschOperatingMode.Manual} + { + BoschTrvThermostatCluster.AttributeDefs.operating_mode.name: BoschOperatingMode.Manual + } ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Heat - assert bosch_thermostat_cluster._attr_cache[ - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Heat + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Manual + ) # - Cooling operation success, fail = await bosch_thermostat_cluster.write_attributes( @@ -145,8 +221,12 @@ def mock_read(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Cooling_Only + ) # -- Off (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( @@ -154,10 +234,24 @@ def mock_read(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off - assert bosch_thermostat_cluster._attr_cache[BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Off + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Pause + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Cooling_Only + ) # -- Cool (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( @@ -165,12 +259,24 @@ def mock_read(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Cool - assert bosch_thermostat_cluster._attr_cache[ - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Cool + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Manual + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Cooling_Only + ) # -- Off (by-id) success, fail = await bosch_thermostat_cluster.write_attributes( @@ -178,10 +284,24 @@ def mock_read(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off - assert bosch_thermostat_cluster._attr_cache[BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Off + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Pause + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Cooling_Only + ) # -- Cool (by-id) success, fail = await bosch_thermostat_cluster.write_attributes( @@ -189,36 +309,69 @@ def mock_read(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Cool - assert bosch_thermostat_cluster._attr_cache[ - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Cool + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Manual + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Cooling_Only + ) # -- operating_mode (by-id) gets ignored when system_mode is written success, fail = await bosch_thermostat_cluster.write_attributes( - {Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Off, - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id: BoschOperatingMode.Manual} + { + Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Off, + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id: BoschOperatingMode.Manual, + } ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off - assert bosch_thermostat_cluster._attr_cache[ - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Pause + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Off + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Pause + ) # -- operating_mode (by-name) gets ignored when system_mode is written success, fail = await bosch_thermostat_cluster.write_attributes( - {Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Cool, - BoschTrvThermostatCluster.AttributeDefs.operating_mode.name: BoschOperatingMode.Pause} + { + Thermostat.AttributeDefs.system_mode.id: Thermostat.SystemMode.Cool, + BoschTrvThermostatCluster.AttributeDefs.operating_mode.name: BoschOperatingMode.Pause, + } ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Cool - assert bosch_thermostat_cluster._attr_cache[ - BoschTrvThermostatCluster.AttributeDefs.operating_mode.id] == BoschOperatingMode.Manual + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Cool + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + ] + == BoschOperatingMode.Manual + ) + async def test_bosch_radiator_thermostat_II_read_attributes(zigpy_device_from_v2_quirk): """Test the Radiator Thermostat II reads behaving correctly.""" @@ -231,7 +384,9 @@ async def test_bosch_radiator_thermostat_II_read_attributes(zigpy_device_from_v2 def mock_read(attributes, manufacturer=None): records = [ foundation.ReadAttributeRecord( - attr, foundation.Status.SUCCESS, foundation.TypeValue(None, BoschOperatingMode.Pause) + attr, + foundation.Status.SUCCESS, + foundation.TypeValue(None, BoschOperatingMode.Pause), ) for attr in attributes ] @@ -262,7 +417,10 @@ def mock_read(attributes, manufacturer=None): assert not fail assert Thermostat.SystemMode.Off in success.values() -async def test_bosch_room_thermostat_II_230v_write_attributes(zigpy_device_from_v2_quirk): + +async def test_bosch_room_thermostat_II_230v_write_attributes( + zigpy_device_from_v2_quirk, +): """Test the Room Thermostat II 230v system_mode writes behaving correctly.""" device = zigpy_device_from_v2_quirk(manufacturer="Bosch", model="RBSH-RTH0-ZB-EU") @@ -271,8 +429,7 @@ async def test_bosch_room_thermostat_II_230v_write_attributes(zigpy_device_from_ def mock_write(attributes, manufacturer=None): records = [ - WriteAttributesStatusRecord(foundation.Status.SUCCESS) - for _ in attributes + WriteAttributesStatusRecord(foundation.Status.SUCCESS) for _ in attributes ] return [records, []] @@ -292,8 +449,12 @@ def mock_write(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Heating_Only + ) # -- Off success, fail = await bosch_thermostat_cluster.write_attributes( @@ -301,9 +462,18 @@ def mock_write(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Off + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Heating_Only + ) # -- Heat success, fail = await bosch_thermostat_cluster.write_attributes( @@ -311,10 +481,18 @@ def mock_write(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Heat - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Heating_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Heat + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Heating_Only + ) # - Cooling operation success, fail = await bosch_thermostat_cluster.write_attributes( @@ -322,8 +500,12 @@ def mock_write(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Cooling_Only + ) # -- Off success, fail = await bosch_thermostat_cluster.write_attributes( @@ -331,9 +513,18 @@ def mock_write(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Off - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Off + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Cooling_Only + ) # -- Cool success, fail = await bosch_thermostat_cluster.write_attributes( @@ -341,12 +532,23 @@ def mock_write(attributes, manufacturer=None): ) assert success assert not fail - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.system_mode.id] == Thermostat.SystemMode.Cool - assert bosch_thermostat_cluster._attr_cache[ - Thermostat.AttributeDefs.ctrl_sequence_of_oper.id] == ControlSequenceOfOperation.Cooling_Only + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Cool + ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id + ] + == ControlSequenceOfOperation.Cooling_Only + ) + -async def test_bosch_radiator_thermostat_II_user_interface_write_attributes(zigpy_device_from_v2_quirk): +async def test_bosch_radiator_thermostat_II_user_interface_write_attributes( + zigpy_device_from_v2_quirk, +): """Test the Radiator Thermostat II user-interface writes behaving correctly.""" device = zigpy_device_from_v2_quirk(manufacturer="BOSCH", model="RBSH-TRV0-ZB-EU") @@ -355,8 +557,7 @@ async def test_bosch_radiator_thermostat_II_user_interface_write_attributes(zigp def mock_write(attributes, manufacturer=None): records = [ - WriteAttributesStatusRecord(foundation.Status.SUCCESS) - for _ in attributes + WriteAttributesStatusRecord(foundation.Status.SUCCESS) for _ in attributes ] return [records, []] @@ -371,36 +572,60 @@ def mock_write(attributes, manufacturer=None): with patch_bosch_trv_ui_write: # - orientation (by-id) normal success, fail = await bosch_thermostat_ui_cluster.write_attributes( - {BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id: BoschDisplayOrientation.Normal} + { + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id: BoschDisplayOrientation.Normal + } ) assert success assert not fail - assert bosch_thermostat_ui_cluster._attr_cache[ - BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id] == 0 + assert ( + bosch_thermostat_ui_cluster._attr_cache[ + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id + ] + == 0 + ) # - orientation (by-id) flipped success, fail = await bosch_thermostat_ui_cluster.write_attributes( - {BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id: BoschDisplayOrientation.Flipped} + { + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id: BoschDisplayOrientation.Flipped + } ) assert success assert not fail - assert bosch_thermostat_ui_cluster._attr_cache[ - BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id] == 1 + assert ( + bosch_thermostat_ui_cluster._attr_cache[ + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id + ] + == 1 + ) # - orientation (by-name) normal success, fail = await bosch_thermostat_ui_cluster.write_attributes( - {BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.name: BoschDisplayOrientation.Normal} + { + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.name: BoschDisplayOrientation.Normal + } ) assert success assert not fail - assert bosch_thermostat_ui_cluster._attr_cache[ - BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id] == 0 + assert ( + bosch_thermostat_ui_cluster._attr_cache[ + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id + ] + == 0 + ) # - orientation (by-name) flipped success, fail = await bosch_thermostat_ui_cluster.write_attributes( - {BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.name: BoschDisplayOrientation.Flipped} + { + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.name: BoschDisplayOrientation.Flipped + } ) assert success assert not fail - assert bosch_thermostat_ui_cluster._attr_cache[ - BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id] == 1 + assert ( + bosch_thermostat_ui_cluster._attr_cache[ + BoschTrvUserInterfaceCluster.AttributeDefs.display_orientation.id + ] + == 1 + ) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 8751ac2cc2..31b1cce914 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -1,6 +1,5 @@ """Device handler for Bosch RBSH-RTH0-ZB-EU thermostat.""" - from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import add_to_registry_v2 from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType @@ -37,6 +36,7 @@ # Control sequence of operation (heating/cooling) CTRL_SEQUENCE_OF_OPERATION_ID = 0x001B + class BoschOperatingMode(t.enum8): """Bosh operating mode attribute values.""" diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 485f0de4e1..fefe9914ed 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -48,6 +48,7 @@ # Control sequence of operation (heating/cooling) CTRL_SEQUENCE_OF_OPERATION_ID = 0x001B + class BoschOperatingMode(t.enum8): """Bosh operating mode attribute values.""" @@ -214,13 +215,20 @@ async def write_attributes( if new_system_mode_value == Thermostat.SystemMode.Heat: """Heating or cooling? Depends on both operating_mode and ctrl_sequence_of_operation.""" - ctrl_sequence_of_oper_attr = Thermostat.AttributeDefs.ctrl_sequence_of_oper + ctrl_sequence_of_oper_attr = ( + Thermostat.AttributeDefs.ctrl_sequence_of_oper + ) successful_r, failed_r = await super().read_attributes( [ctrl_sequence_of_oper_attr.name], True, False, manufacturer ) if ctrl_sequence_of_oper_attr.name in successful_r: - ctrl_sequence_of_oper_value = successful_r.pop(ctrl_sequence_of_oper_attr.name) - if ctrl_sequence_of_oper_value == BoschControlSequenceOfOperation.Cooling: + ctrl_sequence_of_oper_value = successful_r.pop( + ctrl_sequence_of_oper_attr.name + ) + if ( + ctrl_sequence_of_oper_value + == BoschControlSequenceOfOperation.Cooling + ): new_system_mode_value = Thermostat.SystemMode.Cool self._update_attribute(SYSTEM_MODE_ATTR.id, new_system_mode_value) @@ -230,22 +238,33 @@ async def write_attributes( ctrl_sequence_of_oper_value = None if ctrl_sequence_of_oper_attr.id in attributes: - ctrl_sequence_of_oper_value = attributes.get(ctrl_sequence_of_oper_attr.id) + ctrl_sequence_of_oper_value = attributes.get( + ctrl_sequence_of_oper_attr.id + ) elif ctrl_sequence_of_oper_attr.name in attributes: - ctrl_sequence_of_oper_value = attributes.get(ctrl_sequence_of_oper_attr.name) + ctrl_sequence_of_oper_value = attributes.get( + ctrl_sequence_of_oper_attr.name + ) if ctrl_sequence_of_oper_value is not None: successful_r, failed_r = await super().read_attributes( [operating_mode_attr.name], True, False, manufacturer ) if operating_mode_attr.name in successful_r: - operating_mode_attr_value = successful_r.pop(operating_mode_attr.name) + operating_mode_attr_value = successful_r.pop( + operating_mode_attr.name + ) if operating_mode_attr_value == BoschOperatingMode.Manual: new_system_mode_value = Thermostat.SystemMode.Heat - if ctrl_sequence_of_oper_value == BoschControlSequenceOfOperation.Cooling: + if ( + ctrl_sequence_of_oper_value + == BoschControlSequenceOfOperation.Cooling + ): new_system_mode_value = Thermostat.SystemMode.Cool - self._update_attribute(SYSTEM_MODE_ATTR.id, new_system_mode_value) + self._update_attribute( + SYSTEM_MODE_ATTR.id, new_system_mode_value + ) """Write the remaining attributes to thermostat cluster.""" if remaining_attributes: @@ -283,7 +302,10 @@ async def read_attributes( ctrl_sequence_of_oper_attr = Thermostat.AttributeDefs.ctrl_sequence_of_oper successful_r, failed_r = await super().read_attributes( - [operating_mode_attr.name, ctrl_sequence_of_oper_attr.name], allow_cache, only_cache, manufacturer + [operating_mode_attr.name, ctrl_sequence_of_oper_attr.name], + allow_cache, + only_cache, + manufacturer, ) if operating_mode_attr.name in successful_r: operating_mode_value = successful_r.pop(operating_mode_attr.name) @@ -293,8 +315,14 @@ async def read_attributes( """Heating or cooling? Depends on both operating_mode and ctrl_sequence_of_operation.""" if ctrl_sequence_of_oper_attr.name in successful_r: - ctrl_sequence_of_oper_value = successful_r.pop(ctrl_sequence_of_oper_attr.name) - if ctrl_sequence_of_oper_value == BoschControlSequenceOfOperation.Cooling and system_mode_value == Thermostat.SystemMode.Heat: + ctrl_sequence_of_oper_value = successful_r.pop( + ctrl_sequence_of_oper_attr.name + ) + if ( + ctrl_sequence_of_oper_value + == BoschControlSequenceOfOperation.Cooling + and system_mode_value == Thermostat.SystemMode.Heat + ): system_mode_value = Thermostat.SystemMode.Cool successful_r[system_mode_attribute_id] = system_mode_value @@ -448,8 +476,9 @@ async def write_attributes( max_value=10, step=1, translation_key="display_brightness", - # Heating vs Cooling. - ).enum( + # Heating vs Cooling. + ) + .enum( Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, BoschControlSequenceOfOperation, BoschThermostatCluster.cluster_id, From 238b8e69ea6c034bee2a645901e2cf8379b9dfed Mon Sep 17 00:00:00 2001 From: mrrstux Date: Fri, 30 Aug 2024 21:55:34 +0200 Subject: [PATCH 23/40] Adapt to new zigpy quirks v2 API. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 5 +++-- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 31b1cce914..8002b4bc03 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -1,7 +1,7 @@ """Device handler for Bosch RBSH-RTH0-ZB-EU thermostat.""" from zigpy.quirks import CustomCluster -from zigpy.quirks.v2 import add_to_registry_v2 +from zigpy.quirks.v2 import QuirkBuilder from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType import zigpy.types as t from zigpy.zcl.clusters.hvac import ( @@ -119,7 +119,7 @@ class AttributeDefs(UserInterface.AttributeDefs): ( - add_to_registry_v2("Bosch", "RBSH-RTH0-ZB-EU") + QuirkBuilder("Bosch", "RBSH-RTH0-ZB-EU") .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) # Operating mode - read-only: controlled automatically through Thermostat.system_mode (HAVC mode). @@ -168,4 +168,5 @@ class AttributeDefs(UserInterface.AttributeDefs): BoschThermostatCluster.cluster_id, translation_key="ctrl_sequence_of_oper", ) + .add_to_registry() ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index fefe9914ed..ae3958d4a4 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -3,7 +3,7 @@ from typing import Any from zigpy.quirks import CustomCluster -from zigpy.quirks.v2 import add_to_registry_v2 +from zigpy.quirks.v2 import QuirkBuilder from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass import zigpy.types as t @@ -410,7 +410,7 @@ async def write_attributes( ( - add_to_registry_v2("BOSCH", "RBSH-TRV0-ZB-EU") + QuirkBuilder("BOSCH", "RBSH-TRV0-ZB-EU") .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) # Operating mode - read-only: controlled automatically through Thermostat.system_mode (HAVC mode). @@ -484,4 +484,5 @@ async def write_attributes( BoschThermostatCluster.cluster_id, translation_key="ctrl_sequence_of_oper", ) + .add_to_registry() ) From 28de05fc82996b9ebbf3e1b83f194b2a6f2ac415 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Fri, 30 Aug 2024 22:23:36 +0200 Subject: [PATCH 24/40] Drop translation key and attribute name since its inferred from attribute definition. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 18 +++----------- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 38 ++++++------------------------ 2 files changed, 10 insertions(+), 46 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 8002b4bc03..23588e8275 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -69,29 +69,25 @@ class AttributeDefs(Thermostat.AttributeDefs): id=OPERATING_MODE_ATTR_ID, type=BoschOperatingMode, is_manufacturer_specific=True, - name="operating_mode", ) pi_heating_demand = ZCLAttributeDef( id=VALVE_POSITION_ATTR_ID, # Values range from 0-100 - type=t.enum8, + type=t.uint8_t, is_manufacturer_specific=True, - name="pi_heating_demand", ) window_open = ZCLAttributeDef( id=WINDOW_OPEN_ATTR_ID, type=State, is_manufacturer_specific=True, - name="window_open", ) boost = ZCLAttributeDef( id=BOOST_ATTR_ID, type=State, is_manufacturer_specific=True, - name="boost", ) @@ -104,17 +100,15 @@ class AttributeDefs(UserInterface.AttributeDefs): display_on_time = ZCLAttributeDef( id=SCREEN_TIMEOUT_ATTR_ID, # Usable values range from 5-30 - type=t.enum8, + type=t.uint8_t, is_manufacturer_specific=True, - name="display_on_time", ) display_brightness = ZCLAttributeDef( id=SCREEN_BRIGHTNESS_ATTR_ID, # Values range from 0-10 - type=t.enum8, + type=t.uint8_t, is_manufacturer_specific=True, - name="display_brightness", ) @@ -127,7 +121,6 @@ class AttributeDefs(UserInterface.AttributeDefs): BoschThermostatCluster.AttributeDefs.operating_mode.name, BoschOperatingMode, BoschThermostatCluster.cluster_id, - translation_key="operating_mode", entity_platform=EntityPlatform.SENSOR, entity_type=EntityType.DIAGNOSTIC, ) @@ -135,13 +128,11 @@ class AttributeDefs(UserInterface.AttributeDefs): .switch( BoschThermostatCluster.AttributeDefs.boost.name, BoschThermostatCluster.cluster_id, - translation_key="boost", ) # Window open switch: manually set or through an automation. .switch( BoschThermostatCluster.AttributeDefs.window_open.name, BoschThermostatCluster.cluster_id, - translation_key="window_open", ) # Display time-out. .number( @@ -150,7 +141,6 @@ class AttributeDefs(UserInterface.AttributeDefs): min_value=5, max_value=30, step=1, - translation_key="display_on_time", ) # Display brightness. .number( @@ -159,14 +149,12 @@ class AttributeDefs(UserInterface.AttributeDefs): min_value=0, max_value=10, step=1, - translation_key="display_brightness", ) # Heating vs Cooling. .enum( Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, BoschControlSequenceOfOperation, BoschThermostatCluster.cluster_id, - translation_key="ctrl_sequence_of_oper", ) .add_to_registry() ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index ae3958d4a4..04708d4453 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -120,36 +120,25 @@ class AttributeDefs(Thermostat.AttributeDefs): id=OPERATING_MODE_ATTR_ID, type=BoschOperatingMode, is_manufacturer_specific=True, - name="operating_mode", ) pi_heating_demand = ZCLAttributeDef( id=VALVE_POSITION_ATTR_ID, # Values range from 0-100 - type=t.enum8, + type=t.uint8_t, is_manufacturer_specific=True, - name="pi_heating_demand", ) window_open = ZCLAttributeDef( - id=WINDOW_OPEN_ATTR_ID, - type=State, - is_manufacturer_specific=True, - name="window_open", + id=WINDOW_OPEN_ATTR_ID, type=State, is_manufacturer_specific=True ) boost = ZCLAttributeDef( - id=BOOST_ATTR_ID, - type=State, - is_manufacturer_specific=True, - name="boost", + id=BOOST_ATTR_ID, type=State, is_manufacturer_specific=True ) remote_temperature = ZCLAttributeDef( - id=REMOTE_TEMPERATURE_ATTR_ID, - type=t.int16s, - is_manufacturer_specific=True, - name="remote_temperature", + id=REMOTE_TEMPERATURE_ATTR_ID, type=t.int16s, is_manufacturer_specific=True ) async def write_attributes( @@ -351,30 +340,26 @@ class AttributeDefs(UserInterface.AttributeDefs): # To be matched to BoschDisplayOrientation enum. type=t.uint8_t, is_manufacturer_specific=True, - name="display_orientation", ) display_on_time = ZCLAttributeDef( id=SCREEN_TIMEOUT_ATTR_ID, # Usable values range from 5-30 - type=t.enum8, + type=t.uint8_t, is_manufacturer_specific=True, - name="display_on_time", ) display_brightness = ZCLAttributeDef( id=SCREEN_BRIGHTNESS_ATTR_ID, # Values range from 0-10 - type=t.enum8, + type=t.uint8_t, is_manufacturer_specific=True, - name="display_brightness", ) displayed_temperature = ZCLAttributeDef( id=DISPLAY_MODE_ATTR_ID, type=BoschDisplayedTemperature, is_manufacturer_specific=True, - name="displayed_temperature", ) async def write_attributes( @@ -418,7 +403,6 @@ async def write_attributes( BoschThermostatCluster.AttributeDefs.operating_mode.name, BoschOperatingMode, BoschThermostatCluster.cluster_id, - translation_key="operating_mode", entity_platform=EntityPlatform.SENSOR, entity_type=EntityType.DIAGNOSTIC, ) @@ -426,13 +410,11 @@ async def write_attributes( .switch( BoschThermostatCluster.AttributeDefs.boost.name, BoschThermostatCluster.cluster_id, - translation_key="boost", ) # Window open switch: manually set or through an automation. .switch( BoschThermostatCluster.AttributeDefs.window_open.name, BoschThermostatCluster.cluster_id, - translation_key="window_open", ) # Remote temperature. .number( @@ -443,21 +425,18 @@ async def write_attributes( step=0.1, multiplier=100, device_class=NumberDeviceClass.TEMPERATURE, - # translation_key="remote_temperature" ) # Display temperature. .enum( BoschUserInterfaceCluster.AttributeDefs.displayed_temperature.name, BoschDisplayedTemperature, BoschUserInterfaceCluster.cluster_id, - translation_key="displayed_temperature", ) # Display orientation. .enum( BoschUserInterfaceCluster.AttributeDefs.display_orientation.name, BoschDisplayOrientation, BoschUserInterfaceCluster.cluster_id, - translation_key="display_orientation", ) # Display time-out. .number( @@ -466,7 +445,6 @@ async def write_attributes( min_value=5, max_value=30, step=1, - translation_key="display_on_time", ) # Display brightness. .number( @@ -475,14 +453,12 @@ async def write_attributes( min_value=0, max_value=10, step=1, - translation_key="display_brightness", - # Heating vs Cooling. ) + # Heating vs Cooling. .enum( Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, BoschControlSequenceOfOperation, BoschThermostatCluster.cluster_id, - translation_key="ctrl_sequence_of_oper", ) .add_to_registry() ) From 56a65529621c2de09b460e16c746ed46c8d966a2 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 1 Oct 2024 21:29:45 +0200 Subject: [PATCH 25/40] Rename "boost" attribute to "boost_heating". --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 10 +++++----- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 23588e8275..9b6af9367a 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -22,8 +22,8 @@ # Window open switch (changes to a lower target temperature when on). WINDOW_OPEN_ATTR_ID = 0x4042 -# Boost preset mode. -BOOST_ATTR_ID = 0x4043 +# Boost heating preset mode. +BOOST_HEATING_ATTR_ID = 0x4043 """Bosch specific user interface attribute ids.""" @@ -84,8 +84,8 @@ class AttributeDefs(Thermostat.AttributeDefs): is_manufacturer_specific=True, ) - boost = ZCLAttributeDef( - id=BOOST_ATTR_ID, + boost_heating = ZCLAttributeDef( + id=BOOST_HEATING_ATTR_ID, type=State, is_manufacturer_specific=True, ) @@ -126,7 +126,7 @@ class AttributeDefs(UserInterface.AttributeDefs): ) # Fast heating/boost. .switch( - BoschThermostatCluster.AttributeDefs.boost.name, + BoschThermostatCluster.AttributeDefs.boost_heating.name, BoschThermostatCluster.cluster_id, ) # Window open switch: manually set or through an automation. diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 04708d4453..2eb906e0da 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -28,8 +28,8 @@ # Window open switch (changes to a lower target temperature when on). WINDOW_OPEN_ATTR_ID = 0x4042 -# Boost preset mode. -BOOST_ATTR_ID = 0x4043 +# Boost heating preset mode. +BOOST_HEATING_ATTR_ID = 0x4043 """Bosch specific user interface attribute ids.""" @@ -133,8 +133,8 @@ class AttributeDefs(Thermostat.AttributeDefs): id=WINDOW_OPEN_ATTR_ID, type=State, is_manufacturer_specific=True ) - boost = ZCLAttributeDef( - id=BOOST_ATTR_ID, type=State, is_manufacturer_specific=True + boost_heating = ZCLAttributeDef( + id=BOOST_HEATING_ATTR_ID, type=State, is_manufacturer_specific=True ) remote_temperature = ZCLAttributeDef( @@ -408,7 +408,7 @@ async def write_attributes( ) # Fast heating/boost. .switch( - BoschThermostatCluster.AttributeDefs.boost.name, + BoschThermostatCluster.AttributeDefs.boost_heating.name, BoschThermostatCluster.cluster_id, ) # Window open switch: manually set or through an automation. From 5646d91f96ec4f31ecbf996e4cee9e8dd29e35d8 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 01:14:11 +0200 Subject: [PATCH 26/40] Revert display ontime&brightness types to enum8. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 4 ++-- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 9b6af9367a..4f3e0bb0b2 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -100,14 +100,14 @@ class AttributeDefs(UserInterface.AttributeDefs): display_on_time = ZCLAttributeDef( id=SCREEN_TIMEOUT_ATTR_ID, # Usable values range from 5-30 - type=t.uint8_t, + type=t.enum8, is_manufacturer_specific=True, ) display_brightness = ZCLAttributeDef( id=SCREEN_BRIGHTNESS_ATTR_ID, # Values range from 0-10 - type=t.uint8_t, + type=t.enum8, is_manufacturer_specific=True, ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 2eb906e0da..532e7ce0d9 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -345,14 +345,14 @@ class AttributeDefs(UserInterface.AttributeDefs): display_on_time = ZCLAttributeDef( id=SCREEN_TIMEOUT_ATTR_ID, # Usable values range from 5-30 - type=t.uint8_t, + type=t.enum8, is_manufacturer_specific=True, ) display_brightness = ZCLAttributeDef( id=SCREEN_BRIGHTNESS_ATTR_ID, # Values range from 0-10 - type=t.uint8_t, + type=t.enum8, is_manufacturer_specific=True, ) From 087ccfbe1634883270d4c3ed220a5f1d445dd923 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 19:24:28 +0200 Subject: [PATCH 27/40] Fix pi_heating_demand reporting by changing attribute type to enum8. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 532e7ce0d9..f1e79a9a6e 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -125,7 +125,7 @@ class AttributeDefs(Thermostat.AttributeDefs): pi_heating_demand = ZCLAttributeDef( id=VALVE_POSITION_ATTR_ID, # Values range from 0-100 - type=t.uint8_t, + type=t.enum8, is_manufacturer_specific=True, ) From 33255e95288f184e1b9cb3bf4d7dcef11d2c1400 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 21:37:07 +0200 Subject: [PATCH 28/40] Prevent system_mode reporting on TRV (reports always SyetemMode.Heat). --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index f1e79a9a6e..fac7e6224f 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -7,6 +7,7 @@ from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass import zigpy.types as t +from zigpy.zcl import foundation from zigpy.zcl.clusters.hvac import ( ControlSequenceOfOperation, Thermostat, @@ -328,6 +329,32 @@ async def read_attributes( return successful_r, failed_r + async def _configure_reporting( + self, + config_records: list[foundation.AttributeReportingConfig], + *args, + manufacturer: int | t.uint16_t | None = None, + **kwargs, + ): + """system_mode special handling. + + - prevent system_mode reporting (TRV reports always SystemMode.Heat). + """ + + new_config_records = config_records.copy() + [ + new_config_records.pop(record) + for record in new_config_records + if record.attrid == SYSTEM_MODE_ATTR.id + ] + + return await super()._configure_reporting( + new_config_records, + *args, + manufacturer, + **kwargs, + ) + class BoschUserInterfaceCluster(CustomCluster, UserInterface): """Bosch UserInterface cluster.""" From 4d290d47334906f0f0157ffa6639198d58e93683 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 21:37:07 +0200 Subject: [PATCH 29/40] Prevent system_mode reporting on TRV (reports always SyetemMode.Heat). --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index fac7e6224f..1a30f00520 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -343,7 +343,7 @@ async def _configure_reporting( new_config_records = config_records.copy() [ - new_config_records.pop(record) + new_config_records.remove(record) # type: ignore for record in new_config_records if record.attrid == SYSTEM_MODE_ATTR.id ] @@ -351,7 +351,7 @@ async def _configure_reporting( return await super()._configure_reporting( new_config_records, *args, - manufacturer, + manufacturer=manufacturer, **kwargs, ) From cdb455d0f541de9f25d7ebc515261c7dce580b43 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 21:37:07 +0200 Subject: [PATCH 30/40] Prevent system_mode reporting on TRV (reports always SystemMode.Heat). --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 43 ++++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 1a30f00520..93d3739878 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -1,6 +1,6 @@ """Device handler for Bosch RBSH-TRV0-ZB-EU thermostat.""" -from typing import Any +from typing import Any, Optional, Union from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import QuirkBuilder @@ -329,31 +329,34 @@ async def read_attributes( return successful_r, failed_r - async def _configure_reporting( + def handle_cluster_general_request( self, - config_records: list[foundation.AttributeReportingConfig], - *args, - manufacturer: int | t.uint16_t | None = None, - **kwargs, + hdr: foundation.ZCLHeader, + args: list[Any], + *, + dst_addressing: Optional[ + Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK] + ] = None, ): """system_mode special handling. - - prevent system_mode reporting (TRV reports always SystemMode.Heat). + - ignore updates of system_mode coming from device (TRV incorrectly + reports being in Heat mode, even when turned off). """ - new_config_records = config_records.copy() - [ - new_config_records.remove(record) # type: ignore - for record in new_config_records - if record.attrid == SYSTEM_MODE_ATTR.id - ] - - return await super()._configure_reporting( - new_config_records, - *args, - manufacturer=manufacturer, - **kwargs, - ) + """Pass-through anything that is not related to attributes reporting.""" + if hdr.command_id != foundation.GeneralCommand.Report_Attributes: + return super().handle_cluster_general_request( + hdr, args, dst_addressing=dst_addressing + ) + + attr = args[0][0] + + """Pass-through reports of all attributes, except for system_mode.""" + if attr.attrid != SYSTEM_MODE_ATTR.id: + return super().handle_cluster_general_request( + hdr, args, dst_addressing=dst_addressing + ) class BoschUserInterfaceCluster(CustomCluster, UserInterface): From a60ff76bd0ff74ffe1fb078314dcc83e0a8bf857 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 5 Oct 2024 11:56:50 +0200 Subject: [PATCH 31/40] Replace hardcoded ctrl_sequence_of_oper id with value from attribute defs. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 2 +- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index 4f3e0bb0b2..db127fb0e1 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -34,7 +34,7 @@ SCREEN_BRIGHTNESS_ATTR_ID = 0x403B # Control sequence of operation (heating/cooling) -CTRL_SEQUENCE_OF_OPERATION_ID = 0x001B +CTRL_SEQUENCE_OF_OPERATION_ID = Thermostat.AttributeDefs.ctrl_sequence_of_oper.id class BoschOperatingMode(t.enum8): diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 93d3739878..cab2674e20 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -47,7 +47,7 @@ SCREEN_BRIGHTNESS_ATTR_ID = 0x403B # Control sequence of operation (heating/cooling) -CTRL_SEQUENCE_OF_OPERATION_ID = 0x001B +CTRL_SEQUENCE_OF_OPERATION_ID = Thermostat.AttributeDefs.ctrl_sequence_of_oper.id class BoschOperatingMode(t.enum8): From 7e8a8f9a49c5b6583d0c0a921069b9278f510b80 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 26 Oct 2024 17:57:17 +0200 Subject: [PATCH 32/40] Add fallback_name and translation_key to all exposed entities. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 12 ++++++++++++ zhaquirks/bosch/rbsh_trv0_zb_eu.py | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index db127fb0e1..b1a34e22a7 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -123,16 +123,22 @@ class AttributeDefs(UserInterface.AttributeDefs): BoschThermostatCluster.cluster_id, entity_platform=EntityPlatform.SENSOR, entity_type=EntityType.DIAGNOSTIC, + translation_key="operating_mode", + fallback_name="Operating mode", ) # Fast heating/boost. .switch( BoschThermostatCluster.AttributeDefs.boost_heating.name, BoschThermostatCluster.cluster_id, + translation_key="boost_heating", + fallback_name="Boost", ) # Window open switch: manually set or through an automation. .switch( BoschThermostatCluster.AttributeDefs.window_open.name, BoschThermostatCluster.cluster_id, + translation_key="window_open", + fallback_name="Window open", ) # Display time-out. .number( @@ -141,6 +147,8 @@ class AttributeDefs(UserInterface.AttributeDefs): min_value=5, max_value=30, step=1, + translation_key="display_on_time", + fallback_name="Display on-time", ) # Display brightness. .number( @@ -149,12 +157,16 @@ class AttributeDefs(UserInterface.AttributeDefs): min_value=0, max_value=10, step=1, + translation_key="display_brightness", + fallback_name="Display brightness", ) # Heating vs Cooling. .enum( Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, BoschControlSequenceOfOperation, BoschThermostatCluster.cluster_id, + translation_key="ctrl_sequence_of_oper", + fallback_name="Control sequence", ) .add_to_registry() ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index cab2674e20..5661717db9 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -435,16 +435,22 @@ async def write_attributes( BoschThermostatCluster.cluster_id, entity_platform=EntityPlatform.SENSOR, entity_type=EntityType.DIAGNOSTIC, + translation_key="operating_mode", + fallback_name="Operating mode", ) # Fast heating/boost. .switch( BoschThermostatCluster.AttributeDefs.boost_heating.name, BoschThermostatCluster.cluster_id, + translation_key="boost_heating", + fallback_name="Boost", ) # Window open switch: manually set or through an automation. .switch( BoschThermostatCluster.AttributeDefs.window_open.name, BoschThermostatCluster.cluster_id, + translation_key="window_open", + fallback_name="Window open", ) # Remote temperature. .number( @@ -455,18 +461,23 @@ async def write_attributes( step=0.1, multiplier=100, device_class=NumberDeviceClass.TEMPERATURE, + fallback_name="Remote temperature", ) # Display temperature. .enum( BoschUserInterfaceCluster.AttributeDefs.displayed_temperature.name, BoschDisplayedTemperature, BoschUserInterfaceCluster.cluster_id, + translation_key="displayed_temperature", + fallback_name="Displayed temperature", ) # Display orientation. .enum( BoschUserInterfaceCluster.AttributeDefs.display_orientation.name, BoschDisplayOrientation, BoschUserInterfaceCluster.cluster_id, + translation_key="display_orientation", + fallback_name="Display orientation", ) # Display time-out. .number( @@ -475,6 +486,8 @@ async def write_attributes( min_value=5, max_value=30, step=1, + translation_key="display_on_time", + fallback_name="Display on-time", ) # Display brightness. .number( @@ -483,12 +496,16 @@ async def write_attributes( min_value=0, max_value=10, step=1, + translation_key="display_brightness", + fallback_name="Display brightness", ) # Heating vs Cooling. .enum( Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, BoschControlSequenceOfOperation, BoschThermostatCluster.cluster_id, + translation_key="ctrl_sequence_of_oper", + fallback_name="Control sequence", ) .add_to_registry() ) From 8d0312bbc330b1a172e35905b2b940dc8b419a61 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 26 Oct 2024 18:06:00 +0200 Subject: [PATCH 33/40] Add local temperature calibration range override. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 5661717db9..f0e925649a 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -507,5 +507,16 @@ async def write_attributes( translation_key="ctrl_sequence_of_oper", fallback_name="Control sequence", ) + # Local temperature calibration. + .number( + Thermostat.AttributeDefs.local_temperature_calibration.name, + BoschThermostatCluster.cluster_id, + min_value=-5, + max_value=5, + step=0.1, + multiplier=0.1, + translation_key="local_temperature_calibration", + fallback_name="Local temperature offset", + ) .add_to_registry() ) From f43c6688218905d7b266f3c81b44fbaa3ef18622 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 26 Oct 2024 18:18:36 +0200 Subject: [PATCH 34/40] Fix remote temperature entity multiplier. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index f0e925649a..668f6ea49c 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -459,7 +459,7 @@ async def write_attributes( min_value=5, max_value=30, step=0.1, - multiplier=100, + multiplier=0.01, device_class=NumberDeviceClass.TEMPERATURE, fallback_name="Remote temperature", ) From 25385cc8ad1f3843ff82388f6b233b54d59b73a3 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Wed, 30 Oct 2024 12:44:42 +0100 Subject: [PATCH 35/40] Add local temperature calibration range override for RTH. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index b1a34e22a7..cdc5210544 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -168,5 +168,16 @@ class AttributeDefs(UserInterface.AttributeDefs): translation_key="ctrl_sequence_of_oper", fallback_name="Control sequence", ) + # Local temperature calibration. + .number( + Thermostat.AttributeDefs.local_temperature_calibration.name, + BoschThermostatCluster.cluster_id, + min_value=-5, + max_value=5, + step=0.1, + multiplier=0.1, + translation_key="local_temperature_calibration", + fallback_name="Local temperature offset", + ) .add_to_registry() ) From 0fd95e58785d20364bf9afd17358d9be4e81f1f4 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 10 Nov 2024 14:37:11 +0100 Subject: [PATCH 36/40] Add support for valve adaptation status attribute and calibration trigger command. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 72 +++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 668f6ea49c..abe3ad08cb 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -1,6 +1,6 @@ """Device handler for Bosch RBSH-TRV0-ZB-EU thermostat.""" -from typing import Any, Optional, Union +from typing import Any, Final, Optional, Union from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import QuirkBuilder @@ -13,7 +13,7 @@ Thermostat, UserInterface, ) -from zigpy.zcl.foundation import ZCLAttributeDef +from zigpy.zcl.foundation import ZCLAttributeDef, ZCLCommandDef """Bosch specific thermostat attribute ids.""" @@ -23,6 +23,9 @@ # Valve position: 0% - 100% VALVE_POSITION_ATTR_ID = 0x4020 +# Valve adaptation status. +VALVE_ADAPT_STATUS_ATTR_ID = 0x4022 + # Remote measured temperature. REMOTE_TEMPERATURE_ATTR_ID = 0x4040 @@ -49,15 +52,30 @@ # Control sequence of operation (heating/cooling) CTRL_SEQUENCE_OF_OPERATION_ID = Thermostat.AttributeDefs.ctrl_sequence_of_oper.id +"""Bosch specific commands.""" + +# Trigger valve calibration. +CALIBRATE_VALVE_CMD_ID = 0x41 + class BoschOperatingMode(t.enum8): - """Bosh operating mode attribute values.""" + """Bosch operating mode attribute values.""" Schedule = 0x00 Manual = 0x01 Pause = 0x05 +class BoschValveAdaptStatus(t.enum8): + """Bosch valve adapt status attribute values.""" + + Unknown = 0x00 + ReadyToCalibrate = 0x01 + CalibrationInProgress = 0x02 + Error = 0x03 + Success = 0x04 + + class State(t.enum8): """Binary attribute (window open) value.""" @@ -117,31 +135,44 @@ class BoschThermostatCluster(CustomCluster, Thermostat): class AttributeDefs(Thermostat.AttributeDefs): """Bosch thermostat manufacturer specific attributes.""" - operating_mode = ZCLAttributeDef( + operating_mode: Final = ZCLAttributeDef( id=OPERATING_MODE_ATTR_ID, type=BoschOperatingMode, is_manufacturer_specific=True, ) - pi_heating_demand = ZCLAttributeDef( + pi_heating_demand: Final = ZCLAttributeDef( id=VALVE_POSITION_ATTR_ID, # Values range from 0-100 type=t.enum8, is_manufacturer_specific=True, ) - window_open = ZCLAttributeDef( + valve_adapt_status: Final = ZCLAttributeDef( + id=VALVE_ADAPT_STATUS_ATTR_ID, + type=BoschValveAdaptStatus, + is_manufacturer_specific=True, + ) + + window_open: Final = ZCLAttributeDef( id=WINDOW_OPEN_ATTR_ID, type=State, is_manufacturer_specific=True ) - boost_heating = ZCLAttributeDef( + boost_heating: Final = ZCLAttributeDef( id=BOOST_HEATING_ATTR_ID, type=State, is_manufacturer_specific=True ) - remote_temperature = ZCLAttributeDef( + remote_temperature: Final = ZCLAttributeDef( id=REMOTE_TEMPERATURE_ATTR_ID, type=t.int16s, is_manufacturer_specific=True ) + class ServerCommandDefs(Thermostat.ServerCommandDefs): + """Bosch thermostat manufacturer specific server commands.""" + + calibrate_valve: Final = ZCLCommandDef( + id=CALIBRATE_VALVE_CMD_ID, schema={}, direction=False + ) + async def write_attributes( self, attributes: dict[str | int, Any], manufacturer: int | None = None ) -> list: @@ -365,28 +396,28 @@ class BoschUserInterfaceCluster(CustomCluster, UserInterface): class AttributeDefs(UserInterface.AttributeDefs): """Bosch user interface manufacturer specific attributes.""" - display_orientation = ZCLAttributeDef( + display_orientation: Final = ZCLAttributeDef( id=SCREEN_ORIENTATION_ATTR_ID, # To be matched to BoschDisplayOrientation enum. type=t.uint8_t, is_manufacturer_specific=True, ) - display_on_time = ZCLAttributeDef( + display_on_time: Final = ZCLAttributeDef( id=SCREEN_TIMEOUT_ATTR_ID, # Usable values range from 5-30 type=t.enum8, is_manufacturer_specific=True, ) - display_brightness = ZCLAttributeDef( + display_brightness: Final = ZCLAttributeDef( id=SCREEN_BRIGHTNESS_ATTR_ID, # Values range from 0-10 type=t.enum8, is_manufacturer_specific=True, ) - displayed_temperature = ZCLAttributeDef( + displayed_temperature: Final = ZCLAttributeDef( id=DISPLAY_MODE_ATTR_ID, type=BoschDisplayedTemperature, is_manufacturer_specific=True, @@ -438,6 +469,16 @@ async def write_attributes( translation_key="operating_mode", fallback_name="Operating mode", ) + # Valve adapt status - read-only. + .enum( + BoschThermostatCluster.AttributeDefs.valve_adapt_status.name, + BoschValveAdaptStatus, + BoschThermostatCluster.cluster_id, + entity_platform=EntityPlatform.SENSOR, + entity_type=EntityType.DIAGNOSTIC, + translation_key="valve_adapt_status", + fallback_name="Valve adaptation status", + ) # Fast heating/boost. .switch( BoschThermostatCluster.AttributeDefs.boost_heating.name, @@ -463,6 +504,13 @@ async def write_attributes( device_class=NumberDeviceClass.TEMPERATURE, fallback_name="Remote temperature", ) + # Valve calibration. + .command_button( + BoschThermostatCluster.ServerCommandDefs.calibrate_valve.name, + BoschThermostatCluster.cluster_id, + translation_key="calibrate_valve", + fallback_name="Calibrate valve", + ) # Display temperature. .enum( BoschUserInterfaceCluster.AttributeDefs.displayed_temperature.name, From c1f9bb5a870b5e5ff39db2420a23bdeacd4bc8d7 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 10 Nov 2024 20:30:49 +0100 Subject: [PATCH 37/40] Move valve calibration trigger to diagnostic section. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index abe3ad08cb..966171800c 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -508,6 +508,7 @@ async def write_attributes( .command_button( BoschThermostatCluster.ServerCommandDefs.calibrate_valve.name, BoschThermostatCluster.cluster_id, + entity_type=EntityType.DIAGNOSTIC, translation_key="calibrate_valve", fallback_name="Calibrate valve", ) From c0d7e2b22d0912fbfa4bb275e8bc260f03288590 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Mon, 11 Nov 2024 08:36:03 +0100 Subject: [PATCH 38/40] Setup reporting for non-standard attributes. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 966171800c..2cc41b8ff8 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -3,7 +3,7 @@ from typing import Any, Final, Optional, Union from zigpy.quirks import CustomCluster -from zigpy.quirks.v2 import QuirkBuilder +from zigpy.quirks.v2 import QuirkBuilder, ReportingConfig from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass import zigpy.types as t @@ -128,6 +128,11 @@ class BoschControlSequenceOfOperation(t.enum8): BoschDisplayOrientation.Flipped: 0x01, } +"""Battery saving Reporting Configuration""" +REPORT_CONFIG_BATTERY_SAVE = ReportingConfig( + min_interval=3600, max_interval=10800, reportable_change=1 +) + class BoschThermostatCluster(CustomCluster, Thermostat): """Bosch thermostat cluster.""" @@ -466,6 +471,7 @@ async def write_attributes( BoschThermostatCluster.cluster_id, entity_platform=EntityPlatform.SENSOR, entity_type=EntityType.DIAGNOSTIC, + reporting_config=REPORT_CONFIG_BATTERY_SAVE, translation_key="operating_mode", fallback_name="Operating mode", ) @@ -476,6 +482,7 @@ async def write_attributes( BoschThermostatCluster.cluster_id, entity_platform=EntityPlatform.SENSOR, entity_type=EntityType.DIAGNOSTIC, + reporting_config=REPORT_CONFIG_BATTERY_SAVE, translation_key="valve_adapt_status", fallback_name="Valve adaptation status", ) @@ -483,6 +490,7 @@ async def write_attributes( .switch( BoschThermostatCluster.AttributeDefs.boost_heating.name, BoschThermostatCluster.cluster_id, + reporting_config=REPORT_CONFIG_BATTERY_SAVE, translation_key="boost_heating", fallback_name="Boost", ) From e834980ed2bcdc7aa5dd1e463407624723eeefd9 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Mon, 11 Nov 2024 22:05:27 +0100 Subject: [PATCH 39/40] Fix attributes reporting and valve calibration command. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 2cc41b8ff8..b46ac99f0d 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -13,7 +13,7 @@ Thermostat, UserInterface, ) -from zigpy.zcl.foundation import ZCLAttributeDef, ZCLCommandDef +from zigpy.zcl.foundation import Direction, ZCLAttributeDef, ZCLCommandDef """Bosch specific thermostat attribute ids.""" @@ -128,9 +128,9 @@ class BoschControlSequenceOfOperation(t.enum8): BoschDisplayOrientation.Flipped: 0x01, } -"""Battery saving Reporting Configuration""" -REPORT_CONFIG_BATTERY_SAVE = ReportingConfig( - min_interval=3600, max_interval=10800, reportable_change=1 +"""Bosch Attributes Reporting Configuration""" +BOSCH_ATTR_REPORT_CONFIG = ReportingConfig( + min_interval=10, max_interval=10800, reportable_change=1 ) @@ -175,7 +175,10 @@ class ServerCommandDefs(Thermostat.ServerCommandDefs): """Bosch thermostat manufacturer specific server commands.""" calibrate_valve: Final = ZCLCommandDef( - id=CALIBRATE_VALVE_CMD_ID, schema={}, direction=False + id=CALIBRATE_VALVE_CMD_ID, + schema={}, + direction=Direction.Client_to_Server, + is_manufacturer_specific=True, ) async def write_attributes( @@ -471,7 +474,7 @@ async def write_attributes( BoschThermostatCluster.cluster_id, entity_platform=EntityPlatform.SENSOR, entity_type=EntityType.DIAGNOSTIC, - reporting_config=REPORT_CONFIG_BATTERY_SAVE, + reporting_config=BOSCH_ATTR_REPORT_CONFIG, translation_key="operating_mode", fallback_name="Operating mode", ) @@ -482,7 +485,7 @@ async def write_attributes( BoschThermostatCluster.cluster_id, entity_platform=EntityPlatform.SENSOR, entity_type=EntityType.DIAGNOSTIC, - reporting_config=REPORT_CONFIG_BATTERY_SAVE, + reporting_config=BOSCH_ATTR_REPORT_CONFIG, translation_key="valve_adapt_status", fallback_name="Valve adaptation status", ) @@ -490,7 +493,7 @@ async def write_attributes( .switch( BoschThermostatCluster.AttributeDefs.boost_heating.name, BoschThermostatCluster.cluster_id, - reporting_config=REPORT_CONFIG_BATTERY_SAVE, + reporting_config=BOSCH_ATTR_REPORT_CONFIG, translation_key="boost_heating", fallback_name="Boost", ) From fbc408b61a2832fd186483ac33745b1825cf9e6b Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 12 Nov 2024 21:48:01 +0100 Subject: [PATCH 40/40] Filter-out faulty system_mode reports inside multiple reports. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index b46ac99f0d..d04da689a9 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -389,13 +389,30 @@ def handle_cluster_general_request( hdr, args, dst_addressing=dst_addressing ) - attr = args[0][0] - """Pass-through reports of all attributes, except for system_mode.""" - if attr.attrid != SYSTEM_MODE_ATTR.id: + has_system_mode_report = False + for attr in args.attribute_reports: + if attr.attrid == SYSTEM_MODE_ATTR.id: + has_system_mode_report = True + break + + if not has_system_mode_report: return super().handle_cluster_general_request( hdr, args, dst_addressing=dst_addressing ) + else: + update_attributes = [ + attr + for attr in args.attribute_reports + if attr.attrid != SYSTEM_MODE_ATTR.id + ] + if len(update_attributes) > 0: + msg = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Report_Attributes + ].schema(attribute_reports=update_attributes) + return super().handle_cluster_general_request( + hdr, msg, dst_addressing=dst_addressing + ) class BoschUserInterfaceCluster(CustomCluster, UserInterface):