8000 Embed FeatureType inside Feature by rytilahti · Pull Request #860 · python-kasa/python-kasa · GitHub
[go: up one dir, main page]

Skip to content

Embed FeatureType inside Feature #860

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Fai 8000 led to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions kasa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
TimeoutError,
UnsupportedDeviceError,
)
from kasa.feature import Feature, FeatureType
from kasa.feature import Feature
from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors
from kasa.iotprotocol import (
IotProtocol,
Expand All @@ -58,7 +58,6 @@
"TurnOnBehavior",
"DeviceType",
"Feature",
"FeatureType",
"EmeterStatus",
"Device",
"Bulb",
Expand Down
36 changes: 29 additions & 7 deletions kasa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,10 @@ async def sysinfo(dev):


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."""
if category is not None:
Expand All @@ -599,24 +602,42 @@ def _echo_features(
for _, feat in features.items():
try:
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:
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."""
if title_prefix is not None:
echo(f"[bold]\n\t == {title_prefix} ==[/bold]")
_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(
features, title="\n\t== Information ==", category=Feature.Category.Info
features,
title="\n\t== Information ==",
category=Feature.Category.Info,
verbose=verbose,
)
_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()
Expand All @@ -636,6 +657,7 @@ async def state(ctx, dev: Device):
_echo_all_features(
child.features,
title_prefix=f"{child.alias} ({child.model}, {child.device_type})",
verbose=verbose,
)

echo()
Expand All @@ -647,7 +669,7 @@ async def state(ctx, dev: Device):
echo(f"\tMAC (rssi): {dev.mac} ({dev.rssi})")
echo(f"\tLocation: {dev.location}")

_echo_all_features(dev.features)
_echo_all_features(dev.features, verbose=verbose)

echo("\n\t[bold]== Modules ==[/bold]")
for module in dev.modules.values():
Expand Down
60 changes: 44 additions & 16 deletions kasa/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import logging
from dataclasses import dataclass
from enum import Enum, auto
from typing import TYPE_CHECKING, Any, Callable
Expand All @@ -10,26 +11,44 @@
from .device import Device


# TODO: This is only useful for Feature, so maybe move to Feature.Type?
class FeatureType(Enum):
"""Type to help decide how to present the feature."""

Sensor = auto()
BinarySensor = auto()
Switch = auto()
Action = auto()
Number = auto()
_LOGGER = logging.getLogger(__name__)


@dataclass
class Feature:
"""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):
"""Category hint for downstreams."""
"""Category hint to allow feature grouping."""

#: 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()
#: Config features change device behavior without immediate state changes.
Config = auto()
Expand Down Expand Up @@ -58,7 +77,7 @@ class Category(Enum):
#: Category hint for downstreams
category: Feature.Category = Category.Unset
#: Type of the feature
type: FeatureType = FeatureType.Sensor
type: Feature.Type = Type.Sensor

# Number-specific attributes
#: Minimum value
Expand Down Expand Up @@ -92,10 +111,19 @@ def __post_init__(self):
else:
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
def value(self):
"""Return the current value."""
if self.type == FeatureType.Action:
if self.type == Feature.Type.Action:
return "<Action>"
if self.attribute_getter is None:
raise ValueError("Not an action and no attribute_getter set")
Expand All @@ -109,15 +137,15 @@ async def set_value(self, value):
"""Set the value."""
if self.attribute_setter is None:
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:
raise ValueError(
f"Value {value} out of range "
f"[{self.minimum_value}, {self.maximum_value}]"
)

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)(value)
Expand All @@ -127,7 +155,7 @@ def __repr__(self):
if self.unit is not None:
s += f" {self.unit}"

if self.type == FeatureType.Number:
if self.type == Feature.Type.Number:
s += f" (range: {self.minimum_value}-{self.maximum_value})"

return s
4 changes: 2 additions & 2 deletions kasa/iot/iotbulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ..bulb import HSV, Bulb, BulbPreset, ColorTempRange
from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig
from ..feature import Feature, FeatureType
from ..feature import Feature
from ..protocol import BaseProtocol
from .iotdevice import IotDevice, KasaException, requires_update
from .modules import Antitheft, Cloud, Countdown, Emeter, Schedule, Time, Usage
Expand Down Expand Up @@ -220,7 +220,7 @@ async def _initialize_features(self):
attribute_setter="set_brightness",
minimum_value=1,
maximum_value=100,
type=FeatureType.Number,
type=Feature.Type.Number,
category=Feature.Category.Primary,
)
)
Expand Down
4 changes: 2 additions & 2 deletions kasa/iot/iotdimmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig
from ..feature import Feature, FeatureType
from ..feature import Feature
from ..protocol import BaseProtocol
from .iotdevice import KasaException, requires_update
from .iotplug import IotPlug
Expand Down Expand Up @@ -96,7 +96,7 @@ async def _initialize_features(self):
attribute_setter="set_brightness",
minimum_value=1,
maximum_value=100,
type=FeatureType.Number,
type=Feature.Type.Number,
)
)

Expand Down
4 changes: 2 additions & 2 deletions kasa/iot/iotplug.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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

Expand Down
4 changes: 2 additions & 2 deletions kasa/iot/modules/ambientlight.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""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

# TODO create tests and use the config reply there
Expand All @@ -25,7 +25,7 @@ def __init__(self, device, module):
name="Ambient Light",
icon="mdi:brightness-percent",
attribute_getter="ambientlight_brightness",
type=FeatureType.Sensor,
type=Feature.Type.Sensor,
)
)

Expand Down
4 changes: 2 additions & 2 deletions kasa/iot/modules/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
except ImportError:
from pydantic import BaseModel

from ...feature import Feature, FeatureType
from ...feature import Feature
from ..iotmodule import IotModule


Expand Down Expand Up @@ -36,7 +36,7 @@ def __init__(self, device, module):
name="Cloud connection",
icon="mdi:cloud",
attribute_getter="is_connected",
type=FeatureType.BinarySensor,
type=Feature.Type.BinarySensor,
)
)

Expand Down
8 changes: 4 additions & 4 deletions kasa/smart/modules/alarmmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import TYPE_CHECKING

from ...feature import Feature, FeatureType
from ...feature import Feature
from ..smartmodule import SmartModule

if TYPE_CHECKING:
Expand Down Expand Up @@ -32,7 +32,7 @@ def __init__(self, device: SmartDevice, module: str):
container=self,
attribute_getter="active",
icon="mdi:bell",
type=FeatureType.BinarySensor,
type=Feature.Type.BinarySensor,
)
)
self._add_feature(
Expand Down Expand Up @@ -60,7 +60,7 @@ def __init__(self, device: SmartDevice, module: str):
"Test alarm",
container=self,
attribute_setter="play",
type=FeatureType.Action,
type=Feature.Type.Action,
)
)
self._add_feature(
Expand All @@ -69,7 +69,7 @@ def __init__(self, device: SmartDevice, module: str):
"Stop alarm",
container=self,
attribute_setter="stop",
type=FeatureType.Action,
type=Feature.Type.Action,
)
)

Expand Down
2 changes: 2 additions & 0 deletions kasa/smart/modules/autooffmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(self, device: SmartDevice, module: str):
container=self,
attribute_getter="enabled",
attribute_setter="set_enabled",
type=Feature.Type.Switch,
)
)
self._add_feature(
Expand All @@ -36,6 +37,7 @@ def __init__(self, device: SmartDevice, module: str):
container=self,
attribute_getter="delay",
attribute_setter="set_delay",
type=Feature.Type.Number,
)
)
self._add_feature(
Expand Down
4 changes: 2 additions & 2 deletions kasa/smart/modules/battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import TYPE_CHECKING

from ...feature import Feature, FeatureType
from ...feature import Feature
from ..smartmodule import SmartModule

if TYPE_CHECKING:
Expand Down Expand Up @@ -35,7 +35,7 @@ def __init__(self, device: SmartDevice, module: str):
container=self,
attribute_getter="battery_low",
icon="mdi:alert",
type=FeatureType.BinarySensor,
type=Feature.Type.BinarySensor,
)
)

Expand Down
4 changes: 2 additions & 2 deletions kasa/smart/modules/brightness.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import TYPE_CHECKING

from ...feature import Feature, FeatureType
from ...feature import Feature
from ..smartmodule import SmartModule

if TYPE_CHECKING:
Expand All @@ -31,7 +31,7 @@ def __init__(self, device: SmartDevice, module: str):
attribute_setter="set_brightness",
minimum_value=BRIGHTNESS_MIN,
maximum_value=BRIGHTNESS_MAX,
type=FeatureType.Number,
type=Feature.Type.Number,
category=Feature.Category.Primary,
)
)
Expand Down
Loading
0