Implement action feature (#849)

Adds `FeatureType.Action` making it possible to expose features like
"reboot", "test alarm", "pair" etc.

The `attribute_getter` is no longer mandatory, but it will raise an
exception if not defined for other types than actions.
Trying to read returns a static string `<Action>`.
This overloads the `set_value` to call the given callable on any value.

This also fixes the `play` and `stop` coroutines of the alarm module to
await the call.
This commit is contained in:
Teemu R 2024-04-23 19:49:04 +02:00 committed by GitHub
parent b860c32d5f
commit 6e5cae1f47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 50 additions and 6 deletions

View File

@ -17,7 +17,7 @@ class FeatureType(Enum):
Sensor = auto()
BinarySensor = auto()
Switch = auto()
Button = auto()
Action = auto()
Number = auto()
@ -46,7 +46,7 @@ class Feature:
#: User-friendly short description
name: str
#: Name of the property that allows accessing the value
attribute_getter: str | Callable
attribute_getter: str | Callable | None = None
#: Name of the method that allows changing the value
attribute_setter: str | None = None
#: Container storing the data, this overrides 'device' for getters
@ -95,6 +95,11 @@ class Feature:
@property
def value(self):
"""Return the current value."""
if self.type == FeatureType.Action:
return "<Action>"
if self.attribute_getter is None:
raise ValueError("Not an action and no attribute_getter set")
container = self.container if self.container is not None else self.device
if isinstance(self.attribute_getter, Callable):
return self.attribute_getter(container)
@ -112,6 +117,9 @@ class Feature:
)
container = self.container if self.container is not None else self.device
if self.type == FeatureType.Action:
return await getattr(container, self.attribute_setter)()
return await getattr(container, self.attribute_setter)(value)
def __repr__(self):

View File

@ -54,6 +54,24 @@ class AlarmModule(SmartModule):
device, "Alarm volume", container=self, attribute_getter="alarm_volume"
)
)
self._add_feature(
Feature(
device,
"Test alarm",
container=self,
attribute_setter="play",
type=FeatureType.Action,
)
)
self._add_feature(
Feature(
device,
"Stop alarm",
container=self,
attribute_setter="stop",
type=FeatureType.Action,
)
)
@property
def alarm_sound(self):
@ -83,8 +101,8 @@ class AlarmModule(SmartModule):
async def play(self):
"""Play alarm."""
return self.call("play_alarm")
return await self.call("play_alarm")
async def stop(self):
"""Stop alarm."""
return self.call("stop_alarm")
return await self.call("stop_alarm")

View File

@ -3,11 +3,13 @@ import pytest
from kasa import Feature, FeatureType
class DummyDevice:
pass
@pytest.fixture
def dummy_feature() -> Feature:
# create_autospec for device slows tests way too much, so we use a dummy here
class DummyDevice:
pass
feat = Feature(
device=DummyDevice(), # type: ignore[arg-type]
@ -79,3 +81,19 @@ async def test_feature_setter_read_only(dummy_feature):
dummy_feature.attribute_setter = None
with pytest.raises(ValueError):
await dummy_feature.set_value("value for read only feature")
async def test_feature_action(mocker):
"""Test that setting value on button calls the setter."""
feat = Feature(
device=DummyDevice(), # type: ignore[arg-type]
name="dummy_feature",
attribute_setter="call_action",
container=None,
icon="mdi:dummy",
type=FeatureType.Action,
)
mock_call_action = mocker.patch.object(feat.device, "call_action", create=True)
assert feat.value == "<Action>"
await feat.set_value(1234)
mock_call_action.assert_called()