From 72db5c6447cb4dab10e547e1eeee2312b5166a25 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 22 Apr 2024 13:39:07 +0200 Subject: [PATCH] Add temperature control module for smart (#848) --- kasa/device_type.py | 1 + kasa/smart/modules/__init__.py | 2 + kasa/smart/modules/temperaturecontrol.py | 87 +++++++++++++++++++ kasa/smart/smartchilddevice.py | 1 + .../smart/modules/test_temperaturecontrol.py | 34 ++++++++ 5 files changed, 125 insertions(+) create mode 100644 kasa/smart/modules/temperaturecontrol.py create mode 100644 kasa/tests/smart/modules/test_temperaturecontrol.py diff --git a/kasa/device_type.py b/kasa/device_type.py index 6a97867c..3d3b828d 100755 --- a/kasa/device_type.py +++ b/kasa/device_type.py @@ -19,6 +19,7 @@ class DeviceType(Enum): Sensor = "sensor" Hub = "hub" Fan = "fan" + Thermostat = "thermostat" Unknown = "unknown" @staticmethod diff --git a/kasa/smart/modules/__init__.py b/kasa/smart/modules/__init__.py index 938bc2b4..b3b1d9f4 100644 --- a/kasa/smart/modules/__init__.py +++ b/kasa/smart/modules/__init__.py @@ -17,6 +17,7 @@ from .ledmodule import LedModule from .lighttransitionmodule import LightTransitionModule from .reportmodule import ReportModule from .temperature import TemperatureSensor +from .temperaturecontrol import TemperatureControl from .timemodule import TimeModule __all__ = [ @@ -28,6 +29,7 @@ __all__ = [ "BatterySensor", "HumiditySensor", "TemperatureSensor", + "TemperatureControl", "ReportModule", "AutoOffModule", "LedModule", diff --git a/kasa/smart/modules/temperaturecontrol.py b/kasa/smart/modules/temperaturecontrol.py new file mode 100644 index 00000000..8babf116 --- /dev/null +++ b/kasa/smart/modules/temperaturecontrol.py @@ -0,0 +1,87 @@ +"""Implementation of temperature control module.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ...feature import Feature +from ..smartmodule import SmartModule + +if TYPE_CHECKING: + from ..smartdevice import SmartDevice + + +class TemperatureControl(SmartModule): + """Implementation of temperature module.""" + + REQUIRED_COMPONENT = "temperature_control" + + def __init__(self, device: SmartDevice, module: str): + super().__init__(device, module) + self._add_feature( + Feature( + device, + "Target temperature", + container=self, + attribute_getter="target_temperature", + attribute_setter="set_target_temperature", + icon="mdi:thermometer", + ) + ) + # TODO: this might belong into its own module, temperature_correction? + self._add_feature( + Feature( + device, + "Temperature offset", + container=self, + attribute_getter="temperature_offset", + attribute_setter="set_temperature_offset", + minimum_value=-10, + maximum_value=10, + ) + ) + + def query(self) -> dict: + """Query to execute during the update cycle.""" + # Target temperature is contained in the main device info response. + return {} + + @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) -> int: + """Return target temperature.""" + return self._device.sys_info["target_temperature"] + + async def set_target_temperature(self, target: int): + """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}]" + ) + + return await self.call("set_device_info", {"target_temp": target}) + + @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}) diff --git a/kasa/smart/smartchilddevice.py b/kasa/smart/smartchilddevice.py index ecff7cfe..8852262c 100644 --- a/kasa/smart/smartchilddevice.py +++ b/kasa/smart/smartchilddevice.py @@ -52,6 +52,7 @@ class SmartChildDevice(SmartDevice): "subg.trigger.temp-hmdt-sensor": DeviceType.Sensor, "kasa.switch.outlet.sub-fan": DeviceType.Fan, "kasa.switch.outlet.sub-dimmer": DeviceType.Dimmer, + "subg.trv": DeviceType.Thermostat, } dev_type = child_device_map.get(self.sys_info["category"]) if dev_type is None: diff --git a/kasa/tests/smart/modules/test_temperaturecontrol.py b/kasa/tests/smart/modules/test_temperaturecontrol.py new file mode 100644 index 00000000..5768a482 --- /dev/null +++ b/kasa/tests/smart/modules/test_temperaturecontrol.py @@ -0,0 +1,34 @@ +import pytest + +from kasa.smart.modules import TemperatureSensor +from kasa.tests.device_fixtures import parametrize + +temperature = parametrize( + "has temperature control", + component_filter="temperature_control", + protocol_filter={"SMART.CHILD"}, +) + + +@temperature +@pytest.mark.parametrize( + "feature, type", + [ + ("target_temperature", int), + ("temperature_offset", int), + ], +) +async def test_temperature_control_features(dev, feature, type): + """Test that features are registered and work as expected.""" + temp_module: TemperatureSensor = dev.modules["TemperatureControl"] + + prop = getattr(temp_module, feature) + assert isinstance(prop, type) + + feat = temp_module._module_features[feature] + assert feat.value == prop + assert isinstance(feat.value, type) + + await feat.set_value(10) + await dev.update() + assert feat.value == 10