mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-23 19:53:34 +00:00
a044063526
Instead of trying to use the first state when multiple are reported, iterate over the known states and pick the first matching. This will fix an issue where the device reports extra states (like `low_battery`) while having a known mode active. Related to https://github.com/home-assistant/core/issues/121335
180 lines
5.7 KiB
Python
180 lines
5.7 KiB
Python
"""Implementation of temperature control module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from enum import Enum
|
|
|
|
from ...feature import Feature
|
|
from ..smartmodule import SmartModule
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class ThermostatState(Enum):
|
|
"""Thermostat state."""
|
|
|
|
Heating = "heating"
|
|
Calibrating = "progress_calibration"
|
|
Idle = "idle"
|
|
Off = "off"
|
|
Unknown = "unknown"
|
|
|
|
|
|
class TemperatureControl(SmartModule):
|
|
"""Implementation of temperature module."""
|
|
|
|
REQUIRED_COMPONENT = "temp_control"
|
|
|
|
def _initialize_features(self):
|
|
"""Initialize features after the initial update."""
|
|
self._add_feature(
|
|
Feature(
|
|
self._device,
|
|
id="target_temperature",
|
|
name="Target temperature",
|
|
container=self,
|
|
attribute_getter="target_temperature",
|
|
attribute_setter="set_target_temperature",
|
|
range_getter="allowed_temperature_range",
|
|
icon="mdi:thermometer",
|
|
type=Feature.Type.Number,
|
|
category=Feature.Category.Primary,
|
|
)
|
|
)
|
|
# TODO: this might belong into its own module, temperature_correction?
|
|
self._add_feature(
|
|
Feature(
|
|
self._device,
|
|
id="temperature_offset",
|
|
name="Temperature offset",
|
|
container=self,
|
|
attribute_getter="temperature_offset",
|
|
attribute_setter="set_temperature_offset",
|
|
minimum_value=-10,
|
|
maximum_value=10,
|
|
type=Feature.Type.Number,
|
|
category=Feature.Category.Config,
|
|
)
|
|
)
|
|
|
|
self._add_feature(
|
|
Feature(
|
|
self._device,
|
|
id="state",
|
|
name="State",
|
|
container=self,
|
|
attribute_getter="state",
|
|
attribute_setter="set_state",
|
|
category=Feature.Category.Primary,
|
|
type=Feature.Type.Switch,
|
|
)
|
|
)
|
|
|
|
self._add_feature(
|
|
Feature(
|
|
self._device,
|
|
id="thermostat_mode",
|
|
name="Thermostat mode",
|
|
container=self,
|
|
attribute_getter="mode",
|
|
category=Feature.Category.Primary,
|
|
type=Feature.Type.Sensor,
|
|
)
|
|
)
|
|
|
|
def query(self) -> dict:
|
|
"""Query to execute during the update cycle."""
|
|
# Target temperature is contained in the main device info response.
|
|
return {}
|
|
|
|
@property
|
|
def state(self) -> bool:
|
|
"""Return thermostat state."""
|
|
return self._device.sys_info["frost_protection_on"] is False
|
|
|
|
async def set_state(self, enabled: bool):
|
|
"""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
|
|
|
|
states = self.states
|
|
|
|
# If the states is empty, the device is idling
|
|
if not states:
|
|
return ThermostatState.Idle
|
|
|
|
# Discard known extra states, and report on unknown extra states
|
|
states.discard("low_battery")
|
|
if len(states) > 1:
|
|
_LOGGER.warning("Got multiple states: %s", states)
|
|
|
|
# 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
|
|
|
|
@property
|
|
def allowed_temperature_range(self) -> tuple[int, int]:
|
|
"""Return allowed temperature range."""
|
|
return self.minimum_target_temperature, self.maximum_target_temperature
|
|
|
|
@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
|
|
def target_temperature(self) -> float:
|
|
"""Return target temperature."""
|
|
return self._device.sys_info["target_temp"]
|
|
|
|
@property
|
|
def states(self) -> set:
|
|
"""Return thermostat states."""
|
|
return set(self._device.sys_info["trv_states"])
|
|
|
|
async def set_target_temperature(self, target: float):
|
|
"""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}]"
|
|
)
|
|
|
|
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)
|
|
|
|
@property
|
|
def temperature_offset(self) -> int:
|
|
"""Return temperature offset."""
|
|
return self._device.sys_info["temp_offset"]
|
|
|
|
async def set_temperature_offset(self, offset: int):
|
|
"""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})
|