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 functools
import logging import logging
from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT
from zigpy.quirks.v2 import BinarySensorMetadata from zigpy.quirks.v2 import BinarySensorMetadata
import zigpy.types as t import zigpy.types as t
from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.general import OnOff
@ -27,6 +28,7 @@ from .core.const import (
CLUSTER_HANDLER_HUE_OCCUPANCY, CLUSTER_HANDLER_HUE_OCCUPANCY,
CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_OCCUPANCY,
CLUSTER_HANDLER_ON_OFF, CLUSTER_HANDLER_ON_OFF,
CLUSTER_HANDLER_THERMOSTAT,
CLUSTER_HANDLER_ZONE, CLUSTER_HANDLER_ZONE,
ENTITY_METADATA, ENTITY_METADATA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
@ -337,3 +339,43 @@ class AqaraE1CurtainMotorOpenedByHandBinarySensor(BinarySensor):
_attribute_name = "hand_open" _attribute_name = "hand_open"
_attr_translation_key = "hand_open" _attr_translation_key = "hand_open"
_attr_entity_category = EntityCategory.DIAGNOSTIC _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): class ThermostatClusterHandler(ClusterHandler):
"""Thermostat cluster handler.""" """Thermostat cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG: tuple[AttrReportConfig, ...] = (
AttrReportConfig( AttrReportConfig(
attr=Thermostat.AttributeDefs.local_temperature.name, attr=Thermostat.AttributeDefs.local_temperature.name,
config=REPORT_CONFIG_CLIMATE, config=REPORT_CONFIG_CLIMATE,

View file

@ -6,8 +6,13 @@ import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType 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 import zigpy.zcl
from zigpy.zcl import clusters
from zigpy.zcl.clusters.closures import DoorLock from zigpy.zcl.clusters.closures import DoorLock
from homeassistant.core import callback from homeassistant.core import callback
@ -27,6 +32,8 @@ from ..const import (
) )
from . import AttrReportConfig, ClientClusterHandler, ClusterHandler from . import AttrReportConfig, ClientClusterHandler, ClusterHandler
from .general import MultistateInputClusterHandler from .general import MultistateInputClusterHandler
from .homeautomation import DiagnosticClusterHandler
from .hvac import ThermostatClusterHandler, UserInterfaceClusterHandler
if TYPE_CHECKING: if TYPE_CHECKING:
from ..endpoint import Endpoint from ..endpoint import Endpoint
@ -444,3 +451,65 @@ class SonoffPresenceSenorClusterHandler(ClusterHandler):
super().__init__(cluster, endpoint) super().__init__(cluster, endpoint)
if self.cluster.endpoint.model == "SNZB-06P": if self.cluster.endpoint.model == "SNZB-06P":
self.ZCL_INIT_ATTRS = {"last_illumination_state": True} 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 import logging
from typing import TYPE_CHECKING, Any, Self from typing import TYPE_CHECKING, Any, Self
from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT
from zigpy.quirks.v2 import NumberMetadata from zigpy.quirks.v2 import NumberMetadata
from zigpy.zcl.clusters.hvac import Thermostat from zigpy.zcl.clusters.hvac import Thermostat
from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -1073,3 +1080,74 @@ class MinHeatSetpointLimit(ZCLHeatSetpointLimitEntity):
_attr_entity_category = EntityCategory.CONFIG _attr_entity_category = EntityCategory.CONFIG
_max_source = Thermostat.AttributeDefs.max_heat_setpoint_limit.name _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 import logging
from typing import TYPE_CHECKING, Any, Self 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.magnet_ac01 import OppleCluster as MagnetAC01OppleCluster
from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster
from zigpy import types from zigpy import types
@ -29,6 +34,7 @@ from .core.const import (
CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_INOVELLI,
CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_OCCUPANCY,
CLUSTER_HANDLER_ON_OFF, CLUSTER_HANDLER_ON_OFF,
CLUSTER_HANDLER_THERMOSTAT,
ENTITY_METADATA, ENTITY_METADATA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -688,3 +694,105 @@ class KeypadLockout(ZCLEnumSelectEntity):
_attribute_name: str = "keypad_lockout" _attribute_name: str = "keypad_lockout"
_enum = KeypadLockoutEnum _enum = KeypadLockoutEnum
_attr_translation_key: str = "keypad_lockout" _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 import random
from typing import TYPE_CHECKING, Any, Self 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 import types
from zigpy.quirks.v2 import ZCLEnumMetadata, ZCLSensorMetadata from zigpy.quirks.v2 import ZCLEnumMetadata, ZCLSensorMetadata
from zigpy.state import Counter, State from zigpy.state import Counter, State
@ -1499,3 +1501,129 @@ class AqaraCurtainHookStateSensor(EnumSensor):
_attr_translation_key: str = "hooks_state" _attr_translation_key: str = "hooks_state"
_attr_icon: str = "mdi:hook" _attr_icon: str = "mdi:hook"
_attr_entity_category = EntityCategory.DIAGNOSTIC _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": { "hand_open": {
"name": "Opened by hand" "name": "Opened by hand"
},
"mounting_mode_active": {
"name": "Mounting mode active"
},
"heat_required": {
"name": "Heat required"
},
"preheat_status": {
"name": "Pre-heat status"
} }
}, },
"button": { "button": {
@ -739,6 +748,18 @@
}, },
"min_heat_setpoint_limit": { "min_heat_setpoint_limit": {
"name": "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": { "select": {
@ -810,6 +831,21 @@
}, },
"keypad_lockout": { "keypad_lockout": {
"name": "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": { "sensor": {
@ -908,6 +944,78 @@
}, },
"hooks_state": { "hooks_state": {
"name": "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": { "switch": {
@ -991,6 +1099,27 @@
}, },
"buzzer_manual_alarm": { "buzzer_manual_alarm": {
"name": "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 import logging
from typing import TYPE_CHECKING, Any, Self 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.quirks.v2 import SwitchMetadata
from zigpy.zcl.clusters.closures import ConfigStatus, WindowCovering, WindowCoveringMode from zigpy.zcl.clusters.closures import ConfigStatus, WindowCovering, WindowCoveringMode
from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.general import OnOff
@ -25,6 +25,7 @@ from .core.const import (
CLUSTER_HANDLER_COVER, CLUSTER_HANDLER_COVER,
CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_INOVELLI,
CLUSTER_HANDLER_ON_OFF, CLUSTER_HANDLER_ON_OFF,
CLUSTER_HANDLER_THERMOSTAT,
ENTITY_METADATA, ENTITY_METADATA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -716,3 +717,95 @@ class AqaraE1CurtainMotorHooksLockedSwitch(ZHASwitchConfigurationEntity):
_unique_id_suffix = "hooks_lock" _unique_id_suffix = "hooks_lock"
_attribute_name = "hooks_lock" _attribute_name = "hooks_lock"
_attr_translation_key = "hooks_locked" _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 from unittest.mock import MagicMock, patch
import pytest import pytest
from zhaquirks.danfoss import thermostat as danfoss_thermostat
import zigpy.profiles.zha import zigpy.profiles.zha
from zigpy.quirks import CustomCluster from zigpy.quirks import CustomCluster
from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 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) state = hass.states.get(entity_id)
assert state is not None assert state is not None
assert state.state == "2" 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"]