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:
parent
707e422a31
commit
7f7128adbf
9 changed files with 711 additions and 5 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue