Skip to content
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
2 changes: 1 addition & 1 deletion docs/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,5 @@
True
>>> for feat in dev.features.values():
>>> print(f"{feat.name}: {feat.value}")
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
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
"""
9 changes: 5 additions & 4 deletions kasa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
UnsupportedDeviceError,
)
from kasa.feature import Feature
from kasa.interfaces.light import Light, LightPreset
from kasa.interfaces.light import Light, LightState
from kasa.iotprotocol import (
IotProtocol,
_deprecated_TPLinkSmartHomeProtocol, # noqa: F401
Expand All @@ -52,7 +52,7 @@
"BaseProtocol",
"IotProtocol",
"SmartProtocol",
"LightPreset",
"LightState",
"TurnOnBehaviors",
"TurnOnBehavior",
"DeviceType",
Expand All @@ -75,6 +75,7 @@
]

from . import iot
from .iot.modules.lightpreset import IotLightPreset

deprecated_names = ["TPLinkSmartHomeProtocol"]
deprecated_smart_devices = {
Expand All @@ -84,7 +85,7 @@
"SmartLightStrip": iot.IotLightStrip,
"SmartStrip": iot.IotStrip,
"SmartDimmer": iot.IotDimmer,
"SmartBulbPreset": LightPreset,
"SmartBulbPreset": IotLightPreset,
}
deprecated_exceptions = {
"SmartDeviceException": KasaException,
Expand Down Expand Up @@ -124,7 +125,7 @@ def __getattr__(name):
SmartLightStrip = iot.IotLightStrip
SmartStrip = iot.IotStrip
SmartDimmer = iot.IotDimmer
SmartBulbPreset = LightPreset
SmartBulbPreset = IotLightPreset

SmartDeviceException = KasaException
UnsupportedDeviceException = UnsupportedDeviceError
Expand Down
4 changes: 4 additions & 0 deletions kasa/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ def _get_replacing_attr(self, module_name: ModuleName, *attrs):
"set_color_temp": (Module.Light, ["set_color_temp"]),
"valid_temperature_range": (Module.Light, ["valid_temperature_range"]),
"has_effects": (Module.Light, ["has_effects"]),
"_deprecated_set_light_state": (Module.Light, ["has_effects"]),
# led attributes
"led": (Module.Led, ["led"]),
"set_led": (Module.Led, ["set_led"]),
Expand All @@ -376,6 +377,9 @@ def _get_replacing_attr(self, module_name: ModuleName, *attrs):
"effect_list": (Module.LightEffect, ["_deprecated_effect_list", "effect_list"]),
"set_effect": (Module.LightEffect, ["set_effect"]),
"set_custom_effect": (Module.LightEffect, ["set_custom_effect"]),
# light preset attributes
"presets": (Module.LightPreset, ["_deprecated_presets", "preset_states_list"]),
"save_preset": (Module.LightPreset, ["_deprecated_save_preset"]),
}

def __getattr__(self, name):
Expand Down
4 changes: 3 additions & 1 deletion kasa/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

from .fan import Fan
from .led import Led
from .light import Light, LightPreset
from .light import Light, LightState
from .lighteffect import LightEffect
from .lightpreset import LightPreset

__all__ = [
"Fan",
"Led",
"Light",
"LightEffect",
"LightState",
"LightPreset",
]
38 changes: 18 additions & 20 deletions kasa/interfaces/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import NamedTuple, Optional

from pydantic.v1 import BaseModel
from dataclasses import dataclass
from typing import NamedTuple

from ..module import Module


@dataclass
class LightState:
"""Class for smart light preset info."""

light_on: bool | None = None
brightness: int | None = None
hue: int | None = None
saturation: int | None = None
color_temp: int | None = None
transition: bool | None = None


class ColorTempRange(NamedTuple):
"""Color temperature range."""

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


class LightPreset(BaseModel):
"""Light configuration preset."""

index: int
brightness: int

# These are not available for effect mode presets on light strips
hue: Optional[int] # noqa: UP007
saturation: Optional[int] # noqa: UP007
color_temp: Optional[int] # noqa: UP007

# Variables for effect mode presets
custom: Optional[int] # noqa: UP007
id: Optional[str] # noqa: UP007
mode: Optional[int] # noqa: UP007


class Light(Module, ABC):
"""Base class for TP-Link Light."""

Expand Down Expand Up @@ -133,3 +127,7 @@ async def set_brightness(
:param int brightness: brightness in percent
:param int transition: transition in milliseconds.
"""

@abstractmethod
async def set_state(self, state: LightState) -> dict:
"""Set the light state."""
76 changes: 76 additions & 0 deletions kasa/interfaces/lightpreset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Module for LightPreset base class."""

from __future__ import annotations

from abc import abstractmethod
from typing import Sequence

from ..feature import Feature
from ..module import Module
from .light import LightState


class LightPreset(Module):
"""Base interface for light preset module."""

PRESET_NOT_SET = "Not set"

def _initialize_features(self):
"""Initialize features."""
device = self._device
self._add_feature(
Feature(
device,
id="light_preset",
name="Light preset",
container=self,
attribute_getter="preset",
attribute_setter="set_preset",
category=Feature.Category.Config,
type=Feature.Type.Choice,
choices_getter="preset_list",
)
)

@property
@abstractmethod
def preset_list(self) -> list[str]:
"""Return list of preset names.

Example:
['Off', 'Preset 1', 'Preset 2', ...]
"""

@property
@abstractmethod
def preset_states_list(self) -> Sequence[LightState]:
"""Return list of preset states.

Example:
['Off', 'Preset 1', 'Preset 2', ...]
"""

@property
@abstractmethod
def preset(self) -> str:
"""Return current preset name."""

@abstractmethod
async def set_preset(
self,
preset_name: str,
) -> None:
"""Set a light preset for the device."""

@abstractmethod
async def save_preset(
self,
preset_name: str,
preset_info: LightState,
) -> None:
"""Update the preset with *preset_name* with the new *preset_info*."""

@property
@abstractmethod
def has_save_preset(self) -> bool:
"""Return True if the device supports updating presets."""
42 changes: 11 additions & 31 deletions kasa/iot/iotbulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig
from ..interfaces.light import HSV, ColorTempRange, LightPreset
from ..interfaces.light import HSV, ColorTempRange
from ..module import Module
from ..protocol import BaseProtocol
from .iotdevice import IotDevice, KasaException, requires_update
Expand All @@ -21,6 +21,7 @@
Countdown,
Emeter,
Light,
LightPreset,
Schedule,
Time,
Usage,
Expand Down Expand Up @@ -178,7 +179,7 @@ class IotBulb(IotDevice):
Bulb configuration presets can be accessed using the :func:`presets` property:

>>> bulb.presets
[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)]
[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)]

To modify an existing preset, pass :class:`~kasa.interfaces.light.LightPreset`
instance to :func:`save_preset` method:
Expand Down Expand Up @@ -222,7 +223,8 @@ async def _initialize_modules(self):
self.add_module(Module.IotEmeter, Emeter(self, self.emeter_type))
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
self.add_module(Module.IotCloud, Cloud(self, "smartlife.iot.common.cloud"))
self.add_module(Module.Light, Light(self, "light"))
self.add_module(Module.Light, Light(self, self.LIGHT_SERVICE))
self.add_module(Module.LightPreset, LightPreset(self, self.LIGHT_SERVICE))

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

async def set_light_state(
async def _set_light_state(
self, state: dict, *, transition: int | None = None
) -> dict:
"""Set the light state."""
Expand Down Expand Up @@ -400,7 +402,7 @@ async def _set_hsv(
self._raise_for_invalid_brightness(value)
light_state["brightness"] = value

return await self.set_light_state(light_state, transition=transition)
return await self._set_light_state(light_state, transition=transition)

@property # type: ignore
@requires_update
Expand Down Expand Up @@ -436,7 +438,7 @@ async def _set_color_temp(
if brightness is not None:
light_state["brightness"] = brightness

return await self.set_light_state(light_state, transition=transition)
return await self._set_light_state(light_state, transition=transition)

def _raise_for_invalid_brightness(self, value):
if not isinstance(value, int) or not (0 <= value <= 100):
Expand Down Expand Up @@ -467,7 +469,7 @@ async def _set_brightness(
self._raise_for_invalid_brightness(brightness)

light_state = {"brightness": brightness}
return await self.set_light_state(light_state, transition=transition)
return await self._set_light_state(light_state, transition=transition)

@property # type: ignore
@requires_update
Expand All @@ -481,14 +483,14 @@ async def turn_off(self, *, transition: int | None = None, **kwargs) -> dict:

:param int transition: transition in milliseconds.
"""
return await self.set_light_state({"on_off": 0}, transition=transition)
return await self._set_light_state({"on_off": 0}, transition=transition)

async def turn_on(self, *, transition: int | None = None, **kwargs) -> dict:
"""Turn the bulb on.

:param int transition: transition in milliseconds.
"""
return await self.set_light_state({"on_off": 1}, transition=transition)
return await self._set_light_state({"on_off": 1}, transition=transition)

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

@property # type: ignore
@requires_update
def presets(self) -> list[LightPreset]:
"""Return a list of available bulb setting presets."""
return [LightPreset(**vals) for vals in self.sys_info["preferred_state"]]

async def save_preset(self, preset: LightPreset):
"""Save a setting preset.

You can either construct a preset object manually, or pass an existing one
obtained using :func:`presets`.
"""
if len(self.presets) == 0:
raise KasaException("Device does not supported saving presets")

if preset.index >= len(self.presets):
raise KasaException("Invalid preset index")

return await self._query_helper(
self.LIGHT_SERVICE, "set_preferred_state", preset.dict(exclude_none=True)
)

@property
def max_device_response_size(self) -> int:
"""Returns the maximum response size the device can safely construct."""
Expand Down
6 changes: 4 additions & 2 deletions kasa/iot/iotdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,13 @@ async def update(self, update_children: bool = True):

await self._modular_update(req)

self._set_sys_info(self._last_update["system"]["get_sysinfo"])
for module in self._modules.values():
module._post_update_hook()

if not self._features:
await self._initialize_features()

self._set_sys_info(self._last_update["system"]["get_sysinfo"])

async def _initialize_modules(self):
"""Initialize modules not added in init."""

Expand Down
3 changes: 3 additions & 0 deletions kasa/iot/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .led import Led
from .light import Light
from .lighteffect import LightEffect
from .lightpreset import IotLightPreset, LightPreset
from .motion import Motion
from .rulemodule import Rule, RuleModule
from .schedule import Schedule
Expand All @@ -23,6 +24,8 @@
"Led",
"Light",
"LightEffect",
"LightPreset",
"IotLightPreset",
"Motion",
"Rule",
"RuleModule",
Expand Down
Loading