8000 Add light presets common module to devices. (#907) · python-kasa/python-kasa@273c541 · GitHub
[go: up one dir, main page]

Skip to content

Commit 273c541

Browse files
authored
Add light presets common module to devices. (#907)
Adds light preset common module for switching to presets and saving presets. Deprecates the `presets` attribute and `save_preset` method from the `bulb` interface in favour of the modular approach. Allows setting preset for `iot` which was not previously supported.
1 parent 1ba5c73 commit 273c541

20 files changed

+612
-73
lines changed

docs/tutorial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,5 @@
9999
True
100100
>>> for feat in dev.features.values():
101101
>>> print(f"{feat.name}: {feat.value}")
102-
Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nOverheated: False\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: False\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: 1.1.6 Build 240130 Rel.173828\nLight effect: Party\nSmooth transition on: 2\nSmooth transition off: 2\nTime: 2024-02-23 02:40:15+01:00
102+
Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nOverheated: False\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: False\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: 1.1.6 Build 240130 Rel.173828\nLight effect: Party\nLight preset: Light preset 1\nSmooth transition on: 2\nSmooth transition off: 2\nTime: 2024-02-23 02:40:15+01:00
103103
"""

kasa/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
UnsupportedDeviceError,
3636
)
3737
from kasa.feature import Feature
38-
from kasa.interfaces.light import Light, LightPreset
38+
from kasa.interfaces.light import Light, LightState
3939
from kasa.iotprotocol import (
4040
IotProtocol,
4141
_deprecated_TPLinkSmartHomeProtocol, # noqa: F401
@@ -52,7 +52,7 @@
5252
"BaseProtocol",
5353
"IotProtocol",
5454
"SmartProtocol",
55-
"LightPreset",
55+
"LightState",
5656
"TurnOnBehaviors",
5757
"TurnOnBehavior",
5858
"DeviceType",
@@ -75,6 +75,7 @@
7575
]
7676

7777
from . import iot
78+
from .iot.modules.lightpreset import IotLightPreset
7879

7980
deprecated_names = ["TPLinkSmartHomeProtocol"]
8081
deprecated_smart_devices = {
@@ -84,7 +85,7 @@
8485
"SmartLightStrip": iot.IotLightStrip,
8586
"SmartStrip": iot.IotStrip,
8687
"SmartDimmer": iot.IotDimmer,
87-
"SmartBulbPreset": LightPreset,
88+
"SmartBulbPreset": IotLightPreset,
8889
}
8990
deprecated_exceptions = {
9091
"SmartDeviceException": KasaException,
@@ -124,7 +125,7 @@ def __getattr__(name):
124125
SmartLightStrip = iot.IotLightStrip
125126
SmartStrip = iot.IotStrip
126127
SmartDimmer = iot.IotDimmer
127-
SmartBulbPreset = LightPreset
128+
SmartBulbPreset = IotLightPreset
128129

129130
SmartDeviceException = KasaException
130131
UnsupportedDeviceException = UnsupportedDeviceError

kasa/device.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ def _get_replacing_attr(self, module_name: ModuleName, *attrs):
364364
"set_color_temp": (Module.Light, ["set_color_temp"]),
365365
"valid_temperature_range": (Module.Light, ["valid_temperature_range"]),
366366
"has_effects": (Module.Light, ["has_effects"]),
367+
"_deprecated_set_light_state": (Module.Light, ["has_effects"]),
367368
# led attributes
368369
"led": (Module.Led, ["led"]),
369370
"set_led": (Module.Led, ["set_led"]),
@@ -376,6 +377,9 @@ def _get_replacing_attr(self, module_name: ModuleName, *attrs):
376377
"effect_list": (Module.LightEffect, ["_deprecated_effect_list", "effect_list"]),
377378
"set_effect": (Module.LightEffect, ["set_effect"]),
378379
"set_custom_effect": (Module.LightEffect, ["set_custom_effect"]),
380+
# light preset attributes
381+
"presets": (Module.LightPreset, ["_deprecated_presets", "preset_states_list"]),
382+
"save_preset": (Module.LightPreset, ["_deprecated_save_preset"]),
379383
}
380384

381385
def __getattr__(self, name):

kasa/interfaces/__init__.py

Lines changed: 3 additions & 1 deletion
< 10000 td data-grid-cell-id="diff-e2518b6146e8c26f93803fcd71fec7a67b2b657bc7e034350da1e669d4401f91-14-16-2" data-line-anchor="diff-e2518b6146e8c26f93803fcd71fec7a67b2b657bc7e034350da1e669d4401f91R16" data-selected="false" role="gridcell" style="background-color:var(--bgColor-default);padding-right:24px" tabindex="-1" valign="top" class="focusable-grid-cell diff-text-cell right-side-diff-cell left-side">
]
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
from .fan import Fan
44
from .led import Led
5-
from .light import Light, LightPreset
5+
from .light import Light, LightState
66
from .lighteffect import LightEffect
7+
from .lightpreset import LightPreset
78

89
__all__ = [
910
"Fan",
1011
"Led",
1112
"Light",
1213
"LightEffect",
14+
"LightState",
1315
"LightPreset",
1416

kasa/interfaces/light.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@
33
from __future__ import annotations
44

55
from abc import ABC, abstractmethod
6-
from typing import NamedTuple, Optional
7-
8-
from pydantic.v1 import BaseModel
6+
from dataclasses import dataclass
7+
from typing import NamedTuple
98

109
from ..module import Module
1110

1211

12+
@dataclass
13+
class LightState:
14+
"""Class for smart light preset info."""
15+
16+
light_on: bool | None = None
17+
brightness: int | None = None
18+
hue: int | None = None
19+
saturation: int | None = None
20+
color_temp: int | None = None
21+
transition: bool | None = None
22+
23+
1324
class ColorTempRange(NamedTuple):
1425
"""Color temperature range."""
1526

@@ -25,23 +36,6 @@ class HSV(NamedTuple):
2536
value: int
2637

2738

28-
class LightPreset(BaseModel):
29-
"""Light configuration preset."""
30-
31-
index: int
32-
brightness: int
33-
34-
# These are not available for effect mode presets on light strips
35-
hue: Optional[int] # noqa: UP007
36-
saturation: Optional[int] # noqa: UP007
37-
color_temp: Optional[int] # noqa: UP007
38-
39-
# Variables for effect mode presets
40-
custom: Optional[int] # noqa: UP007
41-
id: Optional[str] # noqa: UP007
42-
mode: Optional[int] # noqa: UP007
43-
44-
4539
class Light(Module, ABC):
4640
"""Base class for TP-Link Light."""
4741

@@ -133,3 +127,7 @@ async def set_brightness(
133127
:param int brightness: brightness in percent
134128
:param int transition: transition in milliseconds.
135129
"""
130+
131+
@abstractmethod
132+
async def set_state(self, state: LightState) -> dict:
133+
"""Set the light state."""

kasa/interfaces/lightpreset.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Module for LightPreset base class."""
2+
3+
from __future__ import annotations
4+
5+
from abc import abstractmethod
6+
from typing import Sequence
7+
8+
from ..feature import Feature
9+
from ..module import Module
10+
from .light import LightState
11+
12+
13+
class LightPreset(Module):
14+
"""Base interface for light preset module."""
15+
16+
PRESET_NOT_SET = "Not set"
17+
18+
def _initialize_features(self):
19+
"""Initialize features."""
20+
device = self._device
21+
self._add_feature(
22+
Feature(
23+
device,
24+
id="light_preset",
25+
name="Light preset",
26+
container=self,
27+
attribute_getter="preset",
28+
attribute_setter="set_preset",
29+
category=Feature.Category.Config,
30+
type=Feature.Type.Choice,
31+
choices_getter="preset_list",
32+
)
33+
)
34+
35+
@property
36+
@abstractmethod
37+
def preset_list(self) -> list[str]:
38+
"""Return list of preset names.
39+
40+
Example:
41+
['Off', 'Preset 1', 'Preset 2', ...]
42+
"""
43+
44+
@property
45+
@abstractmethod
46+
def preset_states_list(self) -> Sequence[LightState]:
47+
"""Return list of preset states.
48+
49+
Example:
50+
['Off', 'Preset 1', 'Preset 2', ...]
51+
"""
52+
53+
@property
54+
@abstractmethod
55+
def preset(self) -> str:
56+
"""Return current preset name."""
57+
58+
@abstractmethod
59+
async def set_preset(
60+
self,
61+
preset_name: str,
62+
) -> None:
63+
"""Set a light preset for the device."""
64+
65+
@abstractmethod
66+
async def save_preset(
67+
self,
68+
preset_name: str,
69+
preset_info: LightState,
70+
) -> None:
71+
"""Update the preset with *preset_name* with the new *preset_info*."""
72+
73+
@property
74+
@abstractmethod
75+
def has_save_preset(self) -> bool:
76+
"""Return True if the device supports updating presets."""

kasa/iot/iotbulb.py

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from ..device_type import DeviceType
1313
from ..deviceconfig import DeviceConfig
14-
from ..interfaces.light import HSV, ColorTempRange, LightPreset
14+
from ..interfaces.light import HSV, ColorTempRange
1515
from ..module import Module
1616
from ..protocol import BaseProtocol
1717
from .iotdevice import IotDevice, KasaException, requires_update
@@ -21,6 +21,7 @@
2121
Countdown,
2222
Emeter,
2323
Light,
24+
LightPreset,
2425
Schedule,
2526
Time,
2627
Usage,
@@ -178,7 +179,7 @@ class IotBulb(IotDevice):
178179
Bulb configuration presets can be accessed using the :func:`presets` property:
179180
180181
>>> bulb.presets
181-
[LightPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), LightPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), LightPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), LightPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
182+
[IotLightPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), IotLightPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), IotLightPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), IotLightPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
182183
183184
To modify an existing preset, pass :class:`~kasa.interfaces.light.LightPreset`
184185
instance to :func:`save_preset` method:
@@ -222,7 +223,8 @@ async def _initialize_modules(self):
222223
self.add_module(Module.IotEmeter, Emeter(self, self.emeter_type))
223224
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
224225
self.add_module(Module.IotCloud, Cloud(self, "smartlife.iot.common.cloud"))
225-
self.add_module(Module.Light, Light(self, "light"))
226+
self.add_module(Module.Light, Light(self, self.LIGHT_SERVICE))
227+
self.add_module(Module.LightPreset, LightPreset(self, self.LIGHT_SERVICE))
226228

227229
@property # type: ignore
228230
@requires_update
@@ -320,7 +322,7 @@ async def get_light_state(self) -> dict[str, dict]:
320322
# TODO: add warning and refer to use light.state?
321323
return await self._query_helper(self.LIGHT_SERVICE, "get_light_state")
322324

323-
async def set_light_state(
325+
async def _set_light_state(
324326
self, state: dict, *, transition: int | None = None
325327
) -> dict:
326328
"""Set the light state."""
@@ -400,7 +402,7 @@ async def _set_hsv(
400402
self._raise_for_invalid_brightness(value)
401403
light_state["brightness"] = value
402404

403-
return await self.set_light_state(light_state, transition=transition)
405+
return await self._set_light_state(light_state, transition=transition)
404406

405407
@property # type: ignore
406408
@requires_update
@@ -436,7 +438,7 @@ async def _set_color_temp(
436438
if brightness is not None:
437439
light_state["brightness"] = brightness
438440

439-
return await self.set_light_state(light_state, transition=transition)
441+
return await self._set_light_state(light_state, transition=transition)
440442

441443
def _raise_for_invalid_brightness(self, value):
442444
if not isinstance(value, int) or not (0 <= value <= 100):
@@ -467,7 +469,7 @@ async def _set_brightness(
467469
self._raise_for_invalid_brightness(brightness)
468470

469471
light_state = {"brightness": brightness}
470-
return await self.set_light_state(light_state, transition=transition)
472+
return await self._set_light_state(light_state, transition=transition)
471473

472474
@property # type: ignore
473475
@requires_update
@@ -481,14 +483,14 @@ async def turn_off(self, *, transition: int | None = None, **kwargs) -> dict:
481483
482484
:param int transition: transition in milliseconds.
483485
"""
484-
return await self.set_light_state({"on_off": 0}, transition=transition)
486+
return await self._set_light_state({"on_off": 0}, transition=transition)
485487

486488
async def turn_on(self, *, transition: int | None = None, **kwargs) -> dict:
487489
"""Turn the bulb on.
488490
489491
:param int transition: transition in milliseconds.
490492
"""
491-
return await self.set_light_state({"on_off": 1}, transition=transition)
493+
return await self._set_light_state({"on_off": 1}, transition=transition)
492494

493495
@property # type: ignore
494496
@requires_update
@@ -505,28 +507,6 @@ async def set_alias(self, alias: str) -> None:
505507
"smartlife.iot.common.system", "set_dev_alias", {"alias": alias}
506508
)
507509

508-
@property # type: ignore
509-
@requires_update
510-
def presets(self) -> list[LightPreset]:
511-
"""Return a list of available bulb setting presets."""
512-
return [LightPreset(**vals) for vals in self.sys_info["preferred_state"]]
513-
514-
async def save_preset(self, preset: LightPreset):
515-
"""Save a setting preset.
516-
517-
You can either construct a preset object manually, or pass an existing one
518-
obtained using :func:`presets`.
519-
"""
520-
if len(self.presets) == 0:
521-
raise KasaException("Device does not supported saving presets")
522-
523-
if preset.index >= len(self.presets):
524-
raise KasaException("Invalid preset index")
525-
526-
return await self._query_helper(
527-
self.LIGHT_SERVICE, "set_preferred_state", preset.dict(exclude_none=True)
528-
)
529-
530510
@property
531511
def max_device_response_size(self) -> int:
532512
"""Returns the maximum response size the device can safely construct."""

kasa/iot/iotdevice.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,13 @@ async def update(self, update_children: bool = True):
312312

313313
await self._modular_update(req)
314314

315+
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
316+
for module in self._modules.values():
317+
module._post_update_hook()
318+
315319
if not self._features:
316320
await self._initialize_features()
317321

318-
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
319-
320322
async def _initialize_modules(self):
321323
"""Initialize modules not added in init."""
322324

kasa/iot/modules/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .led import Led
99
from .light import Light
1010
from .lighteffect import LightEffect
11+
from .lightpreset import IotLightPreset, LightPreset
1112
from .motion import Motion
1213
from .rulemodule import Rule, RuleModule
1314
from .schedule import Schedule
@@ -23,6 +24,8 @@
2324
"Led",
2425
"Light",
2526
"LightEffect",
27+
"LightPreset",
28+
"IotLightPreset",
2629
"Motion",
2730
"Rule",
2831
"RuleModule",

0 commit comments

Comments
 (0)
0