8000 Add FeatureAttributes to smartcam Alarm (#1489) · python-kasa/python-kasa@44c561b · GitHub
[go: up one dir, main page]

Skip to content

Commit 44c561b

Browse files
sdb9696rytilahti
andauthored
Add FeatureAttributes to smartcam Alarm (#1489)
Co-authored-by: Teemu R. <tpr@iki.fi>
1 parent ebd370d commit 44c561b

File tree

4 files changed

+78
-11
lines changed

4 files changed

+78
-11
lines changed

kasa/interfaces/energy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class ModuleFeature(IntFlag):
2828

2929
_supported: ModuleFeature = ModuleFeature(0)
3030

31-
def supports(self, module_feature: ModuleFeature) -> bool:
31+
def supports(self, module_feature: Energy.ModuleFeature) -> bool:
3232
"""Return True if module supports the feature."""
3333
return module_feature in self._supported
3434

kasa/smart/smartmodule.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from __future__ import annotations
44

55
import logging
6-
from collections.abc import Awaitable, Callable, Coroutine
6+
from collections.abc import Callable, Coroutine
7+
from functools import wraps
78
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar
89

910
from ..exceptions import DeviceError, KasaException, SmartErrorCode
@@ -20,15 +21,16 @@
2021

2122

2223
def allow_update_after(
23-
func: Callable[Concatenate[_T, _P], Awaitable[dict]],
24-
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, dict]]:
24+
func: Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]],
25+
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]]:
2526
"""Define a wrapper to set _last_update_time to None.
2627
2728
This will ensure that a module is updated in the next update cycle after
2829
a value has been changed.
2930
"""
3031

31-
async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> dict:
32+
@wraps(func)
33+
async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R:
3234
try:
3335
return await func(self, *args, **kwargs)
3436
finally:
@@ -40,6 +42,7 @@ async def _async_wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> dict:
4042
def raise_if_update_error(func: Callable[[_T], _R]) -> Callable[[_T], _R]:
4143
"""Define a wrapper to raise an error if the last module update was an error."""
4244

45+
@wraps(func)
4346
def _wrap(self: _T) -> _R:
4447
if err := self._last_update_error:
4548
raise err

kasa/smartcam/modules/alarm.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
from __future__ import annotations
44

5+
from typing import Annotated
6+
57
from ...feature import Feature
68
from ...interfaces import Alarm as AlarmInterface
9+
from ...module import FeatureAttribute
710
from ...smart.smartmodule import allow_update_after
811
from ..smartcammodule import SmartCamModule
912

@@ -105,12 +108,12 @@ def _initialize_features(self) -> None:
105108
)
106109

107110
@property
108-
def alarm_sound(self) -> str:
111+
def alarm_sound(self) -> Annotated[str, FeatureAttribute()]:
109112
"""Return current alarm sound."""
110113
return self.data["getSirenConfig"]["siren_type"]
111114

112115
@allow_update_after
113-
async def set_alarm_sound(self, sound: str) -> dict:
116+
async def set_alarm_sound(self, sound: str) -> Annotated[dict, FeatureAttribute()]:
114117
"""Set alarm sound.
115118
116119
See *alarm_sounds* for list of available sounds.
@@ -124,26 +127,30 @@ def alarm_sounds(self) -> list[str]:
124127
return self.data["getSirenTypeList"]["siren_type_list"]
125128

126129
@property
127-
def alarm_volume(self) -> int:
130+
def alarm_volume(self) -> Annotated[int, FeatureAttribute()]:
128131
"""Return alarm volume.
129132
130133
Unlike duration the device expects/returns a string for volume.
131134
"""
132135
return int(self.data["getSirenConfig"]["volume"])
133136

134137
@allow_update_after
135-
async def set_alarm_volume(self, volume: int) -> dict:
138+
async def set_alarm_volume(
139+
self, volume: int
140+
) -> Annotated[dict, FeatureAttribute()]:
136141
"""Set alarm volume."""
137142
config = self._validate_and_get_config(volume=volume)
138143
return await self.call("setSirenConfig", {"siren": config})
139144

140145
@property
141-
def alarm_duration(self) -> int:
146+
def alarm_duration(self) -> Annotated[int, FeatureAttribute()]:
142147
"""Return alarm duration."""
143148
return self.data["getSirenConfig"]["duration"]
144149

145150
@allow_update_after
146-
async def set_alarm_duration(self, duration: int) -> dict:
151+
async def set_alarm_duration(
152+
self, duration: int
153+
) -> Annotated[dict, FeatureAttribute()]:
147154
"""Set alarm volume."""
148155
config = self._validate_and_get_config(duration=duration)
149156
return await self.call("setSirenConfig", {"siren": config})

tests/test_common_modules.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import importlib
2+
import inspect
3+
import pkgutil
4+
import sys
15
from datetime import datetime
26
from zoneinfo import ZoneInfo
37

48
import pytest
59
from pytest_mock import MockerFixture
610

11+
import kasa.interfaces
712
from kasa import Device, LightState, Module, ThermostatState
13+
from kasa.module import _get_feature_attribute
814

915
from .device_fixtures import (
1016
bulb_iot,
@@ -64,6 +70,57 @@
6470
)
6571

6672

73+
interfaces = pytest.mark.parametrize("interface", kasa.interfaces.__all__)
74+
75+
76+
def _get_subclasses(of_class, package):
77+
"""Get all the subclasses of a given class."""
78+
subclasses = set()
79+
# iter_modules returns ModuleInfo: (module_finder, name, ispkg)
80+
for _, modname, ispkg in pkgutil.iter_modules(package.__path__):
81+
importlib.import_module("." + modname, package=package.__name__)
82+
module = sys.modules[package.__name__ + "." + modname]
83+
for _, obj in inspect.getmembers(module):
84+
if (
85+
inspect.isclass(obj)
86+
and issubclass(obj, of_class)
87+
and obj is not of_class
88+
):
89+
subclasses.add(obj)
90+
91+
if ispkg:
92+
res = _get_subclasses(of_class, module)
93+
subclasses.update(res)
94+
95+
return subclasses
96+
97+
98+
@interfaces
99+
def test_feature_attributes(interface):
100+
"""Test that all common derived classes define the FeatureAttributes."""
101+
klass = getattr(kasa.interfaces, interface)
102+
103+
package = sys.modules["kasa"]
104+
sub_classes = _get_subclasses(klass, package)
105+
106+
feat_attributes: set[str] = set()
107+
attribute_names = [
108+
k
109+
for k, v in vars(klass).items()
110+
if (callable(v) and not inspect.isclass(v)) or isinstance(v, property)
111+
]
112+
for attr_name in attribute_names:
113+
attribute = getattr(klass, attr_name)
114+
if _get_feature_attribute(attribute):
115+
feat_attributes.add(attr_name)
116+
117+
for sub_class in sub_classes:
118+
for attr_name in feat_attributes:
119+
attribute = getattr(sub_class, attr_name)
120+
fa = _get_feature_attribute(attribute)
121+
assert fa, f"{attr_name} is not a defined module feature for {sub_class}"
122+
123+
67124
@led
68125
async def test_led_module(dev: Device, mocker: MockerFixture):
69126
"""Test fan speed feature."""

0 commit comments

Comments
 (0)
0