diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 6ffb6d6f909c3..bdd2fd03ca0a5 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -5,6 +5,7 @@ import functools import logging +from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT from zigpy.quirks.v2 import BinarySensorMetadata import zigpy.types as t from zigpy.zcl.clusters.general import OnOff @@ -27,6 +28,7 @@ CLUSTER_HANDLER_HUE_OCCUPANCY, CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_ON_OFF, + CLUSTER_HANDLER_THERMOSTAT, CLUSTER_HANDLER_ZONE, ENTITY_METADATA, SIGNAL_ADD_ENTITIES, @@ -337,3 +339,43 @@ class AqaraE1CurtainMotorOpenedByHandBinarySensor(BinarySensor): _attribute_name = "hand_open" _attr_translation_key = "hand_open" _attr_entity_category = EntityCategory.DIAGNOSTIC + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossMountingModeActive(BinarySensor): + """Danfoss TRV proprietary attribute exposing whether in mounting mode.""" + + _unique_id_suffix = "mounting_mode_active" + _attribute_name = "mounting_mode_active" + _attr_translation_key: str = "mounting_mode_active" + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING + _attr_entity_category = EntityCategory.DIAGNOSTIC + + +@MULTI_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossHeatRequired(BinarySensor): + """Danfoss TRV proprietary attribute exposing whether heat is required.""" + + _unique_id_suffix = "heat_required" + _attribute_name = "heat_required" + _attr_translation_key: str = "heat_required" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossPreheatStatus(BinarySensor): + """Danfoss TRV proprietary attribute exposing whether in pre-heating mode.""" + + _unique_id_suffix = "preheat_status" + _attribute_name = "preheat_status" + _attr_translation_key: str = "preheat_status" + _attr_entity_registry_enabled_default = False + _attr_entity_category = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/zha/core/cluster_handlers/hvac.py b/homeassistant/components/zha/core/cluster_handlers/hvac.py index d455ade4e6639..1230549832b90 100644 --- a/homeassistant/components/zha/core/cluster_handlers/hvac.py +++ b/homeassistant/components/zha/core/cluster_handlers/hvac.py @@ -90,7 +90,7 @@ class PumpClusterHandler(ClusterHandler): class ThermostatClusterHandler(ClusterHandler): """Thermostat cluster handler.""" - REPORT_CONFIG = ( + REPORT_CONFIG: tuple[AttrReportConfig, ...] = ( AttrReportConfig( attr=Thermostat.AttributeDefs.local_temperature.name, config=REPORT_CONFIG_CLIMATE, diff --git a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py index dc8af82172480..9d5d68d2c7ef5 100644 --- a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py +++ b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py @@ -6,8 +6,13 @@ from typing import TYPE_CHECKING, Any from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType -from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER, XIAOMI_AQARA_VIBRATION_AQ1 +from zhaquirks.quirk_ids import ( + DANFOSS_ALLY_THERMOSTAT, + TUYA_PLUG_MANUFACTURER, + XIAOMI_AQARA_VIBRATION_AQ1, +) import zigpy.zcl +from zigpy.zcl import clusters from zigpy.zcl.clusters.closures import DoorLock from homeassistant.core import callback @@ -27,6 +32,8 @@ ) from . import AttrReportConfig, ClientClusterHandler, ClusterHandler from .general import MultistateInputClusterHandler +from .homeautomation import DiagnosticClusterHandler +from .hvac import ThermostatClusterHandler, UserInterfaceClusterHandler if TYPE_CHECKING: from ..endpoint import Endpoint @@ -444,3 +451,65 @@ def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: super().__init__(cluster, endpoint) if self.cluster.endpoint.model == "SNZB-06P": self.ZCL_INIT_ATTRS = {"last_illumination_state": True} + + +@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( + clusters.hvac.Thermostat.cluster_id, DANFOSS_ALLY_THERMOSTAT +) +class DanfossThermostatClusterHandler(ThermostatClusterHandler): + """Thermostat cluster handler for the Danfoss TRV and derivatives.""" + + REPORT_CONFIG = ( + *ThermostatClusterHandler.REPORT_CONFIG, + AttrReportConfig(attr="open_window_detection", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="heat_required", config=REPORT_CONFIG_ASAP), + AttrReportConfig(attr="mounting_mode_active", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="load_estimate", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="adaptation_run_status", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="preheat_status", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="preheat_time", config=REPORT_CONFIG_DEFAULT), + ) + + ZCL_INIT_ATTRS = { + **ThermostatClusterHandler.ZCL_INIT_ATTRS, + "external_open_window_detected": True, + "window_open_feature": True, + "exercise_day_of_week": True, + "exercise_trigger_time": True, + "mounting_mode_control": False, # Can change + "orientation": True, + "external_measured_room_sensor": False, # Can change + "radiator_covered": True, + "heat_available": True, + "load_balancing_enable": True, + "load_room_mean": False, # Can change + "control_algorithm_scale_factor": True, + "regulation_setpoint_offset": True, + "adaptation_run_control": True, + "adaptation_run_settings": True, + } + + +@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( + clusters.hvac.UserInterface.cluster_id, DANFOSS_ALLY_THERMOSTAT +) +class DanfossUserInterfaceClusterHandler(UserInterfaceClusterHandler): + """Interface cluster handler for the Danfoss TRV and derivatives.""" + + ZCL_INIT_ATTRS = { + **UserInterfaceClusterHandler.ZCL_INIT_ATTRS, + "viewing_direction": True, + } + + +@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( + clusters.homeautomation.Diagnostic.cluster_id, DANFOSS_ALLY_THERMOSTAT +) +class DanfossDiagnosticClusterHandler(DiagnosticClusterHandler): + """Diagnostic cluster handler for the Danfoss TRV and derivatives.""" + + REPORT_CONFIG = ( + *DiagnosticClusterHandler.REPORT_CONFIG, + AttrReportConfig(attr="sw_error_code", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="motor_step_counter", config=REPORT_CONFIG_DEFAULT), + ) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 8af2fe178c863..9320b4494a444 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -6,12 +6,19 @@ import logging from typing import TYPE_CHECKING, Any, Self +from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT from zigpy.quirks.v2 import NumberMetadata from zigpy.zcl.clusters.hvac import Thermostat from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature +from homeassistant.const import ( + EntityCategory, + Platform, + UnitOfMass, + UnitOfTemperature, + UnitOfTime, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -1073,3 +1080,74 @@ class MinHeatSetpointLimit(ZCLHeatSetpointLimitEntity): _attr_entity_category = EntityCategory.CONFIG _max_source = Thermostat.AttributeDefs.max_heat_setpoint_limit.name + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossExerciseTriggerTime(ZHANumberConfigurationEntity): + """Danfoss proprietary attribute to set the time to exercise the valve.""" + + _unique_id_suffix = "exercise_trigger_time" + _attribute_name: str = "exercise_trigger_time" + _attr_translation_key: str = "exercise_trigger_time" + _attr_native_min_value: int = 0 + _attr_native_max_value: int = 1439 + _attr_mode: NumberMode = NumberMode.BOX + _attr_native_unit_of_measurement: str = UnitOfTime.MINUTES + _attr_icon: str = "mdi:clock" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossExternalMeasuredRoomSensor(ZCLTemperatureEntity): + """Danfoss proprietary attribute to communicate the value of the external temperature sensor.""" + + _unique_id_suffix = "external_measured_room_sensor" + _attribute_name: str = "external_measured_room_sensor" + _attr_translation_key: str = "external_temperature_sensor" + _attr_native_min_value: float = -80 + _attr_native_max_value: float = 35 + _attr_icon: str = "mdi:thermometer" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossLoadRoomMean(ZHANumberConfigurationEntity): + """Danfoss proprietary attribute to set a value for the load.""" + + _unique_id_suffix = "load_room_mean" + _attribute_name: str = "load_room_mean" + _attr_translation_key: str = "load_room_mean" + _attr_native_min_value: int = -8000 + _attr_native_max_value: int = 2000 + _attr_mode: NumberMode = NumberMode.BOX + _attr_icon: str = "mdi:scale-balance" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossRegulationSetpointOffset(ZHANumberConfigurationEntity): + """Danfoss proprietary attribute to set the regulation setpoint offset.""" + + _unique_id_suffix = "regulation_setpoint_offset" + _attribute_name: str = "regulation_setpoint_offset" + _attr_translation_key: str = "regulation_setpoint_offset" + _attr_mode: NumberMode = NumberMode.BOX + _attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS + _attr_icon: str = "mdi:thermostat" + _attr_native_min_value: float = -2.5 + _attr_native_max_value: float = 2.5 + _attr_native_step: float = 0.1 + _attr_multiplier = 1 / 10 diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 98d5debd99963..026a85fbfdcc2 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -7,7 +7,12 @@ import logging from typing import TYPE_CHECKING, Any, Self -from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER, TUYA_PLUG_ONOFF +from zhaquirks.danfoss import thermostat as danfoss_thermostat +from zhaquirks.quirk_ids import ( + DANFOSS_ALLY_THERMOSTAT, + TUYA_PLUG_MANUFACTURER, + TUYA_PLUG_ONOFF, +) from zhaquirks.xiaomi.aqara.magnet_ac01 import OppleCluster as MagnetAC01OppleCluster from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster from zigpy import types @@ -29,6 +34,7 @@ CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_ON_OFF, + CLUSTER_HANDLER_THERMOSTAT, ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, @@ -688,3 +694,105 @@ class KeypadLockout(ZCLEnumSelectEntity): _attribute_name: str = "keypad_lockout" _enum = KeypadLockoutEnum _attr_translation_key: str = "keypad_lockout" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossExerciseDayOfTheWeek(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for setting the day of the week for exercising.""" + + _unique_id_suffix = "exercise_day_of_week" + _attribute_name = "exercise_day_of_week" + _attr_translation_key: str = "exercise_day_of_week" + _enum = danfoss_thermostat.DanfossExerciseDayOfTheWeekEnum + _attr_icon: str = "mdi:wrench-clock" + + +class DanfossOrientationEnum(types.enum8): + """Vertical or Horizontal.""" + + Horizontal = 0x00 + Vertical = 0x01 + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossOrientation(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for setting the orientation of the valve. + + Needed for biasing the internal temperature sensor. + This is implemented as an enum here, but is a boolean on the device. + """ + + _unique_id_suffix = "orientation" + _attribute_name = "orientation" + _attr_translation_key: str = "valve_orientation" + _enum = DanfossOrientationEnum + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossAdaptationRunControl(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for controlling the current adaptation run.""" + + _unique_id_suffix = "adaptation_run_control" + _attribute_name = "adaptation_run_control" + _attr_translation_key: str = "adaptation_run_command" + _enum = danfoss_thermostat.DanfossAdaptationRunControlEnum + + +class DanfossControlAlgorithmScaleFactorEnum(types.enum8): + """The time scale factor for changing the opening of the valve. + + Not all values are given, therefore there are some extrapolated values with a margin of error of about 5 minutes. + This is implemented as an enum here, but is a number on the device. + """ + + quick_5min = 0x01 + + quick_10min = 0x02 # extrapolated + quick_15min = 0x03 # extrapolated + quick_25min = 0x04 # extrapolated + + moderate_30min = 0x05 + + moderate_40min = 0x06 # extrapolated + moderate_50min = 0x07 # extrapolated + moderate_60min = 0x08 # extrapolated + moderate_70min = 0x09 # extrapolated + + slow_80min = 0x0A + + quick_open_disabled = 0x11 # not sure what it does; also requires lower 4 bits to be in [1, 10] I assume + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossControlAlgorithmScaleFactor(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for setting the scale factor of the setpoint filter time constant.""" + + _unique_id_suffix = "control_algorithm_scale_factor" + _attribute_name = "control_algorithm_scale_factor" + _attr_translation_key: str = "setpoint_response_time" + _enum = DanfossControlAlgorithmScaleFactorEnum + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="thermostat_ui", + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossViewingDirection(ZCLEnumSelectEntity): + """Danfoss proprietary attribute for setting the viewing direction of the screen.""" + + _unique_id_suffix = "viewing_direction" + _attribute_name = "viewing_direction" + _attr_translation_key: str = "viewing_direction" + _enum = danfoss_thermostat.DanfossViewingDirectionEnum diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 9e98060667a3c..99d950dc06af3 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -12,6 +12,8 @@ import random from typing import TYPE_CHECKING, Any, Self +from zhaquirks.danfoss import thermostat as danfoss_thermostat +from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT from zigpy import types from zigpy.quirks.v2 import ZCLEnumMetadata, ZCLSensorMetadata from zigpy.state import Counter, State @@ -1499,3 +1501,129 @@ class AqaraCurtainHookStateSensor(EnumSensor): _attr_translation_key: str = "hooks_state" _attr_icon: str = "mdi:hook" _attr_entity_category = EntityCategory.DIAGNOSTIC + + +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class BitMapSensor(Sensor): + """A sensor with only state attributes. + + The sensor value will be an aggregate of the state attributes. + """ + + _bitmap: types.bitmap8 | types.bitmap16 + + def formatter(self, _value: int) -> str: + """Summary of all attributes.""" + binary_state_attributes = [ + key for (key, elem) in self.extra_state_attributes.items() if elem + ] + + return "something" if binary_state_attributes else "nothing" + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Bitmap.""" + value = self._cluster_handler.cluster.get(self._attribute_name) + + state_attr = {} + + for bit in list(self._bitmap): + if value is None: + state_attr[bit.name] = False + else: + state_attr[bit.name] = bit in self._bitmap(value) + + return state_attr + + +@MULTI_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossOpenWindowDetection(EnumSensor): + """Danfoss proprietary attribute. + + Sensor that displays whether the TRV detects an open window using the temperature sensor. + """ + + _unique_id_suffix = "open_window_detection" + _attribute_name = "open_window_detection" + _attr_translation_key: str = "open_window_detected" + _attr_icon: str = "mdi:window-open" + _enum = danfoss_thermostat.DanfossOpenWindowDetectionEnum + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossLoadEstimate(Sensor): + """Danfoss proprietary attribute for communicating its estimate of the radiator load.""" + + _unique_id_suffix = "load_estimate" + _attribute_name = "load_estimate" + _attr_translation_key: str = "load_estimate" + _attr_icon: str = "mdi:scale-balance" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossAdaptationRunStatus(BitMapSensor): + """Danfoss proprietary attribute for showing the status of the adaptation run.""" + + _unique_id_suffix = "adaptation_run_status" + _attribute_name = "adaptation_run_status" + _attr_translation_key: str = "adaptation_run_status" + _attr_entity_category = EntityCategory.DIAGNOSTIC + _bitmap = danfoss_thermostat.DanfossAdaptationRunStatusBitmap + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossPreheatTime(Sensor): + """Danfoss proprietary attribute for communicating the time when it starts pre-heating.""" + + _unique_id_suffix = "preheat_time" + _attribute_name = "preheat_time" + _attr_translation_key: str = "preheat_time" + _attr_icon: str = "mdi:radiator" + _attr_entity_registry_enabled_default = False + _attr_entity_category = EntityCategory.DIAGNOSTIC + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="diagnostic", + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossSoftwareErrorCode(BitMapSensor): + """Danfoss proprietary attribute for communicating the error code.""" + + _unique_id_suffix = "sw_error_code" + _attribute_name = "sw_error_code" + _attr_translation_key: str = "software_error" + _attr_entity_category = EntityCategory.DIAGNOSTIC + _bitmap = danfoss_thermostat.DanfossSoftwareErrorCodeBitmap + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="diagnostic", + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +# pylint: disable-next=hass-invalid-inheritance # needs fixing +class DanfossMotorStepCounter(Sensor): + """Danfoss proprietary attribute for communicating the motor step counter.""" + + _unique_id_suffix = "motor_step_counter" + _attribute_name = "motor_step_counter" + _attr_translation_key: str = "motor_stepcount" + _attr_entity_category = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 3db54712deeff..04cef23b2df09 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -569,6 +569,15 @@ }, "hand_open": { "name": "Opened by hand" + }, + "mounting_mode_active": { + "name": "Mounting mode active" + }, + "heat_required": { + "name": "Heat required" + }, + "preheat_status": { + "name": "Pre-heat status" } }, "button": { @@ -739,6 +748,18 @@ }, "min_heat_setpoint_limit": { "name": "Min heat setpoint limit" + }, + "exercise_trigger_time": { + "name": "Exercise start time" + }, + "external_temperature_sensor": { + "name": "External temperature sensor" + }, + "load_room_mean": { + "name": "Load room mean" + }, + "regulation_setpoint_offset": { + "name": "Regulation setpoint offset" } }, "select": { @@ -810,6 +831,21 @@ }, "keypad_lockout": { "name": "Keypad lockout" + }, + "exercise_day_of_week": { + "name": "Exercise day of the week" + }, + "valve_orientation": { + "name": "Valve orientation" + }, + "adaptation_run_command": { + "name": "Adaptation run command" + }, + "viewing_direction": { + "name": "Viewing direction" + }, + "setpoint_response_time": { + "name": "Setpoint response time" } }, "sensor": { @@ -908,6 +944,78 @@ }, "hooks_state": { "name": "Hooks state" + }, + "open_window_detected": { + "name": "Open window detected" + }, + "load_estimate": { + "name": "Load estimate" + }, + "adaptation_run_status": { + "name": "Adaptation run status", + "state": { + "nothing": "Idle", + "something": "State" + }, + "state_attributes": { + "in_progress": { + "name": "In progress" + }, + "run_successful": { + "name": "Run successful" + }, + "valve_characteristic_lost": { + "name": "Valve characteristic lost" + } + } + }, + "preheat_time": { + "name": "Pre-heat time" + }, + "software_error": { + "name": "Software error", + "state": { + "nothing": "Good", + "something": "Error" + }, + "state_attributes": { + "top_pcb_sensor_error": { + "name": "Top PCB sensor error" + }, + "side_pcb_sensor_error": { + "name": "Side PCB sensor error" + }, + "non_volatile_memory_error": { + "name": "Non-volatile memory error" + }, + "unknown_hw_error": { + "name": "Unknown HW error" + }, + "motor_error": { + "name": "Motor error" + }, + "invalid_internal_communication": { + "name": "Invalid internal communication" + }, + "invalid_clock_information": { + "name": "Invalid clock information" + }, + "radio_communication_error": { + "name": "Radio communication error" + }, + "encoder_jammed": { + "name": "Encoder jammed" + }, + "low_battery": { + "name": "Low battery" + }, + "critical_low_battery": { + "name": "Critical low battery" + } + } + }, + "motor_stepcount": { + "name": "Motor stepcount" } }, "switch": { @@ -991,6 +1099,27 @@ }, "buzzer_manual_alarm": { "name": "Buzzer manual alarm" + }, + "external_window_sensor": { + "name": "External window sensor" + }, + "use_internal_window_detection": { + "name": "Use internal window detection" + }, + "mounting_mode": { + "name": "Mounting mode" + }, + "prioritize_external_temperature_sensor": { + "name": "Prioritize external temperature sensor" + }, + "heat_available": { + "name": "Heat available" + }, + "use_load_balancing": { + "name": "Use load balancing" + }, + "adaptation_run_enabled": { + "name": "Adaptation run enabled" } } } diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 14da2344cd46a..f07d3d4c8e334 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Self -from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF +from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT, TUYA_PLUG_ONOFF from zigpy.quirks.v2 import SwitchMetadata from zigpy.zcl.clusters.closures import ConfigStatus, WindowCovering, WindowCoveringMode from zigpy.zcl.clusters.general import OnOff @@ -25,6 +25,7 @@ CLUSTER_HANDLER_COVER, CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_ON_OFF, + CLUSTER_HANDLER_THERMOSTAT, ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, @@ -716,3 +717,95 @@ class AqaraE1CurtainMotorHooksLockedSwitch(ZHASwitchConfigurationEntity): _unique_id_suffix = "hooks_lock" _attribute_name = "hooks_lock" _attr_translation_key = "hooks_locked" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossExternalOpenWindowDetected(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for communicating an open window.""" + + _unique_id_suffix = "external_open_window_detected" + _attribute_name: str = "external_open_window_detected" + _attr_translation_key: str = "external_window_sensor" + _attr_icon: str = "mdi:window-open" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossWindowOpenFeature(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute enabling open window detection.""" + + _unique_id_suffix = "window_open_feature" + _attribute_name: str = "window_open_feature" + _attr_translation_key: str = "use_internal_window_detection" + _attr_icon: str = "mdi:window-open" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossMountingModeControl(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for switching to mounting mode.""" + + _unique_id_suffix = "mounting_mode_control" + _attribute_name: str = "mounting_mode_control" + _attr_translation_key: str = "mounting_mode" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossRadiatorCovered(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for communicating full usage of the external temperature sensor.""" + + _unique_id_suffix = "radiator_covered" + _attribute_name: str = "radiator_covered" + _attr_translation_key: str = "prioritize_external_temperature_sensor" + _attr_icon: str = "mdi:thermometer" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossHeatAvailable(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for communicating available heat.""" + + _unique_id_suffix = "heat_available" + _attribute_name: str = "heat_available" + _attr_translation_key: str = "heat_available" + _attr_icon: str = "mdi:water-boiler" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossLoadBalancingEnable(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for enabling load balancing.""" + + _unique_id_suffix = "load_balancing_enable" + _attribute_name: str = "load_balancing_enable" + _attr_translation_key: str = "use_load_balancing" + _attr_icon: str = "mdi:scale-balance" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, + quirk_ids={DANFOSS_ALLY_THERMOSTAT}, +) +class DanfossAdaptationRunSettings(ZHASwitchConfigurationEntity): + """Danfoss proprietary attribute for enabling daily adaptation run. + + Actually a bitmap, but only the first bit is used. + """ + + _unique_id_suffix = "adaptation_run_settings" + _attribute_name: str = "adaptation_run_settings" + _attr_translation_key: str = "adaptation_run_enabled" diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 86868ef65c215..8443c4ced0715 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch import pytest +from zhaquirks.danfoss import thermostat as danfoss_thermostat import zigpy.profiles.zha from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 @@ -1316,3 +1317,61 @@ async def test_device_counter_sensors( state = hass.states.get(entity_id) assert state is not None assert state.state == "2" + + +@pytest.fixture +async def zigpy_device_danfoss_thermostat( + hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored +): + """Device tracker zigpy danfoss thermostat device.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.Time.cluster_id, + general.PollControl.cluster_id, + Thermostat.cluster_id, + hvac.UserInterface.cluster_id, + homeautomation.Diagnostic.cluster_id, + ], + SIG_EP_OUTPUT: [general.Basic.cluster_id, general.Ota.cluster_id], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, + } + }, + manufacturer="Danfoss", + model="eTRV0100", + ) + + zha_device = await zha_device_joined_restored(zigpy_device) + return zha_device, zigpy_device + + +async def test_danfoss_thermostat_sw_error( + hass: HomeAssistant, zigpy_device_danfoss_thermostat +) -> None: + """Test quirks defined thermostat.""" + + zha_device, zigpy_device = zigpy_device_danfoss_thermostat + + entity_id = find_entity_id( + Platform.SENSOR, zha_device, hass, qualifier="software_error" + ) + assert entity_id is not None + + cluster = zigpy_device.endpoints[1].diagnostic + + await send_attributes_report( + hass, + cluster, + { + danfoss_thermostat.DanfossDiagnosticCluster.AttributeDefs.sw_error_code.id: 0x0001 + }, + ) + + hass_state = hass.states.get(entity_id) + assert hass_state.state == "something" + assert hass_state.attributes["Top_pcb_sensor_error"]