2024-04-22 11:39:07 +00:00
|
|
|
"""Implementation of temperature control module."""
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-05-02 13:05:26 +00:00
|
|
|
import logging
|
|
|
|
from enum import Enum
|
2024-04-22 11:39:07 +00:00
|
|
|
|
|
|
|
from ...feature import Feature
|
|
|
|
from ..smartmodule import SmartModule
|
|
|
|
|
2024-05-02 13:05:26 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class ThermostatState(Enum):
|
|
|
|
"""Thermostat state."""
|
|
|
|
|
|
|
|
Heating = "heating"
|
|
|
|
Calibrating = "progress_calibration"
|
|
|
|
Idle = "idle"
|
|
|
|
Off = "off"
|
|
|
|
Unknown = "unknown"
|
|
|
|
|
|
|
|
|
2024-04-22 11:39:07 +00:00
|
|
|
class TemperatureControl(SmartModule):
|
|
|
|
"""Implementation of temperature module."""
|
|
|
|
|
2024-04-22 14:24:15 +00:00
|
|
|
REQUIRED_COMPONENT = "temp_control"
|
2024-04-22 11:39:07 +00:00
|
|
|
|
2024-11-10 18:55:13 +00:00
|
|
|
def _initialize_features(self) -> None:
|
2024-07-11 13:11:50 +00:00
|
|
|
"""Initialize features after the initial update."""
|
2024-04-22 11:39:07 +00:00
|
|
|
self._add_feature(
|
|
|
|
Feature(
|
2024-07-11 13:11:50 +00:00
|
|
|
self._device,
|
2024-05-07 09:13:35 +00:00
|
|
|
id="target_temperature",
|
|
|
|
name="Target temperature",
|
2024-04-22 11:39:07 +00:00
|
|
|
container=self,
|
|
|
|
attribute_getter="target_temperature",
|
|
|
|
attribute_setter="set_target_temperature",
|
2024-05-02 13:05:26 +00:00
|
|
|
range_getter="allowed_temperature_range",
|
2024-04-22 11:39:07 +00:00
|
|
|
icon="mdi:thermometer",
|
2024-04-24 16:38:52 +00:00
|
|
|
type=Feature.Type.Number,
|
2024-05-02 13:05:26 +00:00
|
|
|
category=Feature.Category.Primary,
|
2024-04-22 11:39:07 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
# TODO: this might belong into its own module, temperature_correction?
|
|
|
|
self._add_feature(
|
|
|
|
Feature(
|
2024-07-11 13:11:50 +00:00
|
|
|
self._device,
|
2024-05-07 09:13:35 +00:00
|
|
|
id="temperature_offset",
|
|
|
|
name="Temperature offset",
|
2024-04-22 11:39:07 +00:00
|
|
|
container=self,
|
|
|
|
attribute_getter="temperature_offset",
|
|
|
|
attribute_setter="set_temperature_offset",
|
2024-07-30 18:23:07 +00:00
|
|
|
range_getter=lambda: (-10, 10),
|
2024-04-24 16:38:52 +00:00
|
|
|
type=Feature.Type.Number,
|
2024-05-02 13:05:26 +00:00
|
|
|
category=Feature.Category.Config,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
self._add_feature(
|
|
|
|
Feature(
|
2024-07-11 13:11:50 +00:00
|
|
|
self._device,
|
2024-05-07 09:13:35 +00:00
|
|
|
id="state",
|
|
|
|
name="State",
|
2024-05-02 13:05:26 +00:00
|
|
|
container=self,
|
|
|
|
attribute_getter="state",
|
|
|
|
attribute_setter="set_state",
|
|
|
|
category=Feature.Category.Primary,
|
|
|
|
type=Feature.Type.Switch,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
self._add_feature(
|
|
|
|
Feature(
|
2024-07-11 13:11:50 +00:00
|
|
|
self._device,
|
2024-06-21 16:42:43 +00:00
|
|
|
id="thermostat_mode",
|
|
|
|
name="Thermostat mode",
|
2024-05-02 13:05:26 +00:00
|
|
|
container=self,
|
|
|
|
attribute_getter="mode",
|
|
|
|
category=Feature.Category.Primary,
|
2024-06-25 16:30:36 +00:00
|
|
|
type=Feature.Type.Sensor,
|
2024-04-22 11:39:07 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
def query(self) -> dict:
|
|
|
|
"""Query to execute during the update cycle."""
|
|
|
|
# Target temperature is contained in the main device info response.
|
|
|
|
return {}
|
|
|
|
|
2024-05-02 13:05:26 +00:00
|
|
|
@property
|
|
|
|
def state(self) -> bool:
|
|
|
|
"""Return thermostat state."""
|
|
|
|
return self._device.sys_info["frost_protection_on"] is False
|
|
|
|
|
2024-11-10 18:55:13 +00:00
|
|
|
async def set_state(self, enabled: bool) -> dict:
|
2024-05-02 13:05:26 +00:00
|
|
|
"""Set thermostat state."""
|
|
|
|
return await self.call("set_device_info", {"frost_protection_on": not enabled})
|
|
|
|
|
|
|
|
@property
|
|
|
|
def mode(self) -> ThermostatState:
|
|
|
|
"""Return thermostat state."""
|
|
|
|
# If frost protection is enabled, the thermostat is off.
|
|
|
|
if self._device.sys_info.get("frost_protection_on", False):
|
|
|
|
return ThermostatState.Off
|
|
|
|
|
2024-07-11 13:11:50 +00:00
|
|
|
states = self.states
|
2024-05-02 13:05:26 +00:00
|
|
|
|
|
|
|
# If the states is empty, the device is idling
|
|
|
|
if not states:
|
|
|
|
return ThermostatState.Idle
|
|
|
|
|
2024-07-11 13:11:50 +00:00
|
|
|
# Discard known extra states, and report on unknown extra states
|
|
|
|
states.discard("low_battery")
|
2024-05-02 13:05:26 +00:00
|
|
|
if len(states) > 1:
|
2024-07-11 13:11:50 +00:00
|
|
|
_LOGGER.warning("Got multiple states: %s", states)
|
2024-05-02 13:05:26 +00:00
|
|
|
|
2024-07-11 13:11:50 +00:00
|
|
|
# Return the first known state
|
|
|
|
for state in ThermostatState:
|
|
|
|
if state.value in states:
|
|
|
|
return state
|
|
|
|
|
|
|
|
_LOGGER.warning("Got unknown state: %s", states)
|
|
|
|
return ThermostatState.Unknown
|
2024-05-02 13:05:26 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def allowed_temperature_range(self) -> tuple[int, int]:
|
|
|
|
"""Return allowed temperature range."""
|
|
|
|
return self.minimum_target_temperature, self.maximum_target_temperature
|
|
|
|
|
2024-04-22 11:39:07 +00:00
|
|
|
@property
|
|
|
|
def minimum_target_temperature(self) -> int:
|
|
|
|
"""Minimum available target temperature."""
|
|
|
|
return self._device.sys_info["min_control_temp"]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def maximum_target_temperature(self) -> int:
|
|
|
|
"""Minimum available target temperature."""
|
|
|
|
return self._device.sys_info["max_control_temp"]
|
|
|
|
|
|
|
|
@property
|
2024-04-22 14:24:15 +00:00
|
|
|
def target_temperature(self) -> float:
|
2024-04-22 11:39:07 +00:00
|
|
|
"""Return target temperature."""
|
2024-04-22 14:24:15 +00:00
|
|
|
return self._device.sys_info["target_temp"]
|
2024-04-22 11:39:07 +00:00
|
|
|
|
2024-07-11 13:11:50 +00:00
|
|
|
@property
|
|
|
|
def states(self) -> set:
|
|
|
|
"""Return thermostat states."""
|
|
|
|
return set(self._device.sys_info["trv_states"])
|
|
|
|
|
2024-11-10 18:55:13 +00:00
|
|
|
async def set_target_temperature(self, target: float) -> dict:
|
2024-04-22 11:39:07 +00:00
|
|
|
"""Set target temperature."""
|
|
|
|
if (
|
|
|
|
target < self.minimum_target_temperature
|
|
|
|
or target > self.maximum_target_temperature
|
|
|
|
):
|
|
|
|
raise ValueError(
|
|
|
|
f"Invalid target temperature {target}, must be in range "
|
|
|
|
f"[{self.minimum_target_temperature},{self.maximum_target_temperature}]"
|
|
|
|
)
|
|
|
|
|
2024-05-02 13:05:26 +00:00
|
|
|
payload = {"target_temp": target}
|
|
|
|
# If the device has frost protection, we set it off to enable heating
|
|
|
|
if "frost_protection_on" in self._device.sys_info:
|
|
|
|
payload["frost_protection_on"] = False
|
|
|
|
|
|
|
|
return await self.call("set_device_info", payload)
|
2024-04-22 11:39:07 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def temperature_offset(self) -> int:
|
|
|
|
"""Return temperature offset."""
|
|
|
|
return self._device.sys_info["temp_offset"]
|
|
|
|
|
2024-11-10 18:55:13 +00:00
|
|
|
async def set_temperature_offset(self, offset: int) -> dict:
|
2024-04-22 11:39:07 +00:00
|
|
|
"""Set temperature offset."""
|
|
|
|
if offset < -10 or offset > 10:
|
|
|
|
raise ValueError("Temperature offset must be [-10, 10]")
|
|
|
|
|
|
|
|
return await self.call("set_device_info", {"temp_offset": offset})
|