FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Create common interfaces for remaining device types (#895) · python-kasa/python-kasa@9473d97 · GitHub

Repository navigation

Commit 9473d97

Browse files
authored
Create common interfaces for remaining device types (#895)
Introduce common module interfaces across smart and iot devices and provide better typing implementation for getting modules to support this.
1 parent 7d4dc4c commit 9473d97

33 files changed

Lines changed: 673 additions & 220 deletions

‎.pre-commit-config.yaml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ repos:
2121
hooks:
2222
- id: mypy
2323
additional_dependencies: [types-click]
24+
exclude: |
25+
(?x)^(
26+
kasa/modulemapping\.py|
27+
)$
28+
2429
2530
- repo: https://github.com/PyCQA/doc8
2631
rev: 'v1.1.1'

‎devtools/create_module_fixtures.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
def create_fixtures(dev: IotDevice, outputdir: Path):
2020
"""Iterate over supported modules and create version-specific fixture files."""
2121
for name, module in dev.modules.items():
22-
module_dir = outputdir / name
22+
module_dir = outputdir / str(name)
2323
if not module_dir.exists():
2424
module_dir.mkdir(exist_ok=True, parents=True)
2525

‎kasa/__init__.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from typing import TYPE_CHECKING
1717
from warnings import warn
1818

19-
from kasa.bulb import Bulb
19+
from kasa.bulb import Bulb, BulbPreset
2020
from kasa.credentials import Credentials
2121
from kasa.device import Device
2222
from kasa.device_type import DeviceType
@@ -36,12 +36,11 @@
3636
UnsupportedDeviceError,
3737
)
3838
from kasa.feature import Feature
39-
from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors
4039
from kasa.iotprotocol import (
4140
IotProtocol,
4241
_deprecated_TPLinkSmartHomeProtocol, # noqa: F401
4342
)
44-
from kasa.plug import Plug
43+
from kasa.module import Module
4544
from kasa.protocol import BaseProtocol
4645
from kasa.smartprotocol import SmartProtocol
4746

@@ -62,6 +61,7 @@
6261
"Device",
6362
"Bulb",
6463
"Plug",
64+
"Module",
6565
"KasaException",
6666
"AuthenticationError",
6767
"DeviceError",

‎kasa/bulb.py‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,6 @@ def _raise_for_invalid_brightness(self, value):
5454
def is_color(self) -> bool:
5555
"""Whether the bulb supports color changes."""
5656

57-
@property
58-
@abstractmethod
59-
def is_dimmable(self) -> bool:
60-
"""Whether the bulb supports brightness changes."""
61-
6257
@property
6358
@abstractmethod
6459
def is_variable_color_temp(self) -> bool:

‎kasa/device.py‎

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from abc import ABC, abstractmethod
77
from dataclasses import dataclass
88
from datetime import datetime
9-
from typing import Any, Mapping, Sequence, overload
9+
from typing import TYPE_CHECKING, Any, Mapping, Sequence
1010

1111
from .credentials import Credentials
1212
from .device_type import DeviceType
@@ -15,10 +15,13 @@
1515
from .exceptions import KasaException
1616
from .feature import Feature
1717
from .iotprotocol import IotProtocol
18-
from .module import Module, ModuleT
18+
from .module import Module
1919
from .protocol import BaseProtocol
2020
from .xortransport import XorTransport
2121

22+
if TYPE_CHECKING:
23+
from .modulemapping import ModuleMapping
24+
2225

2326
@dataclass
2427
class WifiNetwork:
@@ -113,21 +116,9 @@ async def disconnect(self):
113116

114117
@property
115118
@abstractmethod
116-
def modules(self) -> Mapping[str, Module]:
119+
def modules(self) -> ModuleMapping[Module]:
117120
"""Return the device modules."""
118121

119-
@overload
120-
@abstractmethod
121-
def get_module(self, module_type: type[ModuleT]) -> ModuleT | None: ...
122-
123-
@overload
124-
@abstractmethod
125-
def get_module(self, module_type: str) -> Module | None: ...
126-
127-
@abstractmethod
128-
def get_module(self, module_type: type[ModuleT] | str) -> ModuleT | Module | None:
129-
"""Return the module from the device modules or None if not present."""
130-
131122
@property
132123
@abstractmethod
133124
def is_on(self) -> bool:

‎kasa/interfaces/led.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Module for base light effect module."""
2+
3+
from __future__ import annotations
4+
5+
from abc import ABC, abstractmethod
6+
7+
from ..feature import Feature
8+
from ..module import Module
9+
10+
11+
class Led(Module, ABC):
12+
"""Base interface to represent a LED module."""
13+
14+
def _initialize_features(self):
15+
"""Initialize features."""
16+
device = self._device
17+
self._add_feature(
18+
Feature(
19+
device=device,
20+
container=self,
21+
name="LED",
22+
id="led",
23+
icon="mdi:led",
24+
attribute_getter="led",
25+
attribute_setter="set_led",
26+
type=Feature.Type.Switch,
27+
category=Feature.Category.Config,
28+
)
29+
)
30+
31+
@property
32+
@abstractmethod
33+
def led(self) -> bool:
34+
"""Return current led status."""
35+
36+
@abstractmethod
37+
async def set_led(self, enable: bool) -> None:
38+
"""Set led."""

‎kasa/interfaces/lighteffect.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Module for base light effect module."""
2+
3+
from __future__ import annotations
4+
5+
from abc import ABC, abstractmethod
6+
7+
from ..feature import Feature
8+
from ..module import Module
9+
10+
11+
class LightEffect(Module, ABC):
12+
"""Interface to represent a light effect module."""
13+
14+
LIGHT_EFFECTS_OFF = "Off"
15+
16+
def _initialize_features(self):
17+
"""Initialize features."""
18+
device = self._device
19+
self._add_feature(
20+
Feature(
21+
device,
22+
id="light_effect",
23+
name="Light effect",
24+
container=self,
25+
attribute_getter="effect",
26+
attribute_setter="set_effect",
27+
category=Feature.Category.Primary,
28+
type=Feature.Type.Choice,
29+
choices_getter="effect_list",
30+
)
31+
)
32+
33+
@property
34+
@abstractmethod
35+
def has_custom_effects(self) -> bool:
36+
"""Return True if the device supports setting custom effects."""
37+
38+
@property
39+
@abstractmethod
40+
def effect(self) -> str:
41+
"""Return effect state or name."""
42+
43+
@property
44+
@abstractmethod
45+
def effect_list(self) -> list[str]:
46+
"""Return built-in effects list.
47+
48+
Example:
49+
['Aurora', 'Bubbling Cauldron', ...]
50+
"""
51+
52+
@abstractmethod
53+
async def set_effect(
54+
self,
55+
effect: str,
56+
*,
57+
brightness: int | None = None,
58+
transition: int | None = None,
59+
) -> None:
60+
"""Set an effect on the device.
61+
62+
If brightness or transition is defined,
63+
its value will be used instead of the effect-specific default.
64+
65+
See :meth:`effect_list` for available effects,
66+
or use :meth:`set_custom_effect` for custom effects.
67+
68+
:param str effect: The effect to set
69+
:param int brightness: The wanted brightness
70+
:param int transition: The wanted transition time
71+
"""
72+
73+
async def set_custom_effect(
74+
self,
75+
effect_dict: dict,
76+
) -> None:
77+
"""Set a custom effect on the device.
78+
79+
:param str effect_dict: The custom effect dict to set
80+
"""
File renamed without changes.

‎kasa/iot/iotdevice.py‎

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919
import inspect
2020
import logging
2121
from datetime import datetime, timedelta
22-
from typing import Any, Mapping, Sequence, cast, overload
22+
from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast
2323

2424
from ..device import Device, WifiNetwork
2525
from ..deviceconfig import DeviceConfig
2626
from ..emeterstatus import EmeterStatus
2727
from ..exceptions import KasaException
2828
from ..feature import Feature
29-
from ..module import ModuleT
29+
from ..module import Module
30+
from ..modulemapping import ModuleMapping, ModuleName
3031
from ..protocol import BaseProtocol
3132
from .iotmodule import IotModule
3233
from .modules import Emeter, Time
@@ -190,46 +191,28 @@ def __init__(
190191
self._supported_modules: dict[str, IotModule] | None = None
191192
self._legacy_features: set[str] = set()
192193
self._children: Mapping[str, IotDevice] = {}
193-
self._modules: dict[str, IotModule] = {}
194+
self._modules: dict[str | ModuleName[Module], IotModule] = {}
194195

195196
@property
196197
def children(self) -> Sequence[IotDevice]:
197198
"""Return list of children."""
198199
return list(self._children.values())
199200

200201
@property
201-
def modules(self) -> dict[str, IotModule]:
202+
def modules(self) -> ModuleMapping[IotModule]:
202203
"""Return the device modules."""
204+
if TYPE_CHECKING:
205+
return cast(ModuleMapping[IotModule], self._modules)
203206
return self._modules
204207

205-
@overload
206-
def get_module(self, module_type: type[ModuleT]) -> ModuleT | None: ...
207-
208-
@overload
209-
def get_module(self, module_type: str) -> IotModule | None: ...
210-
211-
def get_module(
212-
self, module_type: type[ModuleT] | str
213-
) -> ModuleT | IotModule | None:
214-
"""Return the module from the device modules or None if not present."""
215-
if isinstance(module_type, str):
216-
module_name = module_type.lower()
217-
elif issubclass(module_type, IotModule):
218-
module_name = module_type.__name__.lower()
219-
else:
220-
return None
221-
if module_name in self.modules:
222-
return self.modules[module_name]
223-
return None
224-
225-
def add_module(self, name: str, module: IotModule):
208+
def add_module(self, name: str | ModuleName[Module], module: IotModule):
226209
"""Register a module."""
227210
if name in self.modules:
228211
_LOGGER.debug("Module %s already registered, ignoring..." % name)
229212
return
230213

231214
_LOGGER.debug("Adding module %s", module)
232-
self.modules[name] = module
215+
self._modules[name] = module
233216

234217
def _create_request(
235218
self, target: str, cmd: str, arg: dict | None = None, child_ids=None
@@ -291,11 +274,11 @@ def features(self) -> dict[str, Feature]:
291274

292275
@property # type: ignore
293276
@requires_update
294-
def supported_modules(self) -> list[str]:
277+
def supported_modules(self) -> list[str | ModuleName[Module]]:
295278
"""Return a set of modules supported by the device."""
296279
# TODO: this should rather be called `features`, but we don't want to break
297280
# the API now. Maybe just deprecate it and point the users to use this?
298-
return list(self.modules.keys())
281+
return list(self._modules.keys())
299282

300283
@property # type: ignore
301284
@requires_update
@@ -324,10 +307,11 @@ async def update(self, update_children: bool = True):
324307
self._last_update = response
325308
self._set_sys_info(response["system"]["get_sysinfo"])
326309

310+
await self._modular_update(req)
311+
327312
if not self._features:
328313
await self._initialize_features()
329314

330-
await self._modular_update(req)
331315
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
332316

333317
async def _initialize_features(self):
@@ -352,6 +336,11 @@ async def _initialize_features(self):
352336
)
353337
)
354338

339+
for module in self._modules.values():
340+
module._initialize_features()
341+
for module_feat in module._module_features.values():
342+
self._add_feature(module_feat)
343+
355344
async def _modular_update(self, req: dict) -> None:
356345
"""Execute an update query."""
357346
if self.has_emeter:
@@ -364,17 +353,15 @@ async def _modular_update(self, req: dict) -> None:
364353
# making separate handling for this unnecessary
365354
if self._supported_modules is None:
366355
supported = {}
367-
for module in self.modules.values():
356+
for module in self._modules.values():
368357
if module.is_supported:
369358
supported[module._module] = module
370-
for module_feat in module._module_features.values():
371-
self._add_feature(module_feat)
372359

373360
self._supported_modules = supported
374361

375362
request_list = []
376363
est_response_size = 1024 if "system" in req else 0
377-
for module in self.modules.values():
364+
for module in self._modules.values():
378365
if not module.is_supported:
379366
_LOGGER.debug("Module %s not supported, skipping" % module)
380367
continue

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL