Update cli, light modules, and docs to use FeatureAttributes (#1364)
Some checks are pending
CI / Perform linting checks (3.13) (push) Waiting to run
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.13) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.13) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.13) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.11) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.12) (push) Blocked by required conditions
CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.13) (push) Blocked by required conditions
CodeQL checks / Analyze (python) (push) Waiting to run

This commit is contained in:
Steven B. 2024-12-11 15:53:35 +00:00 committed by GitHub
parent f8a46f74cd
commit 7709bb967f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 90 additions and 69 deletions

View File

@ -40,7 +40,7 @@ Different groups of functionality are supported by modules which you can access
key from :class:`~kasa.Module`. 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. 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 >>> from kasa import Module
>>> Module.Light in dev.modules >>> Module.Light in dev.modules
@ -52,9 +52,9 @@ True
>>> await dev.update() >>> await dev.update()
>>> light.brightness >>> light.brightness
50 50
>>> light.is_color >>> light.has_feature("hsv")
True True
>>> if light.is_color: >>> if light.has_feature("hsv"):
>>> print(light.hsv) >>> print(light.hsv)
HSV(hue=0, saturation=100, value=50) HSV(hue=0, saturation=100, value=50)

View File

@ -25,7 +25,9 @@ def light(dev) -> None:
@pass_dev_or_child @pass_dev_or_child
async def brightness(dev: Device, brightness: int, transition: int): async def brightness(dev: Device, brightness: int, transition: int):
"""Get or set brightness.""" """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.") error("This device does not support brightness.")
return return
@ -45,13 +47,15 @@ async def brightness(dev: Device, brightness: int, transition: int):
@pass_dev_or_child @pass_dev_or_child
async def temperature(dev: Device, temperature: int, transition: int): async def temperature(dev: Device, temperature: int, transition: int):
"""Get or set color temperature.""" """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") error("Device does not support color temperature")
return return
if temperature is None: if temperature is None:
echo(f"Color temperature: {light.color_temp}") 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): if valid_temperature_range != (0, 0):
echo("(min: {}, max: {})".format(*valid_temperature_range)) echo("(min: {}, max: {})".format(*valid_temperature_range))
else: else:
@ -59,7 +63,7 @@ async def temperature(dev: Device, temperature: int, transition: int):
"Temperature range unknown, please open a github issue" "Temperature range unknown, please open a github issue"
f" or a pull request for model '{dev.model}'" f" or a pull request for model '{dev.model}'"
) )
return light.valid_temperature_range return color_temp_feat.range
else: else:
echo(f"Setting color temperature to {temperature}") echo(f"Setting color temperature to {temperature}")
return await light.set_color_temp(temperature, transition=transition) return await light.set_color_temp(temperature, transition=transition)
@ -99,7 +103,7 @@ async def effect(dev: Device, ctx, effect):
@pass_dev_or_child @pass_dev_or_child
async def hsv(dev: Device, ctx, h, s, v, transition): async def hsv(dev: Device, ctx, h, s, v, transition):
"""Get or set color in HSV.""" """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") error("Device does not support colors")
return return

View File

@ -23,13 +23,13 @@ Get the light module to interact:
>>> light = dev.modules[Module.Light] >>> 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 True
>>> light.is_color >>> light.has_feature("hsv")
True True
>>> light.is_variable_color_temp >>> light.has_feature("color_temp")
True True
All known bulbs support changing the brightness: All known bulbs support changing the brightness:
@ -43,8 +43,9 @@ All known bulbs support changing the brightness:
Bulbs supporting color temperature can be queried for the supported range: Bulbs supporting color temperature can be queried for the supported range:
>>> light.valid_temperature_range >>> if color_temp_feature := light.get_feature("color_temp"):
ColorTempRange(min=2500, max=6500) >>> print(f"{color_temp_feature.minimum_value}, {color_temp_feature.maximum_value}")
2500, 6500
>>> await light.set_color_temp(3000) >>> await light.set_color_temp(3000)
>>> await dev.update() >>> await dev.update()
>>> light.color_temp >>> light.color_temp

View File

@ -13,7 +13,6 @@ Living Room Bulb
Light effects are accessed via the LightPreset module. To list available presets 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 >>> light_effect.effect_list
['Off', 'Party', 'Relax'] ['Off', 'Party', 'Relax']

View File

@ -3,13 +3,14 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import asdict from dataclasses import asdict
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, Annotated, cast
from ...device_type import DeviceType from ...device_type import DeviceType
from ...exceptions import KasaException from ...exceptions import KasaException
from ...feature import Feature from ...feature import Feature
from ...interfaces.light import HSV, ColorTempRange, LightState from ...interfaces.light import HSV, ColorTempRange, LightState
from ...interfaces.light import Light as LightInterface from ...interfaces.light import Light as LightInterface
from ...module import FeatureAttribute
from ..iotmodule import IotModule from ..iotmodule import IotModule
if TYPE_CHECKING: if TYPE_CHECKING:
@ -32,7 +33,7 @@ class Light(IotModule, LightInterface):
super()._initialize_features() super()._initialize_features()
device = self._device device = self._device
if self._device._is_dimmable: if device._is_dimmable:
self._add_feature( self._add_feature(
Feature( Feature(
device, device,
@ -46,7 +47,7 @@ class Light(IotModule, LightInterface):
category=Feature.Category.Primary, category=Feature.Category.Primary,
) )
) )
if self._device._is_variable_color_temp: if device._is_variable_color_temp:
self._add_feature( self._add_feature(
Feature( Feature(
device=device, device=device,
@ -60,7 +61,7 @@ class Light(IotModule, LightInterface):
type=Feature.Type.Number, type=Feature.Type.Number,
) )
) )
if self._device._is_color: if device._is_color:
self._add_feature( self._add_feature(
Feature( Feature(
device=device, device=device,
@ -95,13 +96,13 @@ class Light(IotModule, LightInterface):
return self._device._is_dimmable return self._device._is_dimmable
@property # type: ignore @property # type: ignore
def brightness(self) -> int: def brightness(self) -> Annotated[int, FeatureAttribute()]:
"""Return the current brightness in percentage.""" """Return the current brightness in percentage."""
return self._device._brightness return self._device._brightness
async def set_brightness( async def set_brightness(
self, brightness: int, *, transition: int | None = None 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. """Set the brightness in percentage. A value of 0 will turn off the light.
:param int brightness: brightness in percent :param int brightness: brightness in percent
@ -133,7 +134,7 @@ class Light(IotModule, LightInterface):
return bulb._has_effects return bulb._has_effects
@property @property
def hsv(self) -> HSV: def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
"""Return the current HSV state of the bulb. """Return the current HSV state of the bulb.
:return: hue, saturation and value (degrees, %, %) :return: hue, saturation and value (degrees, %, %)
@ -149,7 +150,7 @@ class Light(IotModule, LightInterface):
value: int | None = None, value: int | None = None,
*, *,
transition: int | None = None, transition: int | None = None,
) -> dict: ) -> Annotated[dict, FeatureAttribute()]:
"""Set new HSV. """Set new HSV.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -176,7 +177,7 @@ class Light(IotModule, LightInterface):
return bulb._valid_temperature_range return bulb._valid_temperature_range
@property @property
def color_temp(self) -> int: def color_temp(self) -> Annotated[int, FeatureAttribute()]:
"""Whether the bulb supports color temperature changes.""" """Whether the bulb supports color temperature changes."""
if ( if (
bulb := self._get_bulb_device() bulb := self._get_bulb_device()
@ -186,7 +187,7 @@ class Light(IotModule, LightInterface):
async def set_color_temp( async def set_color_temp(
self, temp: int, *, brightness: int | None = None, transition: int | None = None self, temp: int, *, brightness: int | None = None, transition: int | None = None
) -> dict: ) -> Annotated[dict, FeatureAttribute()]:
"""Set the color temperature of the device in kelvin. """Set the color temperature of the device in kelvin.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -242,17 +243,18 @@ class Light(IotModule, LightInterface):
return self._light_state return self._light_state
async def _post_update_hook(self) -> None: 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) state = LightState(light_on=False)
else: else:
state = LightState(light_on=True) state = LightState(light_on=True)
if self.is_dimmable: if device._is_dimmable:
state.brightness = self.brightness state.brightness = self.brightness
if self.is_color: if device._is_color:
hsv = self.hsv hsv = self.hsv
state.hue = hsv.hue state.hue = hsv.hue
state.saturation = hsv.saturation state.saturation = hsv.saturation
if self.is_variable_color_temp: if device._is_variable_color_temp:
state.color_temp = self.color_temp state.color_temp = self.color_temp
self._light_state = state self._light_state = state

View File

@ -85,17 +85,19 @@ class LightPreset(IotModule, LightPresetInterface):
def preset(self) -> str: def preset(self) -> str:
"""Return current preset name.""" """Return current preset name."""
light = self._device.modules[Module.Light] light = self._device.modules[Module.Light]
is_color = light.has_feature("hsv")
is_variable_color_temp = light.has_feature("color_temp")
brightness = light.brightness brightness = light.brightness
color_temp = light.color_temp if light.is_variable_color_temp else None color_temp = light.color_temp if is_variable_color_temp else None
h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, None)
h, s = (light.hsv.hue, light.hsv.saturation) if is_color else (None, None)
for preset_name, preset in self._presets.items(): for preset_name, preset in self._presets.items():
if ( if (
preset.brightness == brightness preset.brightness == brightness
and ( and (preset.color_temp == color_temp or not is_variable_color_temp)
preset.color_temp == color_temp or not light.is_variable_color_temp and (preset.hue == h or not is_color)
) and (preset.saturation == s or not is_color)
and (preset.hue == h or not light.is_color)
and (preset.saturation == s or not light.is_color)
): ):
return preset_name return preset_name
return self.PRESET_NOT_SET return self.PRESET_NOT_SET
@ -107,7 +109,7 @@ class LightPreset(IotModule, LightPresetInterface):
"""Set a light preset for the device.""" """Set a light preset for the device."""
light = self._device.modules[Module.Light] light = self._device.modules[Module.Light]
if preset_name == self.PRESET_NOT_SET: if preset_name == self.PRESET_NOT_SET:
if light.is_color: if light.has_feature("hsv"):
preset = LightState(hue=0, saturation=0, brightness=100) preset = LightState(hue=0, saturation=0, brightness=100)
else: else:
preset = LightState(brightness=100) preset = LightState(brightness=100)

View File

@ -55,7 +55,7 @@ class Light(SmartModule, LightInterface):
:return: White temperature range in Kelvin (minimum, maximum) :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") raise KasaException("Color temperature not supported")
return self._device.modules[Module.ColorTemperature].valid_temperature_range return self._device.modules[Module.ColorTemperature].valid_temperature_range
@ -66,7 +66,7 @@ class Light(SmartModule, LightInterface):
:return: hue, saturation and value (degrees, %, %) :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.") raise KasaException("Bulb does not support color.")
return self._device.modules[Module.Color].hsv return self._device.modules[Module.Color].hsv
@ -74,7 +74,7 @@ class Light(SmartModule, LightInterface):
@property @property
def color_temp(self) -> Annotated[int, FeatureAttribute()]: def color_temp(self) -> Annotated[int, FeatureAttribute()]:
"""Whether the bulb supports color temperature changes.""" """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.") raise KasaException("Bulb does not support colortemp.")
return self._device.modules[Module.ColorTemperature].color_temp return self._device.modules[Module.ColorTemperature].color_temp
@ -82,7 +82,7 @@ class Light(SmartModule, LightInterface):
@property @property
def brightness(self) -> Annotated[int, FeatureAttribute()]: def brightness(self) -> Annotated[int, FeatureAttribute()]:
"""Return the current brightness in percentage.""" """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.") raise KasaException("Bulb is not dimmable.")
return self._device.modules[Module.Brightness].brightness return self._device.modules[Module.Brightness].brightness
@ -104,7 +104,7 @@ class Light(SmartModule, LightInterface):
:param int value: value between 1 and 100 :param int value: value between 1 and 100
:param int transition: transition in milliseconds. :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.") raise KasaException("Bulb does not support color.")
return await self._device.modules[Module.Color].set_hsv(hue, saturation, value) return await self._device.modules[Module.Color].set_hsv(hue, saturation, value)
@ -119,7 +119,7 @@ class Light(SmartModule, LightInterface):
:param int temp: The new color temperature, in Kelvin :param int temp: The new color temperature, in Kelvin
:param int transition: transition in milliseconds. :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.") raise KasaException("Bulb does not support colortemp.")
return await self._device.modules[Module.ColorTemperature].set_color_temp( return await self._device.modules[Module.ColorTemperature].set_color_temp(
temp, brightness=brightness temp, brightness=brightness
@ -135,7 +135,7 @@ class Light(SmartModule, LightInterface):
:param int brightness: brightness in percent :param int brightness: brightness in percent
:param int transition: transition in milliseconds. :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.") raise KasaException("Bulb is not dimmable.")
return await self._device.modules[Module.Brightness].set_brightness(brightness) return await self._device.modules[Module.Brightness].set_brightness(brightness)
@ -167,16 +167,17 @@ class Light(SmartModule, LightInterface):
return self._light_state return self._light_state
async def _post_update_hook(self) -> None: 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) state = LightState(light_on=False)
else: else:
state = LightState(light_on=True) state = LightState(light_on=True)
if self.is_dimmable: if Module.Brightness in device.modules:
state.brightness = self.brightness state.brightness = self.brightness
if self.is_color: if Module.Color in device.modules:
hsv = self.hsv hsv = self.hsv
state.hue = hsv.hue state.hue = hsv.hue
state.saturation = hsv.saturation state.saturation = hsv.saturation
if self.is_variable_color_temp: if Module.ColorTemperature in device.modules:
state.color_temp = self.color_temp state.color_temp = self.color_temp
self._light_state = state self._light_state = state

View File

@ -96,13 +96,18 @@ class LightPreset(SmartModule, LightPresetInterface):
"""Return current preset name.""" """Return current preset name."""
light = self._device.modules[SmartModule.Light] light = self._device.modules[SmartModule.Light]
brightness = light.brightness brightness = light.brightness
color_temp = light.color_temp if light.is_variable_color_temp else None color_temp = light.color_temp if light.has_feature("color_temp") else None
h, s = (light.hsv.hue, light.hsv.saturation) if light.is_color else (None, 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(): for preset_name, preset in self._presets.items():
if ( if (
preset.brightness == brightness preset.brightness == brightness
and ( 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.hue == h
and preset.saturation == s and preset.saturation == s
@ -117,7 +122,7 @@ class LightPreset(SmartModule, LightPresetInterface):
"""Set a light preset for the device.""" """Set a light preset for the device."""
light = self._device.modules[SmartModule.Light] light = self._device.modules[SmartModule.Light]
if preset_name == self.PRESET_NOT_SET: if preset_name == self.PRESET_NOT_SET:
if light.is_color: if light.has_feature("hsv"):
preset = LightState(hue=0, saturation=0, brightness=100) preset = LightState(hue=0, saturation=0, brightness=100)
else: else:
preset = LightState(brightness=100) preset = LightState(brightness=100)

View File

@ -91,7 +91,9 @@ async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog):
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb") monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
light = dev.modules.get(Module.Light) light = dev.modules.get(Module.Light)
assert light assert 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 assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text

View File

@ -469,7 +469,9 @@ async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixt
async def test_smart_temp_range(dev: Device): async def test_smart_temp_range(dev: Device):
light = dev.modules.get(Module.Light) light = dev.modules.get(Module.Light)
assert 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 @device_smart

View File

@ -25,7 +25,7 @@ async def test_hsv(dev: Device, turn_on):
light = dev.modules.get(Module.Light) light = dev.modules.get(Module.Light)
assert light assert light
await handle_turn_on(dev, turn_on) await handle_turn_on(dev, turn_on)
assert light.is_color assert light.has_feature("hsv")
hue, saturation, brightness = light.hsv hue, saturation, brightness = light.hsv
assert 0 <= hue <= 360 assert 0 <= hue <= 360
@ -106,7 +106,7 @@ async def test_invalid_hsv(
light = dev.modules.get(Module.Light) light = dev.modules.get(Module.Light)
assert light assert light
await handle_turn_on(dev, turn_on) await handle_turn_on(dev, turn_on)
assert light.is_color assert light.has_feature("hsv")
with pytest.raises(exception_cls, match=error): with pytest.raises(exception_cls, match=error):
await light.set_hsv(hue, sat, brightness) await light.set_hsv(hue, sat, brightness)
@ -124,7 +124,7 @@ async def test_color_state_information(dev: Device):
async def test_hsv_on_non_color(dev: Device): async def test_hsv_on_non_color(dev: Device):
light = dev.modules.get(Module.Light) light = dev.modules.get(Module.Light)
assert light assert light
assert not light.is_color assert not light.has_feature("hsv")
with pytest.raises(KasaException): with pytest.raises(KasaException):
await light.set_hsv(0, 0, 0) await light.set_hsv(0, 0, 0)
@ -173,9 +173,6 @@ async def test_non_variable_temp(dev: Device):
with pytest.raises(KasaException): with pytest.raises(KasaException):
await light.set_color_temp(2700) await light.set_color_temp(2700)
with pytest.raises(KasaException):
print(light.valid_temperature_range)
with pytest.raises(KasaException): with pytest.raises(KasaException):
print(light.color_temp) print(light.color_temp)

View File

@ -12,6 +12,7 @@ from pytest_mock import MockerFixture
from kasa import ( from kasa import (
AuthenticationError, AuthenticationError,
ColorTempRange,
Credentials, Credentials,
Device, Device,
DeviceError, DeviceError,
@ -523,7 +524,9 @@ async def test_emeter(dev: Device, mocker, runner):
async def test_brightness(dev: Device, runner): async def test_brightness(dev: Device, runner):
res = await runner.invoke(brightness, obj=dev) res = await runner.invoke(brightness, obj=dev)
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"
):
assert "This device does not support brightness." in res.output assert "This device does not support brightness." in res.output
return return
@ -540,13 +543,16 @@ async def test_brightness(dev: Device, runner):
async def test_color_temperature(dev: Device, runner): async def test_color_temperature(dev: Device, runner):
res = await runner.invoke(temperature, obj=dev) res = await runner.invoke(temperature, obj=dev)
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")
):
assert "Device does not support color temperature" in res.output assert "Device does not support color temperature" in res.output
return return
res = await runner.invoke(temperature, obj=dev) res = await runner.invoke(temperature, obj=dev)
assert f"Color temperature: {light.color_temp}" in res.output assert f"Color temperature: {light.color_temp}" in res.output
valid_range = light.valid_temperature_range valid_range = color_temp_feat.range
assert isinstance(valid_range, ColorTempRange)
assert f"(min: {valid_range.min}, max: {valid_range.max})" in res.output assert f"(min: {valid_range.min}, max: {valid_range.max})" in res.output
val = int((valid_range.min + valid_range.max) / 2) val = int((valid_range.min + valid_range.max) / 2)
@ -572,7 +578,7 @@ async def test_color_temperature(dev: Device, runner):
async def test_color_hsv(dev: Device, runner: CliRunner): async def test_color_hsv(dev: Device, runner: CliRunner):
res = await runner.invoke(hsv, obj=dev) res = await runner.invoke(hsv, obj=dev)
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"):
assert "Device does not support colors" in res.output assert "Device does not support colors" in res.output
return return

View File

@ -198,7 +198,7 @@ async def test_light_color_temp(dev: Device):
light = next(get_parent_and_child_modules(dev, Module.Light)) light = next(get_parent_and_child_modules(dev, Module.Light))
assert light assert light
if not light.is_variable_color_temp: if not light.has_feature("color_temp"):
pytest.skip( pytest.skip(
"Some smart light strips have color_temperature" "Some smart light strips have color_temperature"
" component but min and max are the same" " component but min and max are the same"

View File

@ -280,19 +280,19 @@ async def test_deprecated_light_attributes(dev: Device):
await _test_attribute(dev, "is_color", bool(light), "Light") await _test_attribute(dev, "is_color", bool(light), "Light")
await _test_attribute(dev, "is_variable_color_temp", bool(light), "Light") await _test_attribute(dev, "is_variable_color_temp", bool(light), "Light")
exc = KasaException if light and not light.is_dimmable else None exc = KasaException if light and not light.has_feature("brightness") else None
await _test_attribute(dev, "brightness", bool(light), "Light", will_raise=exc) await _test_attribute(dev, "brightness", bool(light), "Light", will_raise=exc)
await _test_attribute( await _test_attribute(
dev, "set_brightness", bool(light), "Light", 50, will_raise=exc dev, "set_brightness", bool(light), "Light", 50, will_raise=exc
) )
exc = KasaException if light and not light.is_color else None exc = KasaException if light and not light.has_feature("hsv") else None
await _test_attribute(dev, "hsv", bool(light), "Light", will_raise=exc) await _test_attribute(dev, "hsv", bool(light), "Light", will_raise=exc)
await _test_attribute( await _test_attribute(
dev, "set_hsv", bool(light), "Light", 50, 50, 50, will_raise=exc dev, "set_hsv", bool(light), "Light", 50, 50, 50, will_raise=exc
) )
exc = KasaException if light and not light.is_variable_color_temp else None exc = KasaException if light and not light.has_feature("color_temp") else None
await _test_attribute(dev, "color_temp", bool(light), "Light", will_raise=exc) await _test_attribute(dev, "color_temp", bool(light), "Light", will_raise=exc)
await _test_attribute( await _test_attribute(
dev, "set_color_temp", bool(light), "Light", 2700, will_raise=exc dev, "set_color_temp", bool(light), "Light", 2700, will_raise=exc