Skip to content

Add temperature control module for smart #848

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions kasa/device_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class DeviceType(Enum):
Sensor = "sensor"
Hub = "hub"
Fan = "fan"
Thermostat = "thermostat"
Unknown = "unknown"

@staticmethod
Expand Down
2 changes: 2 additions & 0 deletions kasa/smart/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .lighttransitionmodule import LightTransitionModule
from .reportmodule import ReportModule
from .temperature import TemperatureSensor
from .temperaturecontrol import TemperatureControl
from .timemodule import TimeModule

__all__ = [
Expand All @@ -27,6 +28,7 @@
"BatterySensor",
"HumiditySensor",
"TemperatureSensor",
"TemperatureControl",
"ReportModule",
"AutoOffModule",
"LedModule",
Expand Down
87 changes: 87 additions & 0 deletions kasa/smart/modules/temperaturecontrol.py
Original file line number Diff line number Diff line change
@@ -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(

Check warning on line 21 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L20-L21

Added lines #L20 - L21 were not covered by tests
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(

Check warning on line 32 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L32

Added line #L32 was not covered by tests
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 {}

Check warning on line 47 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L47

Added line #L47 was not covered by tests

@property
def minimum_target_temperature(self) -> int:
"""Minimum available target temperature."""
return self._device.sys_info["min_control_temp"]

Check warning on line 52 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L52

Added line #L52 was not covered by tests

@property
def maximum_target_temperature(self) -> int:
"""Minimum available target temperature."""
return self._device.sys_info["max_control_temp"]

Check warning on line 57 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L57

Added line #L57 was not covered by tests

@property
def target_temperature(self) -> int:
"""Return target temperature."""
return self._device.sys_info["target_temperature"]

Check warning on line 62 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L62

Added line #L62 was not covered by tests

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(

Check warning on line 70 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L70

Added line #L70 was not covered by tests
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})

Check warning on line 75 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L75

Added line #L75 was not covered by tests

@property
def temperature_offset(self) -> int:
"""Return temperature offset."""
return self._device.sys_info["temp_offset"]

Check warning on line 80 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L80

Added line #L80 was not covered by tests

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]")

Check warning on line 85 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L85

Added line #L85 was not covered by tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we know this the allowed range? I couldn't work it out from the fixture

Copy link
Member Author

@rytilahti rytilahti Apr 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a look how it's done here: https://github.com/mihai-dinculescu/tapo/blob/22d8dce597c3dc0ce6e07f89b8434a2d8954eb81/tapo/src/requests/set_device_info/trv.rs#L82

edit: realized we are not linking to that project, so I created #857.


return await self.call("set_device_info", {"temp_offset": offset})

Check warning on line 87 in kasa/smart/modules/temperaturecontrol.py

View check run for this annotation

Codecov / codecov/patch

kasa/smart/modules/temperaturecontrol.py#L87

Added line #L87 was not covered by tests
1 change: 1 addition & 0 deletions kasa/smart/smartchilddevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def device_type(self) -> DeviceType:
"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:
Expand Down
34 changes: 34 additions & 0 deletions kasa/tests/smart/modules/test_temperaturecontrol.py
Original file line number Diff line number Diff line change
@@ -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
Loading