8000 Update cli, light modules, and docs to use FeatureAttributes (#1364) · python-kasa/python-kasa@7709bb9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7709bb9

Browse files
authored
Update cli, light modules, and docs to use FeatureAttributes (#1364)
1 parent f8a46f7 commit 7709bb9

14 files changed

+90
-69
lines changed

docs/tutorial.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
key from :class:`~kasa.Module`.
4141
4242
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.
43-
You can check the availability using ``is_``-prefixed properties like `is_color`.
43+
You can check the availability using ``has_feature()`` method.
4444
4545
>>> from kasa import Module
4646
>>> Module.Light in dev.modules
@@ -52,9 +52,9 @@
5252
>>> await dev.update()
5353
>>> light.brightness
5454
50
55-
>>> light.is_color
55+
>>> light.has_feature("hsv")
5656
True
57-
>>> if light.is_color:
57+
>>> if light.has_feature("hsv"):
5858
>>> print(light.hsv)
5959
HSV(hue=0, saturation=100, value=50)
6060

kasa/cli/light.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ def light(dev) -> None:
2525
@pass_dev_or_child
2626
async def brightness(dev: Device, brightness: int, transition: int):
2727
"""Get or set brightness."""
28-
if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
28+
if not (light := dev.modules.get(Module.Light)) or not light.has_feature(
29+
"brightness"
30+
):
2931
error("This device does not support brightness.")
3032
return
3133

@@ -45,21 +47,23 @@ async def brightness(dev: Device, brightness: int, transition: int):
4547
@pass_dev_or_child
4648
async def temperature(dev: Device, temperature: int, transition: int):
4749
"""Get or set color temperature."""
48-
if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
50+
if not (light := dev.modules.get(Module.Light)) or not (
51+
color_temp_feat := light.get_feature("color_temp")
52+
):
4953
error("Device does not support color temperature")
5054
return
5155

5256
if temperature is None:
5357
echo(f"Color temperature: {light.color_temp}")
54-
valid_temperature_range = light.valid_temperature_range
58+
valid_temperature_range = color_temp_feat.range
5559
if valid_temperature_range != (0, 0):
5660
echo("(min: {}, max: {})".format(*valid_temperature_range))
5761
else:
5862
echo(
5963
"Temperature range unknown, please open a github issue"
6064
f" or a pull request for model '{dev.model}'"
6165
)
62-
return light.valid_temperature_range
66+
return color_temp_feat.range
6367
else:
6468
echo(f"Setting color temperature to {temperature}")
6569
return await light.set_color_temp(temperature, transition=transition)
@@ -99,7 +103,7 @@ async def effect(dev: Device, ctx, effect):
99103
@pass_dev_or_child
100104
async def hsv(dev: Device, ctx, h, s, v, transition):
101105
"""Get or set color in HSV."""
102-
if not (light := dev.modules.get(Module.Light)) or not light.is_color:
106+
if not (light := dev.modules.get(Module.Light)) or not light.has_feature("hsv"):
103107
error("Device does not support colors")
104108
return
105109

kasa/interfaces/light.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
2424
>>> light = dev.modules[Module.Light]
2525
26-
You can use the ``is_``-prefixed properties to check for supported features:
26+
You can use the ``has_feature()`` method to check for supported features:
2727
28-
>>> light.is_dimmable
28+
>>> light.has_feature("brightness")
2929
True
30-
>>> light.is_color
30+
>>> light.has_feature("hsv")
3131
True
32-
>>> light.is_variable_color_temp
32+
>>> light.has_feature("color_temp")
3333
True
3434
3535
All known bulbs support changing the brightness:
@@ -43,8 +43,9 @@
4343
4444
Bulbs supporting color temperature can be queried for the supported range:
4545
46-
>>> light.valid_temperature_range
47-
ColorTempRange(min=2500, max=6500)
46+
>>> if color_temp_feature := light.get_feature("color_temp"):
47+
>>> print(f"{color_temp_feature.minimum_value}, {color_temp_feature.maximum_value}")
48+
2500, 6500
4849
>>> await light.set_color_temp(3000)
4950
>>> await dev.update()
5051
>>> light.color_temp

kasa/interfaces/lighteffect.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
1414
Light effects are accessed via the LightPreset module. To list available presets
1515
16-
>>> if dev.modules[Module.Light].has_effects:
17-
>>> light_effect = dev.modules[Module.LightEffect]
16+
>>> light_effect = dev.modules[Module.LightEffect]
1817
>>> light_effect.effect_list
1918
['Off', 'Party', 'Relax']
2019

kasa/iot/modules/light.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
from __future__ import annotations
44

55
from dataclasses import asdict
6-
from typing import TYPE_CHECKING, cast
6+
from typing import TYPE_CHECKING, Annotated, cast
77

88
from ...device_type import DeviceType
99
from ...exceptions import KasaException
1010
from ...feature import Feature
1111
from ...interfaces.light import HSV, ColorTempRange, LightState
1212
from ...interfaces.light import Light as LightInterface
13+
from ...module import FeatureAttribute
1314
from ..iotmodule import IotModule
1415

1516
if TYPE_CHECKING:
@@ -32,7 +33,7 @@ def _initialize_features(self) -> None:
3233
super()._initialize_features()
3334
device = self._device
3435

35-
if self._device._is_dimmable:
36+
if device._is_dimmable:
3637
self._add_feature(
3738
Feature(
3839
device,
@@ -46,7 +47,7 @@ def _initialize_features(self) -> None:
4647
category=Feature.Category.Primary,
4748
)
4849
)
49-
if self._device._is_variable_color_temp:
50+
if device._is_variable_color_temp:
5051
self._add_feature(
5152
Feature(
5253
device=device,
@@ -60,7 +61,7 @@ def _initialize_features(self) -> None:
6061
type=Feature.Type.Number,
6162
)
6263
)
63-
if self._device._is_color:
64+
if device._is_color:
6465
self._add_feature(
6566
Feature(
6667
device=device,
@@ -95,13 +96,13 @@ def is_dimmable(self) -> int:
9596
return self._device._is_dimmable
9697

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

102103
async def set_brightness(
103104
self, brightness: int, *, transition: int | None = None
104-
) -> dict:
105+
) -> Annotated[dict, FeatureAttribute()]:
105106
"""Set the brightness in percentage. A value of 0 will turn off the light.
106107
107108
:param int brightness: brightness in percent
@@ -133,7 +134,7 @@ def has_effects(self) -> bool:
133134
return bulb._has_effects
134135

135136
@property
136-
def hsv(self) -> HSV:
137+
def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
137138
"""Return the current HSV state of the bulb.
138139
139140
:return: hue, saturation and value (degrees, %, %)
@@ -149,7 +150,7 @@ async def set_hsv(
149150
value: int | None = None,
150151
*,
151152
transition: int | None = None,
152-
) -> dict:
153+
) -> Annotated[dict, FeatureAttribute()]:
153154
"""Set new HSV.
154155
155156
Note, transition is not supported and will be ignored.
@@ -176,7 +177,7 @@ def valid_temperature_range(self) -> ColorTempRange:
176177
return bulb._valid_temperature_range
177178

178179
@property
179-
def color_temp(self) -> int:
180+
def color_temp(self) -> Annotated[int, FeatureAttribute()]:
180181
"""Whether the bulb supports color temperature changes."""
181182
if (
182183
bulb := self._get_bulb_device()
@@ -186,7 +187,7 @@ def color_temp(self) -> int:
186187

187188
async def set_color_temp(
188189
self, temp: int, *, brightness: int | None = None, transition: int | None = None
189-
) -> dict:
190+
) -> Annotated[dict, FeatureAttribute()]:
190191
"""Set the color temperature of the device in kelvin.
191192
192193
Note, transition is not supported and will be ignored.
@@ -242,17 +243,18 @@ def state(self) -> LightState:
242243
return self._light_state
243244

244245
async def _post_update_hook(self) -> None:
245-
if self._device.is_on is False:
246+
device = self._device
247+
if device.is_on is False:
246248
state = LightState(light_on=False)
247249
else:
248250
state = LightState(light_on=True)
249-
if self.is_dimmable:
251+
if device._is_dimmable:
250252
state.brightness = self.brightness
251-
if self.is_color:
253+
if device._is_color:
252254
hsv = self.hsv
253255
state.hue = hsv.hue
254256
state.saturation = hsv.saturation
255-
if self.is_variable_color_temp:
257+
if device._is_variable_color_temp:
256258
state.color_temp = self.color_temp
257259
self._light_state = state
258260

kasa/iot/modules/lightpreset.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,19 @@ def preset_states_list(self) -> Sequence[IotLightPreset]:
8585
def preset(self) -> str:
8686
"""Return current preset name."""
8787
light = self._device.modules[Module.Light]
88+
is_color = light.has_feature("hsv")
89+
is_variable_color_temp = light.has_feature("color_temp")
90+
8891
brightness = light.brightness
89-
color_temp = light.color_temp if light.is_variable_color_temp else None
90-
h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
92+
color_temp = light.color_temp if is_variable_color_temp else None
93+
94+
h, s = (light.hsv.hue, light.hsv.saturation) if is_color else (None, None)
9195
for preset_name, preset in self._presets.items():
9296
if (
9397
preset.brightness == brightness
94-
and (
95-
preset.color_temp == color_temp or not light.is_variable_color_temp
96-
)
97-
and (preset.hue == h or not light.is_color)
98-
and (preset.saturation == s or not light.is_color)
98+
and (preset.color_temp == color_temp or not is_variable_color_temp)
99+
and (preset.hue == h or not is_color)
100+
and (preset.saturation == s or not is_color)
99101
):
100102
return preset_name
101103
return self.PRESET_NOT_SET
@@ -107,7 +109,7 @@ async def set_preset(
107109
"""Set a light preset for the device."""
108110
light = self._device.modules[Module.Light]
109111
if preset_name == self.PRESET_NOT_SET:
110-
if light.is_color:
112+
if light.has_feature("hsv"):
111113
preset = LightState(hue=0, saturation=0, brightness=100)
112114
else:
113115
preset = LightState(brightness=100)

kasa/smart/modules/light.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def valid_temperature_range(self) -> ColorTempRange:
5555
5656
:return: White temperature range in Kelvin (minimum, maximum)
5757
"""
58-
if not self.is_variable_color_temp:
58+
if Module.ColorTemperature not in self._device.modules:
5959
raise KasaException("Color temperature not supported")
6060

6161
return self._device.modules[Module.ColorTemperature].valid_temperature_range
@@ -66,23 +66,23 @@ def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
6666
6767
:return: hue, saturation and value (degrees, %, %)
6868
"""
69-
if not self.is_color:
69+
if Module.Color not in self._device.modules:
7070
raise KasaException("Bulb does not support color.")
7171

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

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

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

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

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

110110
return await self._device.modules[Module.Color].set_hsv(hue, saturation, value)
@@ -119,7 +119,7 @@ async def set_color_temp(
119119
:param int temp: The new color temperature, in Kelvin
120120
:param int transition: transition in milliseconds.
121121
"""
122-
if not self.is_variable_color_temp:
122+
if Module.ColorTemperature not in self._device.modules:
123123
raise KasaException("Bulb does not support colortemp.")
124124
return await self._device.modules[Module.ColorTemperature].set_color_temp(
125125
temp, brightness=brightness
@@ -135,7 +135,7 @@ async def set_brightness(
135135
:param int brightness: brightness in percent
136136
:param int transition: transition in milliseconds.
137137
"""
138-
if not self.is_dimmable: # pragma: no cover
138+
if Module.Brightness not in self._device.modules:
139139
raise KasaException("Bulb is not dimmable.")
140140

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

169169
async def _post_update_hook(self) -> None:
170-
if self._device.is_on is False:
170+
device = self._device
171+
if device.is_on is False:
171172
state = LightState(light_on=False)
172173
else:
173174
state = LightState(light_on=True)
174-
if self.is_dimmable:
175+
if Module.Brightness in device.modules:
175176
state.brightness = self.brightness
176-
if self.is_color:
177+
if Module.Color in device.modules:
177178
hsv = self.hsv
178179
state.hue = hsv.hue
179180
state.saturation = hsv.saturation
180-
if self.is_variable_color_temp:
181+
if Module.ColorTemperature in device.modules:
181182
state.color_temp = self.color_temp
182183
self._light_state = state

kasa/smart/modules/lightpreset.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,18 @@ def preset(self) -> str:
9696
"""Return current preset name."""
9797
light = self._device.modules[SmartModule.Light]
9898
brightness = light.brightness
99-
color_temp = light.color_temp if light.is_variable_color_temp else None
100-
h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
99+
color_temp = light.color_temp if light.has_feature("color_temp") else None
100+
h, s = (
101+
(light.hsv.hue, light.hsv.saturation)
102+
if light.has_feature("hsv")
103+
else (None, None)
104+
)
101105
for preset_name, preset in self._presets.items():
102106
if (
103107
preset.brightness == brightness
104108
and (
105-
preset.color_temp == color_temp or not light.is_variable_color_temp
109+
preset.color_temp == color_temp
110+
or not light.has_feature("color_temp")
106111
)
107112
and preset.hue == h
108113
and preset.saturation == s
@@ -117,7 +122,7 @@ async def set_preset(
117122
"""Set a light preset for the device."""
118123
light = self._device.modules[SmartModule.Light]
119124
if preset_name == self.PRESET_NOT_SET:
120-
if light.is_color:
125+
if light.has_feature("hsv"):
121126
preset = LightState(hue=0, saturation=0, brightness=100)
122127
else:
123128
preset = LightState(brightness=100)

tests/iot/test_iotbulb.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog):
9191
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
9292
light = dev.modules.get(Module.Light)
9393
assert light
94-
assert light.valid_temperature_range == (2700, 5000)
94+
color_temp_feat = light.get_feature("color_temp")
95+
assert color_temp_feat
96+
assert color_temp_feat.range == (2700, 5000)
9597
assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text
9698

9799

tests/smart/test_smartdevice.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,9 @@ async def side_effect_func(*args, **kwargs):
469469
async def test_smart_temp_range(dev: Device):
470470
light = dev.modules.get(Module.Light)
471471
assert light
472-
assert light.valid_temperature_range
472+
color_temp_feat = light.get_feature("color_temp")
473+
assert color_temp_feat
474+
assert color_temp_feat.range
473475

474476

475477
@device_smart

0 commit comments

Comments
 (0)
0