Skip to content

Commit

Permalink
Add Danfoss Ally thermostat and derivatives to ZHA (#86907)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
Caius-Bonus and puddly authored Jun 12, 2024
1 parent 707e422 commit 7f7128a
Show file tree
Hide file tree
Showing 9 changed files with 711 additions and 5 deletions.
42 changes: 42 additions & 0 deletions homeassistant/components/zha/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import functools
import logging

from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT
from zigpy.quirks.v2 import BinarySensorMetadata
import zigpy.types as t
from zigpy.zcl.clusters.general import OnOff
Expand All @@ -27,6 +28,7 @@
CLUSTER_HANDLER_HUE_OCCUPANCY,
CLUSTER_HANDLER_OCCUPANCY,
CLUSTER_HANDLER_ON_OFF,
CLUSTER_HANDLER_THERMOSTAT,
CLUSTER_HANDLER_ZONE,
ENTITY_METADATA,
SIGNAL_ADD_ENTITIES,
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion homeassistant/components/zha/core/cluster_handlers/hvac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
from typing import TYPE_CHECKING, Any

from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType
from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER, XIAOMI_AQARA_VIBRATION_AQ1
from zhaquirks.quirk_ids import (
DANFOSS_ALLY_THERMOSTAT,
TUYA_PLUG_MANUFACTURER,
XIAOMI_AQARA_VIBRATION_AQ1,
)
import zigpy.zcl
from zigpy.zcl import clusters
from zigpy.zcl.clusters.closures import DoorLock

from homeassistant.core import callback
Expand All @@ -27,6 +32,8 @@
)
from . import AttrReportConfig, ClientClusterHandler, ClusterHandler
from .general import MultistateInputClusterHandler
from .homeautomation import DiagnosticClusterHandler
from .hvac import ThermostatClusterHandler, UserInterfaceClusterHandler

if TYPE_CHECKING:
from ..endpoint import Endpoint
Expand Down Expand Up @@ -444,3 +451,65 @@ def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
super().__init__(cluster, endpoint)
if self.cluster.endpoint.model == "SNZB-06P":
self.ZCL_INIT_ATTRS = {"last_illumination_state": True}


@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
clusters.hvac.Thermostat.cluster_id, DANFOSS_ALLY_THERMOSTAT
)
class DanfossThermostatClusterHandler(ThermostatClusterHandler):
"""Thermostat cluster handler for the Danfoss TRV and derivatives."""

REPORT_CONFIG = (
*ThermostatClusterHandler.REPORT_CONFIG,
AttrReportConfig(attr="open_window_detection", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="heat_required", config=REPORT_CONFIG_ASAP),
AttrReportConfig(attr="mounting_mode_active", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="load_estimate", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="adaptation_run_status", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="preheat_status", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="preheat_time", config=REPORT_CONFIG_DEFAULT),
)

ZCL_INIT_ATTRS = {
**ThermostatClusterHandler.ZCL_INIT_ATTRS,
"external_open_window_detected": True,
"window_open_feature": True,
"exercise_day_of_week": True,
"exercise_trigger_time": True,
"mounting_mode_control": False, # Can change
"orientation": True,
"external_measured_room_sensor": False, # Can change
"radiator_covered": True,
"heat_available": True,
"load_balancing_enable": True,
"load_room_mean": False, # Can change
"control_algorithm_scale_factor": True,
"regulation_setpoint_offset": True,
"adaptation_run_control": True,
"adaptation_run_settings": True,
}


@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
clusters.hvac.UserInterface.cluster_id, DANFOSS_ALLY_THERMOSTAT
)
class DanfossUserInterfaceClusterHandler(UserInterfaceClusterHandler):
"""Interface cluster handler for the Danfoss TRV and derivatives."""

ZCL_INIT_ATTRS = {
**UserInterfaceClusterHandler.ZCL_INIT_ATTRS,
"viewing_direction": True,
}


@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
clusters.homeautomation.Diagnostic.cluster_id, DANFOSS_ALLY_THERMOSTAT
)
class DanfossDiagnosticClusterHandler(DiagnosticClusterHandler):
"""Diagnostic cluster handler for the Danfoss TRV and derivatives."""

REPORT_CONFIG = (
*DiagnosticClusterHandler.REPORT_CONFIG,
AttrReportConfig(attr="sw_error_code", config=REPORT_CONFIG_DEFAULT),
AttrReportConfig(attr="motor_step_counter", config=REPORT_CONFIG_DEFAULT),
)
80 changes: 79 additions & 1 deletion homeassistant/components/zha/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@
import logging
from typing import TYPE_CHECKING, Any, Self

from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT
from zigpy.quirks.v2 import NumberMetadata
from zigpy.zcl.clusters.hvac import Thermostat

from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory, Platform, UnitOfMass, UnitOfTemperature
from homeassistant.const import (
EntityCategory,
Platform,
UnitOfMass,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
Expand Down Expand Up @@ -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
110 changes: 109 additions & 1 deletion homeassistant/components/zha/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
import logging
from typing import TYPE_CHECKING, Any, Self

from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER, TUYA_PLUG_ONOFF
from zhaquirks.danfoss import thermostat as danfoss_thermostat
from zhaquirks.quirk_ids import (
DANFOSS_ALLY_THERMOSTAT,
TUYA_PLUG_MANUFACTURER,
TUYA_PLUG_ONOFF,
)
from zhaquirks.xiaomi.aqara.magnet_ac01 import OppleCluster as MagnetAC01OppleCluster
from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster
from zigpy import types
Expand All @@ -29,6 +34,7 @@
CLUSTER_HANDLER_INOVELLI,
CLUSTER_HANDLER_OCCUPANCY,
CLUSTER_HANDLER_ON_OFF,
CLUSTER_HANDLER_THERMOSTAT,
ENTITY_METADATA,
SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED,
Expand Down Expand Up @@ -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
Loading

0 comments on commit 7f7128a

Please sign in to comment.