From 0756d0f32d4099cdfe80536044baf31f77f2b1e1 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/57] 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 390e236db64ac45fc7565ab9c9d7d649bed2ed15 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/57] 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 23ca8edf59558fbb817f9ad87f21fe488e16faf7 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 2 May 2024 22:41:53 +0200 Subject: [PATCH 03/57] 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 41974417ee0e21db6ba2d7796906413d6332d95e 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/57] 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 db6df4123a22e02732d096a2ad82bd1d7f9cb748 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 2 May 2024 23:23:59 +0200 Subject: [PATCH 05/57] 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 9fcb77cff233280c4f3cda27576c56d051888b4a Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 2 May 2024 23:41:36 +0200 Subject: [PATCH 06/57] 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 399e1a716c8b5ff34d3a6855e9f6ebc805fef838 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 7 May 2024 23:20:27 +0200 Subject: [PATCH 07/57] 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 38d79be871f8b9966b6946a3ab580bb9f7589283 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 7 May 2024 23:39:11 +0200 Subject: [PATCH 08/57] 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 3d1b72733b03f47465d26571d126ec8cfb23d960 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 7 May 2024 23:51:33 +0200 Subject: [PATCH 09/57] 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 8f0423db6d72e3d0f952f8e18621a93844cade86 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 12 May 2024 17:26:16 +0200 Subject: [PATCH 10/57] 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 a01952b3f882343c687cfd224b44b34987a0d138 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 12 May 2024 20:19:37 +0200 Subject: [PATCH 11/57] 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 01078b554f6f0f0de586e9f01d029dd60a8cba1b Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 12 May 2024 20:46:34 +0200 Subject: [PATCH 12/57] 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 3e25fa27ef2d24c7d0a76c87830441a55ab7b816 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 14 May 2024 21:51:14 +0200 Subject: [PATCH 13/57] 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 8e4a45b783f56623a7042111d700508e5e9d0d4a Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 14 May 2024 22:52:51 +0200 Subject: [PATCH 14/57] 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 9e74d43e7d882f8cdc17c6ba9e4ebb0c3167e74a Mon Sep 17 00:00:00 2001 From: mrrstux Date: Wed, 15 May 2024 18:20:22 +0200 Subject: [PATCH 15/57] 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 98997515c16e11b1dffa87c34a003c4a1512aac8 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 16 May 2024 13:40:07 +0200 Subject: [PATCH 16/57] 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 2f18ed215940b2021bfcec30ee0ef5ef8d0f790c Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 18 May 2024 23:44:55 +0200 Subject: [PATCH 17/57] 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 b382b5d931061a40c90a04375b40cd7a9c48c8a7 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 19 May 2024 11:45:05 +0200 Subject: [PATCH 18/57] 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 76dbd3b1af94dcdfa1bf020bb8268ccb9a4a2df8 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Mon, 10 Jun 2024 01:40:15 +0200 Subject: [PATCH 19/57] 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 148f3b0191709972b7d5decfbed202ad070f2830 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 30 Jun 2024 11:50:08 +0200 Subject: [PATCH 20/57] 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 9595329aeb5a168a65458860ed98207fcde70eb4 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 30 Jun 2024 23:02:59 +0200 Subject: [PATCH 21/57] 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 32f77acd4a205a31a71edbf0e4a3e35d6652ab26 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/57] [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 b41664f3f497a57365af7fcf4ebae17d4ca11ca6 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Fri, 30 Aug 2024 21:55:34 +0200 Subject: [PATCH 23/57] 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 a418d4840d7d77b43eccc939bcf8d0987debb177 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Fri, 30 Aug 2024 22:23:36 +0200 Subject: [PATCH 24/57] 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 920ff714dae43f0981301b0130d28d8121d08aba Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 1 Oct 2024 21:29:45 +0200 Subject: [PATCH 25/57] 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 41a83fb328d90f924b616d78b46a639a33b6a749 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 01:14:11 +0200 Subject: [PATCH 26/57] 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 46543d5995f05a997cd67305bd3f56da2411cefb Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 19:24:28 +0200 Subject: [PATCH 27/57] 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 b3b1a56ee61eb645927fb7995ac29017ddf717f7 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 21:37:07 +0200 Subject: [PATCH 28/57] 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 e527d1f0f88f5207fec82ab944a380b9b24dfe6a Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 21:37:07 +0200 Subject: [PATCH 29/57] 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 2709b503a7e5490907538dff1a1e55cf373452e1 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 3 Oct 2024 21:37:07 +0200 Subject: [PATCH 30/57] 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 53603614e0e0376e162973a8f47b10e85cb70675 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 5 Oct 2024 11:56:50 +0200 Subject: [PATCH 31/57] 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 2f859a0ea2173c44be2ff51cf93736260de26080 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 26 Oct 2024 17:57:17 +0200 Subject: [PATCH 32/57] 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 d24ea84b5ef0be177c9380ca8b769a3ebc296044 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 26 Oct 2024 18:06:00 +0200 Subject: [PATCH 33/57] 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 2860e0482bc368bd65daf81678c19893ceffd959 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sat, 26 Oct 2024 18:18:36 +0200 Subject: [PATCH 34/57] 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 269907bfdbe3d1e4a380646a73d05dededf10a84 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Wed, 30 Oct 2024 12:44:42 +0100 Subject: [PATCH 35/57] 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 21abb0c3b4658d8a1af82ecc2161d4fe2e83e965 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 10 Nov 2024 14:37:11 +0100 Subject: [PATCH 36/57] 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 4b627d812179337e976e4f12b29dde819b2e1c96 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Sun, 10 Nov 2024 20:30:49 +0100 Subject: [PATCH 37/57] 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 455b7c8ee2f7269a96f25c9a023c402fd88372af Mon Sep 17 00:00:00 2001 From: mrrstux Date: Mon, 11 Nov 2024 08:36:03 +0100 Subject: [PATCH 38/57] 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 1624d30988badd4584d82106debcc15802506ca5 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Mon, 11 Nov 2024 22:05:27 +0100 Subject: [PATCH 39/57] 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 11eebfcfae0f8bbb17b3cc5eae57dab22122a008 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 12 Nov 2024 21:48:01 +0100 Subject: [PATCH 40/57] 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): From 8890097990c4e2bc7d47aa10a8b6d21a02918645 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Mon, 16 Dec 2024 17:10:13 +0100 Subject: [PATCH 41/57] Remove duplicated local temp calibration entity. Add support for newer TRV1 thermostat. --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 11 ----------- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 12 +----------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index cdc5210544..b1a34e22a7 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -168,16 +168,5 @@ 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() ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index d04da689a9..7bc766ea14 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -482,6 +482,7 @@ async def write_attributes( ( QuirkBuilder("BOSCH", "RBSH-TRV0-ZB-EU") + .applies_to("BOSCH", "RBSH-TRV1-ZB-EU") .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) # Operating mode - read-only: controlled automatically through Thermostat.system_mode (HAVC mode). @@ -584,16 +585,5 @@ 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 b73d32f28bbd4c34ceefefb20b230e085dbd2025 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 28 Jan 2025 07:01:28 +0100 Subject: [PATCH 42/57] Use zcl_type to avoid custom write_attribute code for display orientation. --- tests/test_bosch.py | 94 +----------------------------- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 37 +----------- 2 files changed, 6 insertions(+), 125 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 58c0202b31..93c0101e60 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -2,17 +2,14 @@ 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 ( - BoschDisplayOrientation, BoschOperatingMode, BoschThermostatCluster as BoschTrvThermostatCluster, - BoschUserInterfaceCluster as BoschTrvUserInterfaceCluster, ) +from zigpy.zcl import foundation +from zigpy.zcl.clusters.hvac import ControlSequenceOfOperation, Thermostat +from zigpy.zcl.foundation import WriteAttributesStatusRecord zhaquirks.setup() @@ -544,88 +541,3 @@ def mock_write(attributes, manufacturer=None): ] == 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 - ) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 7bc766ea14..a691890bb2 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 Direction, ZCLAttributeDef, ZCLCommandDef +from zigpy.zcl.foundation import DataTypeId, Direction, ZCLAttributeDef, ZCLCommandDef """Bosch specific thermostat attribute ids.""" @@ -423,8 +423,8 @@ class AttributeDefs(UserInterface.AttributeDefs): display_orientation: Final = ZCLAttributeDef( id=SCREEN_ORIENTATION_ATTR_ID, - # To be matched to BoschDisplayOrientation enum. - type=t.uint8_t, + type=BoschDisplayOrientation, + zcl_type=DataTypeId.uint8, is_manufacturer_specific=True, ) @@ -448,37 +448,6 @@ class AttributeDefs(UserInterface.AttributeDefs): 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 - - if display_orientation_attribute_id is not None: - display_orientation_value = remaining_attributes.pop( - display_orientation_attribute_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) - ( QuirkBuilder("BOSCH", "RBSH-TRV0-ZB-EU") From 7a5a4be171cfd9f63799ff3a01827a06dfab579c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 06:02:50 +0000 Subject: [PATCH 43/57] Apply pre-commit auto fixes --- tests/test_bosch.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 93c0101e60..2581416c7f 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -2,14 +2,15 @@ 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, ) -from zigpy.zcl import foundation -from zigpy.zcl.clusters.hvac import ControlSequenceOfOperation, Thermostat -from zigpy.zcl.foundation import WriteAttributesStatusRecord zhaquirks.setup() From af7be4cce97d3b508ae21b323609e607235e5bb4 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Wed, 29 Jan 2025 11:48:27 +0100 Subject: [PATCH 44/57] Add heating-by-id attribute write test. --- tests/test_bosch.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 2581416c7f..9067bd4252 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -2,15 +2,14 @@ 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, ) +from zigpy.zcl import foundation +from zigpy.zcl.clusters.hvac import ControlSequenceOfOperation, Thermostat +from zigpy.zcl.foundation import WriteAttributesStatusRecord zhaquirks.setup() @@ -58,7 +57,7 @@ def mock_read(attributes, manufacturer=None): # check that system_mode ends-up writing operating_mode: with patch_bosch_trv_write, patch_bosch_trv_read: - # - Heating operation + # - Heating operation - by name success, fail = await bosch_thermostat_cluster.write_attributes( {"ctrl_sequence_of_oper": ControlSequenceOfOperation.Heating_Only} ) @@ -71,6 +70,21 @@ def mock_read(attributes, manufacturer=None): == ControlSequenceOfOperation.Heating_Only ) + # - Heating operation - by id + success, fail = await bosch_thermostat_cluster.write_attributes( + { + Thermostat.AttributeDefs.ctrl_sequence_of_oper.id: 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 (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( {Thermostat.AttributeDefs.system_mode.name: Thermostat.SystemMode.Off} From d0123d21c3d84a21a9c51f887bd7abaaa746a42a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:49:05 +0000 Subject: [PATCH 45/57] Apply pre-commit auto fixes --- tests/test_bosch.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 9067bd4252..bf86a0a1d0 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -2,14 +2,15 @@ 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, ) -from zigpy.zcl import foundation -from zigpy.zcl.clusters.hvac import ControlSequenceOfOperation, Thermostat -from zigpy.zcl.foundation import WriteAttributesStatusRecord zhaquirks.setup() From 62e091476b7d2ed5fd91e2ba9aaabbabf4dfcae3 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Wed, 29 Jan 2025 12:09:08 +0100 Subject: [PATCH 46/57] Improve cooling mode test. Remove unused display orientation mapping. --- tests/test_bosch.py | 6 ++++++ zhaquirks/bosch/rbsh_trv0_zb_eu.py | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index bf86a0a1d0..f7687c886e 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -240,6 +240,12 @@ def mock_read(attributes, manufacturer=None): ] == ControlSequenceOfOperation.Cooling_Only ) + assert ( + bosch_thermostat_cluster._attr_cache[ + Thermostat.AttributeDefs.system_mode.id + ] + == Thermostat.SystemMode.Cool + ) # -- Off (by-name) success, fail = await bosch_thermostat_cluster.write_attributes( diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index a691890bb2..86ab5069d2 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -122,12 +122,6 @@ class BoschControlSequenceOfOperation(t.enum8): Thermostat.SystemMode.Auto: BoschOperatingMode.Schedule, } -"""Bosch display orientation enum to uint8_t mapping.""" -DISPLAY_ORIENTATION_ENUM_TO_INT_MAP = { - BoschDisplayOrientation.Normal: 0x00, - BoschDisplayOrientation.Flipped: 0x01, -} - """Bosch Attributes Reporting Configuration""" BOSCH_ATTR_REPORT_CONFIG = ReportingConfig( min_interval=10, max_interval=10800, reportable_change=1 From e25b0e08e05d9997383d09bce62b9f5408be7d95 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Wed, 29 Jan 2025 12:19:43 +0100 Subject: [PATCH 47/57] Test setting operating mode while cooling. --- tests/test_bosch.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index f7687c886e..7a5250af77 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -2,15 +2,14 @@ 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, ) +from zigpy.zcl import foundation +from zigpy.zcl.clusters.hvac import ControlSequenceOfOperation, Thermostat +from zigpy.zcl.foundation import WriteAttributesStatusRecord zhaquirks.setup() @@ -347,6 +346,27 @@ def mock_read(attributes, manufacturer=None): == ControlSequenceOfOperation.Cooling_Only ) + # -- operating_mode (by-id) in cooling mode + success, fail = await bosch_thermostat_cluster.write_attributes( + { + BoschTrvThermostatCluster.AttributeDefs.operating_mode.id: BoschOperatingMode.Manual, + } + ) + assert success + assert not fail + 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 + ) + # -- operating_mode (by-id) gets ignored when system_mode is written success, fail = await bosch_thermostat_cluster.write_attributes( { From d12c1470fbb98fa734f971895d0ed08170913d26 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:20:44 +0000 Subject: [PATCH 48/57] Apply pre-commit auto fixes --- tests/test_bosch.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 7a5250af77..4a6e38e975 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -2,14 +2,15 @@ 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, ) -from zigpy.zcl import foundation -from zigpy.zcl.clusters.hvac import ControlSequenceOfOperation, Thermostat -from zigpy.zcl.foundation import WriteAttributesStatusRecord zhaquirks.setup() From 7022f62e780ffe0431d387203c78fceae1eda0b5 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Wed, 29 Jan 2025 12:41:08 +0100 Subject: [PATCH 49/57] Add test for reading system mode when operating manually on heating or cooling. --- tests/test_bosch.py | 110 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 4a6e38e975..1eedccd382 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -413,8 +413,10 @@ def mock_read(attributes, manufacturer=None): ) -async def test_bosch_radiator_thermostat_II_read_attributes(zigpy_device_from_v2_quirk): - """Test the Radiator Thermostat II reads behaving correctly.""" +async def test_bosch_radiator_thermostat_II_read_attributes_paused( + zigpy_device_from_v2_quirk, +): + """Test the Radiator Thermostat II reads behaving correctly when paused.""" device = zigpy_device_from_v2_quirk(manufacturer="BOSCH", model="RBSH-TRV0-ZB-EU") @@ -458,6 +460,110 @@ def mock_read(attributes, manufacturer=None): assert Thermostat.SystemMode.Off in success.values() +async def test_bosch_radiator_thermostat_II_read_attributes_manual_heat( + zigpy_device_from_v2_quirk, +): + """Test the Radiator Thermostat II reads behaving correctly when heat is enabled.""" + + 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.Manual/ControlSequenceOfOperation.Heating_Only for all attributes + def mock_read(attributes, manufacturer=None): + records = [ + foundation.ReadAttributeRecord( + attr, + foundation.Status.SUCCESS, + foundation.TypeValue( + None, + BoschOperatingMode.Manual + if attr == BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + else ControlSequenceOfOperation.Heating_Only, + ), + ) + 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 and ControlSequenceOfOperation: + 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.Heat 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.Heat in success.values() + + +async def test_bosch_radiator_thermostat_II_read_attributes_manual_cool( + zigpy_device_from_v2_quirk, +): + """Test the Radiator Thermostat II reads behaving correctly when cooling is enabled.""" + + 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.Manual/ControlSequenceOfOperation.Cooling_Only for all attributes + def mock_read(attributes, manufacturer=None): + records = [ + foundation.ReadAttributeRecord( + attr, + foundation.Status.SUCCESS, + foundation.TypeValue( + None, + BoschOperatingMode.Manual + if attr == BoschTrvThermostatCluster.AttributeDefs.operating_mode.id + else ControlSequenceOfOperation.Cooling_Only, + ), + ) + 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 and ControlSequenceOfOperation: + 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.Cool 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.Cool in success.values() + + async def test_bosch_room_thermostat_II_230v_write_attributes( zigpy_device_from_v2_quirk, ): From 738f7348868145ddb2ec7f8546172cac36294fd7 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 30 Jan 2025 22:27:50 +0100 Subject: [PATCH 50/57] Small refactor - extract attribute name/id fetching from attributes to function. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 93 ++++++++++++++++-------------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 86ab5069d2..ead6ee86fb 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -128,6 +128,19 @@ class BoschControlSequenceOfOperation(t.enum8): ) +def get_attribute_id_or_name( + attribute: ZCLAttributeDef, attributes: dict[str | int, Any] | list[int | str] +) -> int | str | None: + """Return the attribute id/name when the id/name of the attribute is in the attributes list or None otherwise.""" + + if attribute.id in attributes: + return attribute.id + elif attribute.name in attributes: + return attribute.name + else: + return None + + class BoschThermostatCluster(CustomCluster, Thermostat): """Bosch thermostat cluster.""" @@ -196,24 +209,21 @@ async def write_attributes( - 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) + system_mode_attribute_id = get_attribute_id_or_name( + SYSTEM_MODE_ATTR, attributes + ) + if system_mode_attribute_id is not None: + remaining_attributes.pop(system_mode_attribute_id) + system_mode_value = attributes.get(system_mode_attribute_id) """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 - + operating_mode_attribute_id = get_attribute_id_or_name( + operating_mode_attr, attributes + ) if operating_mode_attribute_id is not None: if system_mode_value is not None: operating_mode_value = remaining_attributes.pop( @@ -259,35 +269,32 @@ async def write_attributes( """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_attribute_id = get_attribute_id_or_name( + ctrl_sequence_of_oper_attr, attributes + ) + if ctrl_sequence_of_oper_attribute_id is not None: 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 + ctrl_sequence_of_oper_attribute_id ) - if operating_mode_attr.name in successful_r: - operating_mode_attr_value = successful_r.pop( - operating_mode_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_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 + 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: @@ -310,16 +317,14 @@ async def read_attributes( 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.""" + system_mode_attribute_id = get_attribute_id_or_name( + SYSTEM_MODE_ATTR, attributes + ) if system_mode_attribute_id is not None: + """Read operating_mode instead and convert it to system_mode.""" + remaining_attributes.remove(system_mode_attribute_id) ctrl_sequence_of_oper_attr = Thermostat.AttributeDefs.ctrl_sequence_of_oper From 4e7b2f8abfea457834ee00fa7fec94f1e1e6d54b Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 6 Mar 2025 07:01:32 +0100 Subject: [PATCH 51/57] Rename fallback for boost attribute to be same as for Tuya TRVs. --- 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 b1a34e22a7..b4a587ae43 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -131,7 +131,7 @@ class AttributeDefs(UserInterface.AttributeDefs): BoschThermostatCluster.AttributeDefs.boost_heating.name, BoschThermostatCluster.cluster_id, translation_key="boost_heating", - fallback_name="Boost", + fallback_name="Boost heating", ) # Window open switch: manually set or through an automation. .switch( diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index ead6ee86fb..213be2f66b 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -481,7 +481,7 @@ class AttributeDefs(UserInterface.AttributeDefs): BoschThermostatCluster.cluster_id, reporting_config=BOSCH_ATTR_REPORT_CONFIG, translation_key="boost_heating", - fallback_name="Boost", + fallback_name="Boost heating", ) # Window open switch: manually set or through an automation. .switch( From 89aee0b89688df185eb3f5b766867a654cdf515b Mon Sep 17 00:00:00 2001 From: mrrstux Date: Thu, 7 Aug 2025 11:24:03 +0200 Subject: [PATCH 52/57] Test for read extra attributes next to system mode. --- tests/test_bosch.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 1eedccd382..498224418f 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -2,15 +2,14 @@ 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, ) +from zigpy.zcl import foundation +from zigpy.zcl.clusters.hvac import ControlSequenceOfOperation, Thermostat +from zigpy.zcl.foundation import WriteAttributesStatusRecord zhaquirks.setup() @@ -451,6 +450,17 @@ def mock_read(attributes, manufacturer=None): assert not fail assert Thermostat.SystemMode.Off in success.values() + # - system_mode by id along other attributes + success, fail = await bosch_thermostat_cluster.read_attributes( + [ + Thermostat.AttributeDefs.system_mode.id, + Thermostat.AttributeDefs.pi_heating_demand.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] @@ -459,6 +469,17 @@ def mock_read(attributes, manufacturer=None): assert not fail assert Thermostat.SystemMode.Off in success.values() + # - system_mode by name along other attributes + success, fail = await bosch_thermostat_cluster.read_attributes( + [ + Thermostat.AttributeDefs.system_mode.name, + Thermostat.AttributeDefs.pi_heating_demand.name, + ] + ) + assert success + assert not fail + assert Thermostat.SystemMode.Off in success.values() + async def test_bosch_radiator_thermostat_II_read_attributes_manual_heat( zigpy_device_from_v2_quirk, From 76b950feaba0c4402e282dd87cf50a4a609de372 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:30:51 +0000 Subject: [PATCH 53/57] Apply pre-commit auto fixes --- tests/test_bosch.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_bosch.py b/tests/test_bosch.py index 498224418f..7bd84d7aa4 100644 --- a/tests/test_bosch.py +++ b/tests/test_bosch.py @@ -2,14 +2,15 @@ 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, ) -from zigpy.zcl import foundation -from zigpy.zcl.clusters.hvac import ControlSequenceOfOperation, Thermostat -from zigpy.zcl.foundation import WriteAttributesStatusRecord zhaquirks.setup() From 817986cdf0b6ab5ee12fdfa3771c26c6f5262d9b Mon Sep 17 00:00:00 2001 From: mrrstux Date: Tue, 12 Aug 2025 23:24:38 +0200 Subject: [PATCH 54/57] Fix pi_heating_demand attribute types. --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index 213be2f66b..cacc431282 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -156,7 +156,8 @@ class AttributeDefs(Thermostat.AttributeDefs): pi_heating_demand: Final = ZCLAttributeDef( id=VALVE_POSITION_ATTR_ID, # Values range from 0-100 - type=t.enum8, + type=t.uint8_t, + zcl_type=DataTypeId.enum8, is_manufacturer_specific=True, ) From 968b21f21dde7ede683bc40e2ef7f4e8fcca27e4 Mon Sep 17 00:00:00 2001 From: mrrstux Date: Wed, 13 Aug 2025 13:49:13 +0200 Subject: [PATCH 55/57] Add support for Bosch RoomThermostat II (battery version). --- zhaquirks/bosch/rbsh_rth0_zb_eu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhaquirks/bosch/rbsh_rth0_zb_eu.py b/zhaquirks/bosch/rbsh_rth0_zb_eu.py index b4a587ae43..8e0eb80092 100644 --- a/zhaquirks/bosch/rbsh_rth0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_rth0_zb_eu.py @@ -114,6 +114,7 @@ class AttributeDefs(UserInterface.AttributeDefs): ( QuirkBuilder("Bosch", "RBSH-RTH0-ZB-EU") + .applies_to("Bosch", "RBSH-RTH0-BAT-ZB-EU") .replaces(BoschThermostatCluster) .replaces(BoschUserInterfaceCluster) # Operating mode - read-only: controlled automatically through Thermostat.system_mode (HAVC mode). From 1bface78f9be1f1c3fd499396e079f7d939061fd Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Sun, 24 Aug 2025 01:57:41 +0200 Subject: [PATCH 56/57] Remove `direction` kwarg from command definition --- zhaquirks/bosch/rbsh_trv0_zb_eu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zhaquirks/bosch/rbsh_trv0_zb_eu.py b/zhaquirks/bosch/rbsh_trv0_zb_eu.py index cacc431282..ae38721b1c 100644 --- a/zhaquirks/bosch/rbsh_trv0_zb_eu.py +++ b/zhaquirks/bosch/rbsh_trv0_zb_eu.py @@ -185,7 +185,6 @@ class ServerCommandDefs(Thermostat.ServerCommandDefs): calibrate_valve: Final = ZCLCommandDef( id=CALIBRATE_VALVE_CMD_ID, schema={}, - direction=Direction.Client_to_Server, is_manufacturer_specific=True, ) From 25b47257fa966c13d66bea0e60eba979f150652c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:57:47 +0000 Subject: [PATCH 57/57] Apply pre-commit auto fixes --- 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 ae38721b1c..4dfa79affb 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 DataTypeId, Direction, ZCLAttributeDef, ZCLCommandDef +from zigpy.zcl.foundation import DataTypeId, ZCLAttributeDef, ZCLCommandDef """Bosch specific thermostat attribute ids."""