| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -21,6 +21,11 @@ repos: | |||
| 21 | 21 | hooks: | |
| 22 | 22 | - id: mypy | |
| 23 | 23 | additional_dependencies: [types-click] | |
| 24 | + exclude: | | ||
| 25 | + (?x)^( | ||
| 26 | + kasa/modulemapping\.py| | ||
| 27 | + )$ | ||
| 28 | + | ||
| 24 | 29 | ||
| 25 | 30 | - repo: https://github.com/PyCQA/doc8 | |
| 26 | 31 | rev: 'v1.1.1' | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,7 +19,7 @@ | |||
| 19 | 19 | def create_fixtures(dev: IotDevice, outputdir: Path): | |
| 20 | 20 | """Iterate over supported modules and create version-specific fixture files.""" | |
| 21 | 21 | for name, module in dev.modules.items(): | |
| 22 | - module_dir = outputdir / name | ||
| 22 | + module_dir = outputdir / str(name) | ||
| 23 | 23 | if not module_dir.exists(): | |
| 24 | 24 | module_dir.mkdir(exist_ok=True, parents=True) | |
| 25 | 25 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -16,7 +16,7 @@ | |||
| 16 | 16 | from typing import TYPE_CHECKING | |
| 17 | 17 | from warnings import warn | |
| 18 | 18 | ||
| 19 | - from kasa.bulb import Bulb | ||
| 19 | + from kasa.bulb import Bulb, BulbPreset | ||
| 20 | 20 | from kasa.credentials import Credentials | |
| 21 | 21 | from kasa.device import Device | |
| 22 | 22 | from kasa.device_type import DeviceType | |
@@ -36,12 +36,11 @@ | |||
| 36 | 36 | UnsupportedDeviceError, | |
| 37 | 37 | ) | |
| 38 | 38 | from kasa.feature import Feature | |
| 39 | - from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors | ||
| 40 | 39 | from kasa.iotprotocol import ( | |
| 41 | 40 | IotProtocol, | |
| 42 | 41 | _deprecated_TPLinkSmartHomeProtocol, # noqa: F401 | |
| 43 | 42 | ) | |
| 44 | - from kasa.plug import Plug | ||
| 43 | + from kasa.module import Module | ||
| 45 | 44 | from kasa.protocol import BaseProtocol | |
| 46 | 45 | from kasa.smartprotocol import SmartProtocol | |
| 47 | 46 | ||
@@ -62,6 +61,7 @@ | |||
| 62 | 61 | "Device", | |
| 63 | 62 | "Bulb", | |
| 64 | 63 | "Plug", | |
| 64 | + "Module", | ||
| 65 | 65 | "KasaException", | |
| 66 | 66 | "AuthenticationError", | |
| 67 | 67 | "DeviceError", | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -54,11 +54,6 @@ def _raise_for_invalid_brightness(self, value): | |||
| 54 | 54 | def is_color(self) -> bool: | |
| 55 | 55 | """Whether the bulb supports color changes.""" | |
| 56 | 56 | ||
| 57 | - @property | ||
| 58 | - @abstractmethod | ||
| 59 | - def is_dimmable(self) -> bool: | ||
| 60 | - """Whether the bulb supports brightness changes.""" | ||
| 61 | - | ||
| 62 | 57 | @property | |
| 63 | 58 | @abstractmethod | |
| 64 | 59 | def is_variable_color_temp(self) -> bool: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,7 +6,7 @@ | |||
| 6 | 6 | from abc import ABC, abstractmethod | |
| 7 | 7 | from dataclasses import dataclass | |
| 8 | 8 | from datetime import datetime | |
| 9 | - from typing import Any, Mapping, Sequence, overload | ||
| 9 | + from typing import TYPE_CHECKING, Any, Mapping, Sequence | ||
| 10 | 10 | ||
| 11 | 11 | from .credentials import Credentials | |
| 12 | 12 | from .device_type import DeviceType | |
@@ -15,10 +15,13 @@ | |||
| 15 | 15 | from .exceptions import KasaException | |
| 16 | 16 | from .feature import Feature | |
| 17 | 17 | from .iotprotocol import IotProtocol | |
| 18 | - from .module import Module, ModuleT | ||
| 18 | + from .module import Module | ||
| 19 | 19 | from .protocol import BaseProtocol | |
| 20 | 20 | from .xortransport import XorTransport | |
| 21 | 21 | ||
| 22 | + if TYPE_CHECKING: | ||
| 23 | + from .modulemapping import ModuleMapping | ||
| 24 | + | ||
| 22 | 25 | ||
| 23 | 26 | @dataclass | |
| 24 | 27 | class WifiNetwork: | |
@@ -113,21 +116,9 @@ async def disconnect(self): | |||
| 113 | 116 | ||
| 114 | 117 | @property | |
| 115 | 118 | @abstractmethod | |
| 116 | - def modules(self) -> Mapping[str, Module]: | ||
| 119 | + def modules(self) -> ModuleMapping[Module]: | ||
| 117 | 120 | """Return the device modules.""" | |
| 118 | 121 | ||
| 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 | - | ||
| 131 | 122 | @property | |
| 132 | 123 | @abstractmethod | |
| 133 | 124 | def is_on(self) -> bool: | |
| Original file line number | Diff line number | Diff 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.""" | ||
| Original file line number | Diff line number | Diff 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 | + """ | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,14 +19,15 @@ | |||
| 19 | 19 | import inspect | |
| 20 | 20 | import logging | |
| 21 | 21 | from datetime import datetime, timedelta | |
| 22 | - from typing import Any, Mapping, Sequence, cast, overload | ||
| 22 | + from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast | ||
| 23 | 23 | ||
| 24 | 24 | from ..device import Device, WifiNetwork | |
| 25 | 25 | from ..deviceconfig import DeviceConfig | |
| 26 | 26 | from ..emeterstatus import EmeterStatus | |
| 27 | 27 | from ..exceptions import KasaException | |
| 28 | 28 | from ..feature import Feature | |
| 29 | - from ..module import ModuleT | ||
| 29 | + from ..module import Module | ||
| 30 | + from ..modulemapping import ModuleMapping, ModuleName | ||
| 30 | 31 | from ..protocol import BaseProtocol | |
| 31 | 32 | from .iotmodule import IotModule | |
| 32 | 33 | from .modules import Emeter, Time | |
@@ -190,46 +191,28 @@ def __init__( | |||
| 190 | 191 | self._supported_modules: dict[str, IotModule] | None = None | |
| 191 | 192 | self._legacy_features: set[str] = set() | |
| 192 | 193 | self._children: Mapping[str, IotDevice] = {} | |
| 193 | - self._modules: dict[str, IotModule] = {} | ||
| 194 | + self._modules: dict[str | ModuleName[Module], IotModule] = {} | ||
| 194 | 195 | ||
| 195 | 196 | @property | |
| 196 | 197 | def children(self) -> Sequence[IotDevice]: | |
| 197 | 198 | """Return list of children.""" | |
| 198 | 199 | return list(self._children.values()) | |
| 199 | 200 | ||
| 200 | 201 | @property | |
| 201 | - def modules(self) -> dict[str, IotModule]: | ||
| 202 | + def modules(self) -> ModuleMapping[IotModule]: | ||
| 202 | 203 | """Return the device modules.""" | |
| 204 | + if TYPE_CHECKING: | ||
| 205 | + return cast(ModuleMapping[IotModule], self._modules) | ||
| 203 | 206 | return self._modules | |
| 204 | 207 | ||
| 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): | ||
| 226 | 209 | """Register a module.""" | |
| 227 | 210 | if name in self.modules: | |
| 228 | 211 | _LOGGER.debug("Module %s already registered, ignoring..." % name) | |
| 229 | 212 | return | |
| 230 | 213 | ||
| 231 | 214 | _LOGGER.debug("Adding module %s", module) | |
| 232 | - self.modules[name] = module | ||
| 215 | + self._modules[name] = module | ||
| 233 | 216 | ||
| 234 | 217 | def _create_request( | |
| 235 | 218 | self, target: str, cmd: str, arg: dict | None = None, child_ids=None | |
@@ -291,11 +274,11 @@ def features(self) -> dict[str, Feature]: | |||
| 291 | 274 | ||
| 292 | 275 | @property # type: ignore | |
| 293 | 276 | @requires_update | |
| 294 | - def supported_modules(self) -> list[str]: | ||
| 277 | + def supported_modules(self) -> list[str | ModuleName[Module]]: | ||
| 295 | 278 | """Return a set of modules supported by the device.""" | |
| 296 | 279 | # TODO: this should rather be called `features`, but we don't want to break | |
| 297 | 280 | # 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()) | ||
| 299 | 282 | ||
| 300 | 283 | @property # type: ignore | |
| 301 | 284 | @requires_update | |
@@ -324,10 +307,11 @@ async def update(self, update_children: bool = True): | |||
| 324 | 307 | self._last_update = response | |
| 325 | 308 | self._set_sys_info(response["system"]["get_sysinfo"]) | |
| 326 | 309 | ||
| 310 | + await self._modular_update(req) | ||
| 311 | + | ||
| 327 | 312 | if not self._features: | |
| 328 | 313 | await self._initialize_features() | |
| 329 | 314 | ||
| 330 | - await self._modular_update(req) | ||
| 331 | 315 | self._set_sys_info(self._last_update["system"]["get_sysinfo"]) | |
| 332 | 316 | ||
| 333 | 317 | async def _initialize_features(self): | |
@@ -352,6 +336,11 @@ async def _initialize_features(self): | |||
| 352 | 336 | ) | |
| 353 | 337 | ) | |
| 354 | 338 | ||
| 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 | + | ||
| 355 | 344 | async def _modular_update(self, req: dict) -> None: | |
| 356 | 345 | """Execute an update query.""" | |
| 357 | 346 | if self.has_emeter: | |
@@ -364,17 +353,15 @@ async def _modular_update(self, req: dict) -> None: | |||
| 364 | 353 | # making separate handling for this unnecessary | |
| 365 | 354 | if self._supported_modules is None: | |
| 366 | 355 | supported = {} | |
| 367 | - for module in self.modules.values(): | ||
| 356 | + for module in self._modules.values(): | ||
| 368 | 357 | if module.is_supported: | |
| 369 | 358 | supported[module._module] = module | |
| 370 | - for module_feat in module._module_features.values(): | ||
| 371 | - self._add_feature(module_feat) | ||
| 372 | 359 | ||
| 373 | 360 | self._supported_modules = supported | |
| 374 | 361 | ||
| 375 | 362 | request_list = [] | |
| 376 | 363 | est_response_size = 1024 if "system" in req else 0 | |
| 377 | - for module in self.modules.values(): | ||
| 364 | + for module in self._modules.values(): | ||
| 378 | 365 | if not module.is_supported: | |
| 379 | 366 | _LOGGER.debug("Module %s not supported, skipping" % module) | |
| 380 | 367 | continue | |
| Back | FazBrowse Home | New Git URL |
0 commit comments