Embed FeatureType inside Feature (#860)

Moves `FeatureType` into `Feature` to make it easier to use the API.
This also enforces that no invalid types are accepted (i.e.,
`Category.Config` cannot be a `Sensor`)
If `--verbose` is used with the cli tool, some extra information is
displayed for features when in the state command.
This commit is contained in:
Teemu R 2024-04-24 18:38:52 +02:00 committed by GitHub
parent e410e4f3f3
commit 65874c0365
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 135 additions and 69 deletions

View File

@ -35,7 +35,7 @@ from kasa.exceptions import (
TimeoutError, TimeoutError,
UnsupportedDeviceError, UnsupportedDeviceError,
) )
from kasa.feature import Feature, FeatureType from kasa.feature import Feature
from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors
from kasa.iotprotocol import ( from kasa.iotprotocol import (
IotProtocol, IotProtocol,
@ -58,7 +58,6 @@ __all__ = [
"TurnOnBehavior", "TurnOnBehavior",
"DeviceType", "DeviceType",
"Feature", "Feature",
"FeatureType",
"EmeterStatus", "EmeterStatus",
"Device", "Device",
"Bulb", "Bulb",

View File

@ -585,7 +585,10 @@ async def sysinfo(dev):
def _echo_features( def _echo_features(
features: dict[str, Feature], title: str, category: Feature.Category | None = None features: dict[str, Feature],
title: str,
category: Feature.Category | None = None,
verbose: bool = False,
): ):
"""Print out a listing of features and their values.""" """Print out a listing of features and their values."""
if category is not None: if category is not None:
@ -599,24 +602,42 @@ def _echo_features(
for _, feat in features.items(): for _, feat in features.items():
try: try:
echo(f"\t{feat}") echo(f"\t{feat}")
if verbose:
echo(f"\t\tType: {feat.type}")
echo(f"\t\tCategory: {feat.category}")
echo(f"\t\tIcon: {feat.icon}")
except Exception as ex: except Exception as ex:
echo(f"\t{feat.name} ({feat.id}): got exception (%s)" % ex) echo(f"\t{feat.name} ({feat.id}): got exception (%s)" % ex)
def _echo_all_features(features, title_prefix=None): def _echo_all_features(features, *, verbose=False, title_prefix=None):
"""Print out all features by category.""" """Print out all features by category."""
if title_prefix is not None: if title_prefix is not None:
echo(f"[bold]\n\t == {title_prefix} ==[/bold]") echo(f"[bold]\n\t == {title_prefix} ==[/bold]")
_echo_features( _echo_features(
features, title="\n\t== Primary features ==", category=Feature.Category.Primary features,
title="\n\t== Primary features ==",
category=Feature.Category.Primary,
verbose=verbose,
) )
_echo_features( _echo_features(
features, title="\n\t== Information ==", category=Feature.Category.Info features,
title="\n\t== Information ==",
category=Feature.Category.Info,
verbose=verbose,
) )
_echo_features( _echo_features(
features, title="\n\t== Configuration ==", category=Feature.Category.Config features,
title="\n\t== Configuration ==",
category=Feature.Category.Config,
verbose=verbose,
)
_echo_features(
features,
title="\n\t== Debug ==",
category=Feature.Category.Debug,
verbose=verbose,
) )
_echo_features(features, title="\n\t== Debug ==", category=Feature.Category.Debug)
@cli.command() @cli.command()
@ -636,6 +657,7 @@ async def state(ctx, dev: Device):
_echo_all_features( _echo_all_features(
child.features, child.features,
title_prefix=f"{child.alias} ({child.model}, {child.device_type})", title_prefix=f"{child.alias} ({child.model}, {child.device_type})",
verbose=verbose,
) )
echo() echo()
@ -647,7 +669,7 @@ async def state(ctx, dev: Device):
echo(f"\tMAC (rssi): {dev.mac} ({dev.rssi})") echo(f"\tMAC (rssi): {dev.mac} ({dev.rssi})")
echo(f"\tLocation: {dev.location}") echo(f"\tLocation: {dev.location}")
_echo_all_features(dev.features) _echo_all_features(dev.features, verbose=verbose)
echo("\n\t[bold]== Modules ==[/bold]") echo("\n\t[bold]== Modules ==[/bold]")
for module in dev.modules.values(): for module in dev.modules.values():

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import Enum, auto
from typing import TYPE_CHECKING, Any, Callable from typing import TYPE_CHECKING, Any, Callable
@ -10,26 +11,44 @@ if TYPE_CHECKING:
from .device import Device from .device import Device
# TODO: This is only useful for Feature, so maybe move to Feature.Type? _LOGGER = logging.getLogger(__name__)
class FeatureType(Enum):
"""Type to help decide how to present the feature."""
Sensor = auto()
BinarySensor = auto()
Switch = auto()
Action = auto()
Number = auto()
@dataclass @dataclass
class Feature: class Feature:
"""Feature defines a generic interface for device features.""" """Feature defines a generic interface for device features."""
class Type(Enum):
"""Type to help decide how to present the feature."""
#: Sensor is an informative read-only value
Sensor = auto()
#: BinarySensor is a read-only boolean
BinarySensor = auto()
#: Switch is a boolean setting
Switch = auto()
#: Action triggers some action on device
Action = auto()
#: Number defines a numeric setting
#: See :ref:`range_getter`, :ref:`minimum_value`, and :ref:`maximum_value`
Number = auto()
#: Choice defines a setting with pre-defined values
Choice = auto()
Unknown = -1
# TODO: unsure if this is a great idea..
Sensor = Type.Sensor
BinarySensor = Type.BinarySensor
Switch = Type.Switch
Action = Type.Action
Number = Type.Number
Choice = Type.Choice
class Category(Enum): class Category(Enum):
"""Category hint for downstreams.""" """Category hint to allow feature grouping."""
#: Primary features control the device state directly. #: Primary features control the device state directly.
#: Examples including turning the device on, or adjust its brightness. #: Examples include turning the device on/off, or adjusting its brightness.
Primary = auto() Primary = auto()
#: Config features change device behavior without immediate state changes. #: Config features change device behavior without immediate state changes.
Config = auto() Config = auto()
@ -58,7 +77,7 @@ class Feature:
#: Category hint for downstreams #: Category hint for downstreams
category: Feature.Category = Category.Unset category: Feature.Category = Category.Unset
#: Type of the feature #: Type of the feature
type: FeatureType = FeatureType.Sensor type: Feature.Type = Type.Sensor
# Number-specific attributes # Number-specific attributes
#: Minimum value #: Minimum value
@ -92,10 +111,19 @@ class Feature:
else: else:
self.category = Feature.Category.Info self.category = Feature.Category.Info
if self.category == Feature.Category.Config and self.type in [
Feature.Type.Sensor,
Feature.Type.BinarySensor,
]:
raise ValueError(
f"Invalid type for configurable feature: {self.name} ({self.id}):"
f" {self.type}"
)
@property @property
def value(self): def value(self):
"""Return the current value.""" """Return the current value."""
if self.type == FeatureType.Action: if self.type == Feature.Type.Action:
return "<Action>" return "<Action>"
if self.attribute_getter is None: if self.attribute_getter is None:
raise ValueError("Not an action and no attribute_getter set") raise ValueError("Not an action and no attribute_getter set")
@ -109,7 +137,7 @@ class Feature:
"""Set the value.""" """Set the value."""
if self.attribute_setter is None: if self.attribute_setter is None:
raise ValueError("Tried to set read-only feature.") raise ValueError("Tried to set read-only feature.")
if self.type == FeatureType.Number: # noqa: SIM102 if self.type == Feature.Type.Number: # noqa: SIM102
if value < self.minimum_value or value > self.maximum_value: if value < self.minimum_value or value > self.maximum_value:
raise ValueError( raise ValueError(
f"Value {value} out of range " f"Value {value} out of range "
@ -117,7 +145,7 @@ class Feature:
) )
container = self.container if self.container is not None else self.device container = self.container if self.container is not None else self.device
if self.type == FeatureType.Action: if self.type == Feature.Type.Action:
return await getattr(container, self.attribute_setter)() return await getattr(container, self.attribute_setter)()
return await getattr(container, self.attribute_setter)(value) return await getattr(container, self.attribute_setter)(value)
@ -127,7 +155,7 @@ class Feature:
if self.unit is not None: if self.unit is not None:
s += f" {self.unit}" s += f" {self.unit}"
if self.type == FeatureType.Number: if self.type == Feature.Type.Number:
s += f" (range: {self.minimum_value}-{self.maximum_value})" s += f" (range: {self.minimum_value}-{self.maximum_value})"
return s return s

View File

@ -15,7 +15,7 @@ except ImportError:
from ..bulb import HSV, Bulb, BulbPreset, ColorTempRange from ..bulb import HSV, Bulb, BulbPreset, ColorTempRange
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..feature import Feature, FeatureType from ..feature import Feature
from ..protocol import BaseProtocol from ..protocol import BaseProtocol
from .iotdevice import IotDevice, KasaException, requires_update from .iotdevice import IotDevice, KasaException, requires_update
from .modules import Antitheft, Cloud, Countdown, Emeter, Schedule, Time, Usage from .modules import Antitheft, Cloud, Countdown, Emeter, Schedule, Time, Usage
@ -221,7 +221,7 @@ class IotBulb(IotDevice, Bulb):
attribute_setter="set_brightness", attribute_setter="set_brightness",
minimum_value=1, minimum_value=1,
maximum_value=100, maximum_value=100,
type=FeatureType.Number, type=Feature.Type.Number,
category=Feature.Category.Primary, category=Feature.Category.Primary,
) )
) )

View File

@ -7,7 +7,7 @@ from typing import Any
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..feature import Feature, FeatureType from ..feature import Feature
from ..protocol import BaseProtocol from ..protocol import BaseProtocol
from .iotdevice import KasaException, requires_update from .iotdevice import KasaException, requires_update
from .iotplug import IotPlug from .iotplug import IotPlug
@ -96,7 +96,7 @@ class IotDimmer(IotPlug):
attribute_setter="set_brightness", attribute_setter="set_brightness",
minimum_value=1, minimum_value=1,
maximum_value=100, maximum_value=100,
type=FeatureType.Number, type=Feature.Type.Number,
) )
) )

View File

@ -6,7 +6,7 @@ import logging
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..feature import Feature, FeatureType from ..feature import Feature
from ..protocol import BaseProtocol from ..protocol import BaseProtocol
from .iotdevice import IotDevice, requires_update from .iotdevice import IotDevice, requires_update
from .modules import Antitheft, Cloud, Schedule, Time, Usage from .modules import Antitheft, Cloud, Schedule, Time, Usage
@ -69,7 +69,7 @@ class IotPlug(IotDevice):
icon="mdi:led-{state}", icon="mdi:led-{state}",
attribute_getter="led", attribute_getter="led",
attribute_setter="set_led", attribute_setter="set_led",
type=FeatureType.Switch, type=Feature.Type.Switch,
) )
) )

View File

@ -1,6 +1,6 @@
"""Implementation of the ambient light (LAS) module found in some dimmers.""" """Implementation of the ambient light (LAS) module found in some dimmers."""
from ...feature import Feature, FeatureType from ...feature import Feature
from ..iotmodule import IotModule, merge from ..iotmodule import IotModule, merge
# TODO create tests and use the config reply there # TODO create tests and use the config reply there
@ -25,7 +25,7 @@ class AmbientLight(IotModule):
name="Ambient Light", name="Ambient Light",
icon="mdi:brightness-percent", icon="mdi:brightness-percent",
attribute_getter="ambientlight_brightness", attribute_getter="ambientlight_brightness",
type=FeatureType.Sensor, type=Feature.Type.Sensor,
) )
) )

View File

@ -5,7 +5,7 @@ try:
except ImportError: except ImportError:
from pydantic import BaseModel from pydantic import BaseModel
from ...feature import Feature, FeatureType from ...feature import Feature
from ..iotmodule import IotModule from ..iotmodule import IotModule
@ -36,7 +36,7 @@ class Cloud(IotModule):
name="Cloud connection", name="Cloud connection",
icon="mdi:cloud", icon="mdi:cloud",
attribute_getter="is_connected", attribute_getter="is_connected",
type=FeatureType.BinarySensor, type=Feature.Type.BinarySensor,
) )
) )

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -32,7 +32,7 @@ class AlarmModule(SmartModule):
container=self, container=self,
attribute_getter="active", attribute_getter="active",
icon="mdi:bell", icon="mdi:bell",
type=FeatureType.BinarySensor, type=Feature.Type.BinarySensor,
) )
) )
self._add_feature( self._add_feature(
@ -60,7 +60,7 @@ class AlarmModule(SmartModule):
"Test alarm", "Test alarm",
container=self, container=self,
attribute_setter="play", attribute_setter="play",
type=FeatureType.Action, type=Feature.Type.Action,
) )
) )
self._add_feature( self._add_feature(
@ -69,7 +69,7 @@ class AlarmModule(SmartModule):
"Stop alarm", "Stop alarm",
container=self, container=self,
attribute_setter="stop", attribute_setter="stop",
type=FeatureType.Action, type=Feature.Type.Action,
) )
) )

View File

@ -27,6 +27,7 @@ class AutoOffModule(SmartModule):
container=self, container=self,
attribute_getter="enabled", attribute_getter="enabled",
attribute_setter="set_enabled", attribute_setter="set_enabled",
type=Feature.Type.Switch,
) )
) )
self._add_feature( self._add_feature(
@ -36,6 +37,7 @@ class AutoOffModule(SmartModule):
container=self, container=self,
attribute_getter="delay", attribute_getter="delay",
attribute_setter="set_delay", attribute_setter="set_delay",
type=Feature.Type.Number,
) )
) )
self._add_feature( self._add_feature(

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -35,7 +35,7 @@ class BatterySensor(SmartModule):
container=self, container=self,
attribute_getter="battery_low", attribute_getter="battery_low",
icon="mdi:alert", icon="mdi:alert",
type=FeatureType.BinarySensor, type=Feature.Type.BinarySensor,
) )
) )

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -31,7 +31,7 @@ class Brightness(SmartModule):
attribute_setter="set_brightness", attribute_setter="set_brightness",
minimum_value=BRIGHTNESS_MIN, minimum_value=BRIGHTNESS_MIN,
maximum_value=BRIGHTNESS_MAX, maximum_value=BRIGHTNESS_MAX,
type=FeatureType.Number, type=Feature.Type.Number,
category=Feature.Category.Primary, category=Feature.Category.Primary,
) )
) )

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...exceptions import SmartErrorCode from ...exceptions import SmartErrorCode
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -28,7 +28,7 @@ class CloudModule(SmartModule):
container=self, container=self,
attribute_getter="is_connected", attribute_getter="is_connected",
icon="mdi:cloud", icon="mdi:cloud",
type=FeatureType.BinarySensor, type=Feature.Type.BinarySensor,
) )
) )

View File

@ -25,8 +25,9 @@ class ColorModule(SmartModule):
"HSV", "HSV",
container=self, container=self,
attribute_getter="hsv", attribute_getter="hsv",
# TODO proper type for setting hsv
attribute_setter="set_hsv", attribute_setter="set_hsv",
# TODO proper type for setting hsv
type=Feature.Type.Unknown,
) )
) )

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -27,7 +27,7 @@ class FanModule(SmartModule):
attribute_getter="fan_speed_level", attribute_getter="fan_speed_level",
attribute_setter="set_fan_speed_level", attribute_setter="set_fan_speed_level",
icon="mdi:fan", icon="mdi:fan",
type=FeatureType.Number, type=Feature.Type.Number,
minimum_value=1, minimum_value=1,
maximum_value=4, maximum_value=4,
category=Feature.Category.Primary, category=Feature.Category.Primary,
@ -41,7 +41,7 @@ class FanModule(SmartModule):
attribute_getter="sleep_mode", attribute_getter="sleep_mode",
attribute_setter="set_sleep_mode", attribute_setter="set_sleep_mode",
icon="mdi:sleep", icon="mdi:sleep",
type=FeatureType.Switch, type=Feature.Type.Switch,
) )
) )

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional from typing import TYPE_CHECKING, Any, Optional
from ...exceptions import SmartErrorCode from ...exceptions import SmartErrorCode
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
try: try:
@ -59,7 +59,7 @@ class Firmware(SmartModule):
container=self, container=self,
attribute_getter="auto_update_enabled", attribute_getter="auto_update_enabled",
attribute_setter="set_auto_update_enabled", attribute_setter="set_auto_update_enabled",
type=FeatureType.Switch, type=Feature.Type.Switch,
) )
) )
self._add_feature( self._add_feature(
@ -68,7 +68,7 @@ class Firmware(SmartModule):
"Update available", "Update available",
container=self, container=self,
attribute_getter="update_available", attribute_getter="update_available",
type=FeatureType.BinarySensor, type=Feature.Type.BinarySensor,
) )
) )

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -34,7 +34,7 @@ class HumiditySensor(SmartModule):
"Humidity warning", "Humidity warning",
container=self, container=self,
attribute_getter="humidity_warning", attribute_getter="humidity_warning",
type=FeatureType.BinarySensor, type=Feature.Type.BinarySensor,
icon="mdi:alert", icon="mdi:alert",
) )
) )

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -27,7 +27,7 @@ class LedModule(SmartModule):
icon="mdi:led-{state}", icon="mdi:led-{state}",
attribute_getter="led", attribute_getter="led",
attribute_setter="set_led", attribute_setter="set_led",
type=FeatureType.Switch, type=Feature.Type.Switch,
category=Feature.Category.Config, category=Feature.Category.Config,
) )
) )

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...exceptions import KasaException from ...exceptions import KasaException
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -35,7 +35,7 @@ class LightTransitionModule(SmartModule):
icon=icon, icon=icon,
attribute_getter="enabled_v1", attribute_getter="enabled_v1",
attribute_setter="set_enabled_v1", attribute_setter="set_enabled_v1",
type=FeatureType.Switch, type=Feature.Type.Switch,
) )
) )
elif self.supported_version >= 2: elif self.supported_version >= 2:
@ -51,7 +51,7 @@ class LightTransitionModule(SmartModule):
attribute_getter="turn_on_transition", attribute_getter="turn_on_transition",
attribute_setter="set_turn_on_transition", attribute_setter="set_turn_on_transition",
icon=icon, icon=icon,
type=FeatureType.Number, type=Feature.Type.Number,
maximum_value=self.MAXIMUM_DURATION, maximum_value=self.MAXIMUM_DURATION,
) )
) # self._turn_on_transition_max ) # self._turn_on_transition_max
@ -63,7 +63,7 @@ class LightTransitionModule(SmartModule):
attribute_getter="turn_off_transition", attribute_getter="turn_off_transition",
attribute_setter="set_turn_off_transition", attribute_setter="set_turn_off_transition",
icon=icon, icon=icon,
type=FeatureType.Number, type=Feature.Type.Number,
maximum_value=self.MAXIMUM_DURATION, maximum_value=self.MAXIMUM_DURATION,
) )
) # self._turn_off_transition_max ) # self._turn_off_transition_max

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
from ...feature import Feature, FeatureType from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -35,7 +35,7 @@ class TemperatureSensor(SmartModule):
"Temperature warning", "Temperature warning",
container=self, container=self,
attribute_getter="temperature_warning", attribute_getter="temperature_warning",
type=FeatureType.BinarySensor, type=Feature.Type.BinarySensor,
icon="mdi:alert", icon="mdi:alert",
) )
) )
@ -46,6 +46,7 @@ class TemperatureSensor(SmartModule):
container=self, container=self,
attribute_getter="temperature_unit", attribute_getter="temperature_unit",
attribute_setter="set_temperature_unit", attribute_setter="set_temperature_unit",
type=Feature.Type.Choice,
) )
) )
# TODO: use temperature_unit for feature creation # TODO: use temperature_unit for feature creation

View File

@ -26,6 +26,7 @@ class TemperatureControl(SmartModule):
attribute_getter="target_temperature", attribute_getter="target_temperature",
attribute_setter="set_target_temperature", attribute_setter="set_target_temperature",
icon="mdi:thermometer", icon="mdi:thermometer",
type=Feature.Type.Number,
) )
) )
# TODO: this might belong into its own module, temperature_correction? # TODO: this might belong into its own module, temperature_correction?
@ -38,6 +39,7 @@ class TemperatureControl(SmartModule):
attribute_setter="set_temperature_offset", attribute_setter="set_temperature_offset",
minimum_value=-10, minimum_value=-10,
maximum_value=10, maximum_value=10,
type=Feature.Type.Number,
) )
) )

View File

@ -13,7 +13,7 @@ from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
from ..emeterstatus import EmeterStatus from ..emeterstatus import EmeterStatus
from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
from ..feature import Feature, FeatureType from ..feature import Feature
from ..smartprotocol import SmartProtocol from ..smartprotocol import SmartProtocol
from .modules import * # noqa: F403 from .modules import * # noqa: F403
@ -191,7 +191,7 @@ class SmartDevice(Device):
"State", "State",
attribute_getter="is_on", attribute_getter="is_on",
attribute_setter="set_state", attribute_setter="set_state",
type=FeatureType.Switch, type=Feature.Type.Switch,
category=Feature.Category.Primary, category=Feature.Category.Primary,
) )
) )
@ -236,7 +236,7 @@ class SmartDevice(Device):
"Overheated", "Overheated",
attribute_getter=lambda x: x._info["overheated"], attribute_getter=lambda x: x._info["overheated"],
icon="mdi:heat-wave", icon="mdi:heat-wave",
type=FeatureType.BinarySensor, type=Feature.Type.BinarySensor,
category=Feature.Category.Debug, category=Feature.Category.Debug,
) )
) )

View File

@ -1,6 +1,6 @@
import pytest import pytest
from kasa import Feature, FeatureType from kasa import Feature
class DummyDevice: class DummyDevice:
@ -18,7 +18,7 @@ def dummy_feature() -> Feature:
attribute_setter="dummysetter", attribute_setter="dummysetter",
container=None, container=None,
icon="mdi:dummy", icon="mdi:dummy",
type=FeatureType.BinarySensor, type=Feature.Type.Switch,
unit="dummyunit", unit="dummyunit",
) )
return feat return feat
@ -32,10 +32,21 @@ def test_feature_api(dummy_feature: Feature):
assert dummy_feature.attribute_setter == "dummysetter" assert dummy_feature.attribute_setter == "dummysetter"
assert dummy_feature.container is None assert dummy_feature.container is None
assert dummy_feature.icon == "mdi:dummy" assert dummy_feature.icon == "mdi:dummy"
assert dummy_feature.type == FeatureType.BinarySensor assert dummy_feature.type == Feature.Type.Switch
assert dummy_feature.unit == "dummyunit" assert dummy_feature.unit == "dummyunit"
def test_feature_missing_type():
"""Test that creating a feature with a setter but without type causes an error."""
with pytest.raises(ValueError):
Feature(
device=DummyDevice(), # type: ignore[arg-type]
name="dummy error",
attribute_getter="dummygetter",
attribute_setter="dummysetter",
)
def test_feature_value(dummy_feature: Feature): def test_feature_value(dummy_feature: Feature):
"""Verify that property gets accessed on *value* access.""" """Verify that property gets accessed on *value* access."""
dummy_feature.attribute_getter = "test_prop" dummy_feature.attribute_getter = "test_prop"
@ -91,7 +102,7 @@ async def test_feature_action(mocker):
attribute_setter="call_action", attribute_setter="call_action",
container=None, container=None,
icon="mdi:dummy", icon="mdi:dummy",
type=FeatureType.Action, type=Feature.Type.Action,
) )
mock_call_action = mocker.patch.object(feat.device, "call_action", create=True) mock_call_action = mocker.patch.object(feat.device, "call_action", create=True)
assert feat.value == "<Action>" assert feat.value == "<Action>"