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.
This commit is contained in:
Steven B
2024-05-10 19:29:28 +01:00
committed by GitHub
parent 7d4dc4c710
commit 9473d97ad2
33 changed files with 673 additions and 220 deletions

View File

@@ -2,37 +2,16 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from ...feature import Feature
from ...interfaces.led import Led
from ..smartmodule import SmartModule
if TYPE_CHECKING:
from ..smartdevice import SmartDevice
class LedModule(SmartModule):
class LedModule(SmartModule, Led):
"""Implementation of led controls."""
REQUIRED_COMPONENT = "led"
QUERY_GETTER_NAME = "get_led_info"
def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module)
self._add_feature(
Feature(
device=device,
container=self,
id="led",
name="LED",
icon="mdi:led-{state}",
attribute_getter="led",
attribute_setter="set_led",
type=Feature.Type.Switch,
category=Feature.Category.Config,
)
)
def query(self) -> dict:
"""Query to execute during the update cycle."""
return {self.QUERY_GETTER_NAME: {"led_rule": None}}
@@ -56,7 +35,7 @@ class LedModule(SmartModule):
This should probably be a select with always/never/nightmode.
"""
rule = "always" if enable else "never"
return await self.call("set_led_info", self.data | {"led_rule": rule})
return await self.call("set_led_info", dict(self.data, **{"led_rule": rule}))
@property
def night_mode_settings(self):

View File

@@ -6,14 +6,14 @@ import base64
import copy
from typing import TYPE_CHECKING, Any
from ...feature import Feature
from ...interfaces.lighteffect import LightEffect
from ..smartmodule import SmartModule
if TYPE_CHECKING:
from ..smartdevice import SmartDevice
class LightEffectModule(SmartModule):
class LightEffectModule(SmartModule, LightEffect):
"""Implementation of dynamic light effects."""
REQUIRED_COMPONENT = "light_effect"
@@ -22,29 +22,11 @@ class LightEffectModule(SmartModule):
"L1": "Party",
"L2": "Relax",
}
LIGHT_EFFECTS_OFF = "Off"
def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module)
self._scenes_names_to_id: dict[str, str] = {}
def _initialize_features(self):
"""Initialize features."""
device = self._device
self._add_feature(
Feature(
device,
id="light_effect",
name="Light effect",
container=self,
attribute_getter="effect",
attribute_setter="set_effect",
category=Feature.Category.Config,
type=Feature.Type.Choice,
choices_getter="effect_list",
)
)
def _initialize_effects(self) -> dict[str, dict[str, Any]]:
"""Return built-in effects."""
# Copy the effects so scene name updates do not update the underlying dict.
@@ -64,7 +46,7 @@ class LightEffectModule(SmartModule):
return effects
@property
def effect_list(self) -> list[str] | None:
def effect_list(self) -> list[str]:
"""Return built-in effects list.
Example:
@@ -90,6 +72,9 @@ class LightEffectModule(SmartModule):
async def set_effect(
self,
effect: str,
*,
brightness: int | None = None,
transition: int | None = None,
) -> None:
"""Set an effect for the device.
@@ -108,6 +93,24 @@ class LightEffectModule(SmartModule):
params["id"] = effect_id
return await self.call("set_dynamic_light_effect_rule_enable", params)
async def set_custom_effect(
self,
effect_dict: dict,
) -> None:
"""Set a custom effect on the device.
:param str effect_dict: The custom effect dict to set
"""
raise NotImplementedError(
"Device does not support setting custom effects. "
"Use has_custom_effects to check for support."
)
@property
def has_custom_effects(self) -> bool:
"""Return True if the device supports setting custom effects."""
return False
def query(self) -> dict:
"""Query to execute during the update cycle."""
return {self.QUERY_GETTER_NAME: {"start_index": 0}}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import base64
import logging
from datetime import datetime, timedelta
from typing import Any, Mapping, Sequence, cast, overload
from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast
from ..aestransport import AesTransport
from ..bulb import HSV, Bulb, BulbPreset, ColorTempRange
@@ -16,7 +16,8 @@ from ..emeterstatus import EmeterStatus
from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
from ..fan import Fan
from ..feature import Feature
from ..module import ModuleT
from ..module import Module
from ..modulemapping import ModuleMapping, ModuleName
from ..smartprotocol import SmartProtocol
from .modules import (
Brightness,
@@ -61,7 +62,7 @@ class SmartDevice(Bulb, Fan, Device):
self._components_raw: dict[str, Any] | None = None
self._components: dict[str, int] = {}
self._state_information: dict[str, Any] = {}
self._modules: dict[str, SmartModule] = {}
self._modules: dict[str | ModuleName[Module], SmartModule] = {}
self._exposes_child_modules = False
self._parent: SmartDevice | None = None
self._children: Mapping[str, SmartDevice] = {}
@@ -102,8 +103,20 @@ class SmartDevice(Bulb, Fan, Device):
return list(self._children.values())
@property
def modules(self) -> dict[str, SmartModule]:
def modules(self) -> ModuleMapping[SmartModule]:
"""Return the device modules."""
if self._exposes_child_modules:
modules = {k: v for k, v in self._modules.items()}
for child in self._children.values():
for k, v in child._modules.items():
if k not in modules:
modules[k] = v
if TYPE_CHECKING:
return cast(ModuleMapping[SmartModule], modules)
return modules
if TYPE_CHECKING: # Needed for python 3.8
return cast(ModuleMapping[SmartModule], self._modules)
return self._modules
def _try_get_response(self, responses: dict, request: str, default=None) -> dict:
@@ -315,30 +328,6 @@ class SmartDevice(Bulb, Fan, Device):
for feat in module._module_features.values():
self._add_feature(feat)
@overload
def get_module(self, module_type: type[ModuleT]) -> ModuleT | None: ...
@overload
def get_module(self, module_type: str) -> SmartModule | None: ...
def get_module(
self, module_type: type[ModuleT] | str
) -> ModuleT | SmartModule | None:
"""Return the module from the device modules or None if not present."""
if isinstance(module_type, str):
module_name = module_type
elif issubclass(module_type, SmartModule):
module_name = module_type.__name__
else:
return None
if module_name in self.modules:
return self.modules[module_name]
elif self._exposes_child_modules:
for child in self._children.values():
if module_name in child.modules:
return child.modules[module_name]
return None
@property
def is_cloud_connected(self):
"""Returns if the device is connected to the cloud."""