8000 Update cli, light modules, and docs to use FeatureAttributes by sdb9696 · Pull Request #1364 · python-kasa/python-kasa · GitHub
[go: up one dir, main page]

Skip to content

Update cli, light modules, and docs to use FeatureAttributes #1364

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 4 commits into from
Dec 11, 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
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
key from :class:`~kasa.Module`.

Modules will only be available on the device if they are supported but some individual features of a module may not be available for your device.
You can check the availability using ``is_``-prefixed properties like `is_color`.
You can check the availability using ``has_feature()`` method.

>>> from kasa import Module
>>> Module.Light in dev.modules
Expand All @@ -52,9 +52,9 @@
>>> await dev.update()
>>> light.brightness
50
>>> light.is_color
>>> light.has_feature("hsv")
True
>>> if light.is_color:
>>> if light.has_feature("hsv"):
>>> print(light.hsv)
HSV(hue=0, saturation=100, value=50)

Expand Down
14 changes: 9 additions & 5 deletions kasa/cli/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def light(dev) -> None:
@pass_dev_or_child
async def brightness(dev: Device, brightness: int, transition: int):
"""Get or set brightness."""
if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
if not (light := dev.modules.get(Module.Light)) or not light.has_feature(
"brightness"
):
error("This device does not support brightness.")
return

Expand All @@ -45,21 +47,23 @@ async def brightness(dev: Device, brightness: int, transition: int):
@pass_dev_or_child
async def temperature(dev: Device, temperature: int, transition: int):
"""Get or set color temperature."""
if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
if not (light := dev.modules.get(Module.Light)) or not (
color_temp_feat := light.get_feature("color_temp")
):
error("Device does not support color temperature")
return

if temperature is None:
echo(f"Color temperature: {light.color_temp}")
valid_temperature_range = light.valid_temperature_range
valid_temperature_range = color_temp_feat.range
if valid_temperature_range != (0, 0):
echo("(min: {}, max: {})".format(*valid_temperature_range))
else:
echo(
"Temperature range unknown, please open a github issue"
f" or a pull request for model '{dev.model}'"
)
return light.valid_temperature_range
return color_temp_feat.range
else:
echo(f"Setting color temperature to {temperature}")
return await light.set_color_temp(temperature, transition=transition)
Expand Down Expand Up @@ -99,7 +103,7 @@ async def effect(dev: Device, ctx, effect):
@pass_dev_or_child
async def hsv(dev: Device, ctx, h, s, v, transition):
"""Get or set color in HSV."""
if not (light := dev.modules.get(Module.Light)) or not light.is_color:
if not (light := dev.modules.get(Module.Light)) or not light.has_feature("hsv"):
error("Device does not support colors")
return

Expand Down
13 changes: 7 additions & 6 deletions kasa/interfaces/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@

>>> light = dev.modules[Module.Light]

You can use the ``is_``-prefixed properties to check for supported features:
You can use the ``has_feature()`` method to check for supported features:

>>> light.is_dimmable
>>> light.has_feature("brightness")
True
>>> light.is_color
>>> light.has_feature("hsv")
True
>>> light.is_variable_color_temp
>>> light.has_feature("color_temp")
True

All known bulbs support changing the brightness:
Expand All @@ -43,8 +43,9 @@

Bulbs supporting color temperature can be queried for the supported range:

>>> light.valid_temperature_range
ColorTempRange(min=2500, max=6500)
>>> if color_temp_feature := light.get_feature("color_temp"):
>>> print(f"{color_temp_feature.minimum_value}, {color_temp_feature.maximum_value}")
2500, 6500
>>> await light.set_color_temp(3000)
>>> await dev.update()
>>> light.color_temp
Expand Down
3 changes: 1 addition & 2 deletions kasa/interfaces/lighteffect.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@

Light effects are accessed via the LightPreset module. To list available presets

>>> if dev.modules[Module.Light].has_effects:
>>> light_effect = dev.modules[Module.LightEffect]
>>> light_effect = dev.modules[Module.LightEffect]
>>> light_effect.effect_list
['Off', 'Party', 'Relax']

Expand Down
30 changes: 16 additions & 14 deletions kasa/iot/modules/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
from __future__ import annotations

from dataclasses import asdict
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, Annotated, cast

from ...device_type import DeviceType
from ...exceptions import KasaException
from ...feature import Feature
from ...interfaces.light import HSV, ColorTempRange, LightState
from ...interfaces.light import Light as LightInterface
from ...module import FeatureAttribute
from ..iotmodule import IotModule

if TYPE_CHECKING:
Expand All @@ -32,7 +33,7 @@ def _initialize_features(self) -> None:
super()._initialize_features()
device = self._device

if self._device._is_dimmable:
if device._is_dimmable:
self._add_feature(
Feature(
device,
Expand All @@ -46,7 +47,7 @@ def _initialize_features(self) -> None:
category=Feature.Category.Primary,
)
)
if self._device._is_variable_color_temp:
if device._is_variable_color_temp:
self._add_feature(
Feature(
device=device,
Expand All @@ -60,7 +61,7 @@ def _initialize_features(self) -> None:
type=Feature.Type.Number,
)
)
if self._device._is_color:
if device._is_color:
self._add_feature(
Feature(
device=device,
Expand Down Expand Up @@ -95,13 +96,13 @@ def is_dimmable(self) -> int:
return self._device._is_dimmable

@property # type: ignore
def brightness(self) -> int:
def brightness(self) -> Annotated[int, FeatureAttribute()]:
"""Return the current brightness in percentage."""
return self._device._brightness

async def set_brightness(
self, brightness: int, *, transition: int | None = None
) -> dict:
) -> Annotated[dict, FeatureAttribute()]:
"""Set the brightness in percentage. A value of 0 will turn off the light.

:param int brightness: brightness in percent
Expand Down Expand Up @@ -133,7 +134,7 @@ def has_effects(self) -> bool:
return bulb._has_effects

@property
def hsv(self) -> HSV:
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
"""Return the current HSV state of the bulb.

:return: hue, saturation and value (degrees, %, %)
Expand All @@ -149,7 +150,7 @@ async def set_hsv(
value: int | None = None,
*,
transition: int | None = None,
) -> dict:
) -> Annotated[dict, FeatureAttribute()]:
"""Set new HSV.

Note, transition is not supported and will be ignored.
Expand All @@ -176,7 +177,7 @@ def valid_temperature_range(self) -> ColorTempRange:
return bulb._valid_temperature_range

@property
def color_temp(self) -> int:
def color_temp(self) -> Annotated[int, FeatureAttribute()]:
"""Whether the bulb supports color temperature changes."""
if (
bulb := self._get_bulb_device()
Expand All @@ -186,7 +187,7 @@ def color_temp(self) -> int:

async def set_color_temp(
self, temp: int, *, brightness: int | None = None, transition: int | None = None
) -> dict:
) -> Annotated[dict, FeatureAttribute()]:
"""Set the color temperature of the device in kelvin.

Note, transition is not supported and will be ignored.
Expand Down Expand Up @@ -242,17 +243,18 @@ def state(self) -> LightState:
return self._light_state

async def _post_update_hook(self) -> None:
if self._device.is_on is False:
device = self._device
if device.is_on is False:
state = LightState(light_on=False)
else:
state = LightState(light_on=True)
if self.is_dimmable:
if device._is_dimmable:
state.brightness = self.brightness
if self.is_color:
if device._is_color:
hsv = self.hsv
state.hue = hsv.hue
state.saturation = hsv.saturation
if self.is_variable_color_temp:
if device._is_variable_color_temp:
state.color_temp = self.color_temp
self._light_state = state

Expand Down
18 changes: 10 additions & 8 deletions kasa/iot/modules/lightpreset.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,19 @@ def preset_states_list(self) -> Sequence[IotLightPreset]:
def preset(self) -> str:
"""Return current preset name."""
light = self._device.modules[Module.Light]
is_color = light.has_feature("hsv")
is_variable_color_temp = light.has_feature("color_temp")

brightness = light.brightness
color_temp = light.color_temp if light.is_variable_color_temp else None
h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
color_temp = light.color_temp if is_variable_color_temp else None

h, s = (light.hsv.hue, light.hsv.saturation) if is_color else (None, None)
for preset_name, preset in self._presets.items():
if (
preset.brightness == brightness
and (
preset.color_temp == color_temp or not light.is_variable_color_temp
)
and (preset.hue == h or not light.is_color)
and (preset.saturation == s or not light.is_color)
and (preset.color_temp == color_temp or not is_variable_color_temp)
and (preset.hue == h or not is_color)
and (preset.saturation == s or not is_color)
):
return preset_name
return self.PRESET_NOT_SET
Expand All @@ -107,7 +109,7 @@ async def set_preset(
"""Set a light preset for the device."""
light = self._device.modules[Module.Light]
if preset_name == self.PRESET_NOT_SET:
if light.is_color:
if light.has_feature("hsv"):
preset = LightState(hue=0, saturation=0, brightness=100)
else:
preset = LightState(brightness=100)
Expand Down
23 changes: 12 additions & 11 deletions kasa/smart/modules/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def valid_temperature_range(self) -> ColorTempRange:

:return: White temperature range in Kelvin (minimum, maximum)
"""
if not self.is_variable_color_temp:
if Module.ColorTemperature not in self._device.modules:
raise KasaException("Color temperature not supported")

return self._device.modules[Module.ColorTemperature].valid_temperature_range
Expand All @@ -66,23 +66,23 @@ def hsv(self) -> Annotated[HSV, FeatureAttribute()]:

:return: hue, saturation and value (degrees, %, %)
"""
if not self.is_color:
if Module.Color not in self._device.modules:
raise KasaException("Bulb does not support color.")

return self._device.modules[Module.Color].hsv

@property
def color_temp(self) -> Annotated[int, FeatureAttribute()]:
"""Whether the bulb supports color temperature changes."""
if not self.is_variable_color_temp:
if Module.ColorTemperature not in self._device.modules:
raise KasaException("Bulb does not support colortemp.")

return self._device.modules[Module.ColorTemperature].color_temp

@property
def brightness(self) -> Annotated[int, FeatureAttribute()]:
"""Return the current brightness in percentage."""
if not self.is_dimmable: # pragma: no cover
if Module.Brightness not in self._device.modules:
raise KasaException("Bulb is not dimmable.")

return self._device.modules[Module.Brightness].brightness
Expand All @@ -104,7 +104,7 @@ async def set_hsv(
:param int value: value between 1 and 100
:param int transition: transition in milliseconds.
"""
if not self.is_color:
if Module.Color not in self._device.modules:
raise KasaException("Bulb does not support color.")

return await self._device.modules[Module.Color].set_hsv(hue, saturation, value)
Expand All @@ -119,7 +119,7 @@ async def set_color_temp(
:param int temp: The new color temperature, in Kelvin
:param int transition: transition in milliseconds.
"""
if not self.is_variable_color_temp:
if Module.ColorTemperature not in self._device.modules:
raise KasaException("Bulb does not support colortemp.")
return await self._device.modules[Module.ColorTemperature].set_color_temp(
temp, brightness=brightness
Expand All @@ -135,7 +135,7 @@ async def set_brightness(
:param int brightness: brightness in percent
:param int transition: transition in milliseconds.
"""
if not self.is_dimmable: # pragma: no cover
if Module.Brightness not in self._device.modules:
raise KasaException("Bulb is not dimmable.")

return await self._device.modules[Module.Brightness].set_brightness(brightness)
Expand Down Expand Up @@ -167,16 +167,17 @@ def state(self) -> LightState:
return self._light_state

async def _post_update_hook(self) -> None:
if self._device.is_on is False:
device = self._device
if device.is_on is False:
state = LightState(light_on=False)
else:
state = LightState(light_on=True)
if self.is_dimmable:
if Module.Brightness in device.modules:
state.brightness = self.brightness
if self.is_color:
if Module.Color in device.modules:
hsv = self.hsv
state.hue = hsv.hue
state.saturation = hsv.saturation
if self.is_variable_color_temp:
if Module.ColorTemperature in device.modules:
state.color_temp = self.color_temp
self._light_state = state
13 changes: 9 additions & 4 deletions kasa/smart/modules/lightpreset.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,18 @@ def preset(self) -> str:
"""Return current preset name."""
light = self._device.modules[SmartModule.Light]
brightness = light.brightness
color_temp = light.color_temp if light.is_variable_color_temp else None
h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
color_temp = light.color_temp if light.has_feature("color_temp") else None
h, s = (
(light.hsv.hue, light.hsv.saturation)
if light.has_feature("hsv")
else (None, None)
)
for preset_name, preset in self._presets.items():
if (
preset.brightness == brightness
and (
preset.color_temp == color_temp or not light.is_variable_color_temp
preset.color_temp == color_temp
or not light.has_feature("color_temp")
)
and preset.hue == h
and preset.saturation == s
Expand All @@ -117,7 +122,7 @@ async def set_preset(
"""Set a light preset for the device."""
light = self._device.modules[SmartModule.Light]
if preset_name == self.PRESET_NOT_SET:
if light.is_color:
if light.has_feature("hsv"):
preset = LightState(hue=0, saturation=0, brightness=100)
else:
preset = LightState(brightness=100)
Expand Down
4 changes: 3 additions & 1 deletion tests/iot/test_iotbulb.py
< BCFB td class="blob-code blob-code-context js-file-line"> assert light
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog):
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
light = dev.modules.get(Module.Light)
assert light.valid_temperature_range == (2700, 5000)
color_temp_feat = light.get_feature("color_temp")
assert color_temp_feat
assert color_temp_feat.range == (2700, 5000)
assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text


Expand Down
4 changes: 3 additions & 1 deletion tests/smart/test_smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,9 @@ async def side_effect_func(*args, **kwargs):
async def test_smart_temp_range(dev: Device):
light = dev.modules.get(Module.Light)
assert light
assert light.valid_temperature_range
color_temp_feat = light.get_feature("color_temp")
assert color_temp_feat
assert color_temp_feat.range


@device_smart
Expand Down
Loading
Loading
0