Add Danfoss Ally thermostat and derivatives to ZHA (#86907)

* zha integration: Add danfoss specific clusters and attributes; add thermostat.pi_heating_demand and thermostat_ui.keypad_lockout

* zha integration: fix Danfoss thermostat viewing direction not working because of use of bitmap8 instead of enum8

* ZHA Integration: add missing ThermostatChannelSensor

* ZHA integration: format using black

* zha integration: fix flake8 issues

* ZHA danfoss: Add MinHeatSetpointLimit, MaxHeatSetpointLimit, add reporting and read config for danfoss and keypad_lockout.

* ZHA danfoss: fix mypy complaining about type of _attr_entity_category

* ZHA danfoss: ruff fix

* fix tests

* pylint: disable-next=hass-invalid-inheritance

* fix pylint tests

* refactoring

* remove scheduled setpoint

* remove scheduled setpoint in manufacturer specific

* refactor

* fix tests

* change cluster ids

* remove custom clusters

* code quality

* match clusters in manufacturerspecific on quirk class

* fix comment

* fix match on quirk in manufacturerspecific.py

* correctly extend cluster handlers in manufacturerspecific.py and remove workaround for illegal use of attribute updated signals in climate.py

* fix style

* allow non-danfoss thermostats to work in manufacturerspecific.py

* correct order of init of parent and subclasses in manufacturerspecific.py

* improve entity names

* fix pylint

* explicitly state changing size of tuple

* ignore tuple size change error

* really ignore error

* initial

* fix tests

* match on specific name and quirk name

* don't restructure file as it is out of scope

* move back

* remove unnecessary change

* fix tests

* fix tests

* remove code duplication

* reduce code duplication

* empty line

* remove unused variable

* end file on newline

* comply with recent PRs

* correctly initialize all attributes

* comply with recent PRs

* make class variables private

* forgot one reference

* swap 2 lines for consistency

* reorder 2 lines

* fix tests

* align with recent PR

* store cluster handlers in only one place

* edit tests

* use correct device for quirk id

* change quirk id

* fix tests

* even if there is a quirk id, it doesn't have to have a specific cluster handler

* add tests

* use quirk id for manufacturer specific cluster handlers

* use quirk_ids instead of quirks_classes

* rename quirk_id

* rename quirk_id

* forgot to rename here

* rename id

* add tests

* fix tests

* fix tests

* use quirk ids from zha_quirks

* use quirk id from zha_quirks

* wrong translation

* sync changes with ZCL branch

* sync

* style

* merge error

* move bitmapSensor

* merge error

* merge error

* watch the capitals

* fix entity categories

* more decapitalization

* translate BitmapSensor

* translate all enums

* translate all enums

* don't convert camelcase to snakecase

* don't change enums at all

* remove comments

* fix bitmaps and add enum for algorithm scale factor

* improve readability if bitmapsensor

* fix capitals

* better setpoint response time

* feedback

* lowercase every enum to adhere to the translation_key standard

* remove enum state translations and use enums from quirks

* correctly capitalize OrientationEnum

* bump zha dependencies; this will have to be done in a separate PR, but this aids review

* accidentally removed enum

* tests

* comment

* Migrate reporting and ZCL attribute config out of `__init__`

* hvac.py shouldn't be changed in this pull request

* change wording comment

* I forgot I changed the size of the tuple.

---------

Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
This commit is contained in:
Caius-Bonus 2024-06-12 18:48:37 +02:00 committed by GitHub
parent 707e422a31
commit 7f7128adbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 711 additions and 5 deletions

View file

@ -5,6 +5,7 @@ from __future__ import annotations
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 @@ from .core.const import (
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

View file

@ -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,

View file

@ -6,8 +6,13 @@ import logging
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 ..const import (
)
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 @@ class SonoffPresenceSenorClusterHandler(ClusterHandler):
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),
)

View file

@ -6,12 +6,19 @@ import functools
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

View file

@ -7,7 +7,12 @@ import functools
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 @@ from .core.const import (
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

View file

@ -12,6 +12,8 @@ import numbers
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

View file

@ -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"
}
}
}

View file

@ -6,7 +6,7 @@ import functools
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 @@ from .core.const import (
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"

View file

@ -7,6 +7,7 @@ from typing import Any
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"]