mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Use feature checks in tplink integration (#133795)
Clean up to use new upstream API: * Use Feature attributes to check for supported * Use color_temp range and update tests
This commit is contained in:
parent
26d5c55d11
commit
3cc75c3cf6
@ -200,14 +200,13 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity):
|
||||
# If _attr_name is None the entity name will be the device name
|
||||
self._attr_name = None if parent is None else device.alias
|
||||
modes: set[ColorMode] = {ColorMode.ONOFF}
|
||||
if light_module.is_variable_color_temp:
|
||||
if color_temp_feat := light_module.get_feature("color_temp"):
|
||||
modes.add(ColorMode.COLOR_TEMP)
|
||||
temp_range = light_module.valid_temperature_range
|
||||
self._attr_min_color_temp_kelvin = temp_range.min
|
||||
self._attr_max_color_temp_kelvin = temp_range.max
|
||||
if light_module.is_color:
|
||||
self._attr_min_color_temp_kelvin = color_temp_feat.minimum_value
|
||||
self._attr_max_color_temp_kelvin = color_temp_feat.maximum_value
|
||||
if light_module.has_feature("hsv"):
|
||||
modes.add(ColorMode.HS)
|
||||
if light_module.is_dimmable:
|
||||
if light_module.has_feature("brightness"):
|
||||
modes.add(ColorMode.BRIGHTNESS)
|
||||
self._attr_supported_color_modes = filter_supported_color_modes(modes)
|
||||
if len(self._attr_supported_color_modes) == 1:
|
||||
@ -270,15 +269,17 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity):
|
||||
self, color_temp: float, brightness: int | None, transition: int | None
|
||||
) -> None:
|
||||
light_module = self._light_module
|
||||
valid_temperature_range = light_module.valid_temperature_range
|
||||
color_temp_feat = light_module.get_feature("color_temp")
|
||||
assert color_temp_feat
|
||||
|
||||
requested_color_temp = round(color_temp)
|
||||
# Clamp color temp to valid range
|
||||
# since if the light in a group we will
|
||||
# get requests for color temps for the range
|
||||
# of the group and not the light
|
||||
clamped_color_temp = min(
|
||||
valid_temperature_range.max,
|
||||
max(valid_temperature_range.min, requested_color_temp),
|
||||
color_temp_feat.maximum_value,
|
||||
max(color_temp_feat.minimum_value, requested_color_temp),
|
||||
)
|
||||
await light_module.set_color_temp(
|
||||
clamped_color_temp,
|
||||
@ -325,8 +326,11 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity):
|
||||
# The light supports only a single color mode, return it
|
||||
return self._fixed_color_mode
|
||||
|
||||
# The light supports both color temp and color, determine which on is active
|
||||
if self._light_module.is_variable_color_temp and self._light_module.color_temp:
|
||||
# The light supports both color temp and color, determine which one is active
|
||||
if (
|
||||
self._light_module.has_feature("color_temp")
|
||||
and self._light_module.color_temp
|
||||
):
|
||||
return ColorMode.COLOR_TEMP
|
||||
return ColorMode.HS
|
||||
|
||||
@ -335,7 +339,7 @@ class TPLinkLightEntity(CoordinatedTPLinkEntity, LightEntity):
|
||||
"""Update the entity's attributes."""
|
||||
light_module = self._light_module
|
||||
self._attr_is_on = light_module.state.light_on is True
|
||||
if light_module.is_dimmable:
|
||||
if light_module.has_feature("brightness"):
|
||||
self._attr_brightness = round((light_module.brightness * 255.0) / 100.0)
|
||||
color_mode = self._determine_color_mode()
|
||||
self._attr_color_mode = color_mode
|
||||
|
@ -257,20 +257,27 @@ def _mocked_device(
|
||||
for module_name in modules
|
||||
}
|
||||
|
||||
device_features = {}
|
||||
if features:
|
||||
device.features = {
|
||||
device_features = {
|
||||
feature_id: _mocked_feature(feature_id, require_fixture=True)
|
||||
for feature_id in features
|
||||
if isinstance(feature_id, str)
|
||||
}
|
||||
|
||||
device.features.update(
|
||||
device_features.update(
|
||||
{
|
||||
feature.id: feature
|
||||
for feature in features
|
||||
if isinstance(feature, Feature)
|
||||
}
|
||||
)
|
||||
device.features = device_features
|
||||
|
||||
for mod in device.modules.values():
|
||||
mod.get_feature.side_effect = device_features.get
|
||||
mod.has_feature.side_effect = lambda id: id in device_features
|
||||
|
||||
device.children = []
|
||||
if children:
|
||||
for child in children:
|
||||
@ -289,6 +296,7 @@ def _mocked_device(
|
||||
device.protocol = _mock_protocol()
|
||||
device.config = device_config
|
||||
device.credentials_hash = credentials_hash
|
||||
|
||||
return device
|
||||
|
||||
|
||||
@ -303,8 +311,8 @@ def _mocked_feature(
|
||||
precision_hint=None,
|
||||
choices=None,
|
||||
unit=None,
|
||||
minimum_value=0,
|
||||
maximum_value=2**16, # Arbitrary max
|
||||
minimum_value=None,
|
||||
maximum_value=None,
|
||||
) -> Feature:
|
||||
"""Get a mocked feature.
|
||||
|
||||
@ -334,11 +342,14 @@ def _mocked_feature(
|
||||
feature.unit = unit or fixture.get("unit")
|
||||
|
||||
# number
|
||||
feature.minimum_value = minimum_value or fixture.get("minimum_value")
|
||||
feature.maximum_value = maximum_value or fixture.get("maximum_value")
|
||||
min_val = minimum_value or fixture.get("minimum_value")
|
||||
feature.minimum_value = 0 if min_val is None else min_val
|
||||
max_val = maximum_value or fixture.get("maximum_value")
|
||||
feature.maximum_value = 2**16 if max_val is None else max_val
|
||||
|
||||
# select
|
||||
feature.choices = choices or fixture.get("choices")
|
||||
|
||||
return feature
|
||||
|
||||
|
||||
@ -350,13 +361,7 @@ def _mocked_light_module(device) -> Light:
|
||||
light.state = LightState(
|
||||
light_on=True, brightness=light.brightness, color_temp=light.color_temp
|
||||
)
|
||||
light.is_color = True
|
||||
light.is_variable_color_temp = True
|
||||
light.is_dimmable = True
|
||||
light.is_brightness = True
|
||||
light.has_effects = False
|
||||
light.hsv = (10, 30, 5)
|
||||
light.valid_temperature_range = ColorTempRange(min=4000, max=9000)
|
||||
light.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
|
||||
|
||||
async def _set_state(state, *_, **__):
|
||||
@ -389,7 +394,6 @@ def _mocked_light_module(device) -> Light:
|
||||
|
||||
def _mocked_light_effect_module(device) -> LightEffect:
|
||||
effect = MagicMock(spec=LightEffect, name="Mocked light effect")
|
||||
effect.has_effects = True
|
||||
effect.has_custom_effects = True
|
||||
effect.effect = "Effect1"
|
||||
effect.effect_list = ["Off", "Effect1", "Effect2"]
|
||||
|
@ -267,7 +267,9 @@
|
||||
"target_temperature": {
|
||||
"value": false,
|
||||
"type": "Number",
|
||||
"category": "Primary"
|
||||
"category": "Primary",
|
||||
"minimum_value": 5,
|
||||
"maximum_value": 30
|
||||
},
|
||||
"fan_speed_level": {
|
||||
"value": 2,
|
||||
|
@ -9,8 +9,8 @@
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 65536,
|
||||
'min_temp': None,
|
||||
'max_temp': 30,
|
||||
'min_temp': 5,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@ -49,8 +49,8 @@
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 65536,
|
||||
'min_temp': None,
|
||||
'max_temp': 30,
|
||||
'min_temp': 5,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'temperature': 22.2,
|
||||
}),
|
||||
|
@ -41,7 +41,7 @@
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 65536,
|
||||
'max': 60,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
@ -77,7 +77,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Smooth off',
|
||||
'max': 65536,
|
||||
'max': 60,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
@ -96,7 +96,7 @@
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 65536,
|
||||
'max': 60,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
@ -132,7 +132,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Smooth on',
|
||||
'max': 65536,
|
||||
'max': 60,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
@ -151,7 +151,7 @@
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 65536,
|
||||
'max': 10,
|
||||
'min': -10,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
@ -187,7 +187,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Temperature offset',
|
||||
'max': 65536,
|
||||
'max': 10,
|
||||
'min': -10,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
@ -206,7 +206,7 @@
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 65536,
|
||||
'max': 60,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
@ -242,7 +242,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my_device Turn off in',
|
||||
'max': 65536,
|
||||
'max': 60,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.BOX: 'box'>,
|
||||
'step': 1.0,
|
||||
|
@ -54,6 +54,7 @@ from . import (
|
||||
MAC_ADDRESS,
|
||||
MODEL,
|
||||
_mocked_device,
|
||||
_mocked_feature,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
_patch_single_discovery,
|
||||
@ -335,7 +336,14 @@ async def test_update_attrs_fails_in_init(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
light = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
features = [
|
||||
_mocked_feature("brightness", value=50),
|
||||
_mocked_feature("hsv", value=(10, 30, 5)),
|
||||
_mocked_feature(
|
||||
"color_temp", value=4000, minimum_value=4000, maximum_value=9000
|
||||
),
|
||||
]
|
||||
light = _mocked_device(modules=[Module.Light], alias="my_light", features=features)
|
||||
light_module = light.modules[Module.Light]
|
||||
p = PropertyMock(side_effect=KasaException)
|
||||
type(light_module).color_temp = p
|
||||
@ -363,7 +371,14 @@ async def test_update_attrs_fails_on_update(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
light = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
features = [
|
||||
_mocked_feature("brightness", value=50),
|
||||
_mocked_feature("hsv", value=(10, 30, 5)),
|
||||
_mocked_feature(
|
||||
"color_temp", value=4000, minimum_value=4000, maximum_value=9000
|
||||
),
|
||||
]
|
||||
light = _mocked_device(modules=[Module.Light], alias="my_light", features=features)
|
||||
light_module = light.modules[Module.Light]
|
||||
|
||||
with _patch_discovery(device=light), _patch_connect(device=light):
|
||||
|
@ -54,6 +54,7 @@ from . import (
|
||||
DEVICE_ID,
|
||||
MAC_ADDRESS,
|
||||
_mocked_device,
|
||||
_mocked_feature,
|
||||
_patch_connect,
|
||||
_patch_discovery,
|
||||
_patch_single_discovery,
|
||||
@ -118,8 +119,32 @@ async def test_legacy_dimmer_unique_id(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("device", "transition"),
|
||||
[
|
||||
(_mocked_device(modules=[Module.Light]), 2.0),
|
||||
(_mocked_device(modules=[Module.Light, Module.LightEffect]), None),
|
||||
(
|
||||
_mocked_device(
|
||||
modules=[Module.Light],
|
||||
features=[
|
||||
_mocked_feature("brightness", value=50),
|
||||
_mocked_feature("hsv", value=(10, 30, 5)),
|
||||
_mocked_feature(
|
||||
"color_temp", value=4000, minimum_value=4000, maximum_value=9000
|
||||
),
|
||||
],
|
||||
),
|
||||
2.0,
|
||||
),
|
||||
(
|
||||
_mocked_device(
|
||||
modules=[Module.Light, Module.LightEffect],
|
||||
features=[
|
||||
_mocked_feature("brightness", value=50),
|
||||
_mocked_feature("hsv", value=(10, 30, 5)),
|
||||
_mocked_feature(
|
||||
"color_temp", value=4000, minimum_value=4000, maximum_value=9000
|
||||
),
|
||||
],
|
||||
),
|
||||
None,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_color_light(
|
||||
@ -131,7 +156,10 @@ async def test_color_light(
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
light = device.modules[Module.Light]
|
||||
|
||||
# Setting color_temp to None emulates a device with active effects
|
||||
light.color_temp = None
|
||||
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
@ -220,9 +248,14 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None:
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
features = [
|
||||
_mocked_feature("brightness", value=50),
|
||||
_mocked_feature("hsv", value=(10, 30, 5)),
|
||||
]
|
||||
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light", features=features)
|
||||
light = device.modules[Module.Light]
|
||||
light.is_variable_color_temp = False
|
||||
|
||||
type(light).color_temp = PropertyMock(side_effect=Exception)
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
@ -272,25 +305,47 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("bulb", "is_color"),
|
||||
("device", "is_color"),
|
||||
[
|
||||
(_mocked_device(modules=[Module.Light], alias="my_light"), True),
|
||||
(_mocked_device(modules=[Module.Light], alias="my_light"), False),
|
||||
(
|
||||
_mocked_device(
|
||||
modules=[Module.Light],
|
||||
alias="my_light",
|
||||
features=[
|
||||
_mocked_feature("brightness", value=50),
|
||||
_mocked_feature("hsv", value=(10, 30, 5)),
|
||||
_mocked_feature(
|
||||
"color_temp", value=4000, minimum_value=4000, maximum_value=9000
|
||||
),
|
||||
],
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
_mocked_device(
|
||||
modules=[Module.Light],
|
||||
alias="my_light",
|
||||
features=[
|
||||
_mocked_feature("brightness", value=50),
|
||||
_mocked_feature(
|
||||
"color_temp", value=4000, minimum_value=4000, maximum_value=9000
|
||||
),
|
||||
],
|
||||
),
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_color_temp_light(
|
||||
hass: HomeAssistant, bulb: MagicMock, is_color: bool
|
||||
hass: HomeAssistant, device: MagicMock, is_color: bool
|
||||
) -> None:
|
||||
"""Test a light."""
|
||||
already_migrated_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
# device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
light = device.modules[Module.Light]
|
||||
light.is_color = is_color
|
||||
light.color_temp = 4000
|
||||
light.is_variable_color_temp = True
|
||||
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
@ -303,7 +358,7 @@ async def test_color_temp_light(
|
||||
attributes = state.attributes
|
||||
assert attributes[ATTR_BRIGHTNESS] == 128
|
||||
assert attributes[ATTR_COLOR_MODE] == "color_temp"
|
||||
if light.is_color:
|
||||
if is_color:
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"]
|
||||
else:
|
||||
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"]
|
||||
@ -368,10 +423,11 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None:
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
features = [
|
||||
_mocked_feature("brightness", value=50),
|
||||
]
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light", features=features)
|
||||
light = device.modules[Module.Light]
|
||||
light.is_color = False
|
||||
light.is_variable_color_temp = False
|
||||
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
@ -414,11 +470,8 @@ async def test_on_off_light(hass: HomeAssistant) -> None:
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light", features=[])
|
||||
light = device.modules[Module.Light]
|
||||
light.is_color = False
|
||||
light.is_variable_color_temp = False
|
||||
light.is_dimmable = False
|
||||
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
@ -450,11 +503,9 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None:
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light")
|
||||
device = _mocked_device(modules=[Module.Light], alias="my_light", features=[])
|
||||
light = device.modules[Module.Light]
|
||||
light.is_color = False
|
||||
light.is_variable_color_temp = False
|
||||
light.is_dimmable = False
|
||||
|
||||
light.state = LightState(light_on=False)
|
||||
|
||||
with _patch_discovery(device=device), _patch_connect(device=device):
|
||||
@ -513,8 +564,15 @@ async def test_smart_strip_effects(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
features = [
|
||||
_mocked_feature("brightness", value=50),
|
||||
_mocked_feature("hsv", value=(10, 30, 5)),
|
||||
_mocked_feature(
|
||||
"color_temp", value=4000, minimum_value=4000, maximum_value=9000
|
||||
),
|
||||
]
|
||||
device = _mocked_device(
|
||||
modules=[Module.Light, Module.LightEffect], alias="my_light"
|
||||
modules=[Module.Light, Module.LightEffect], alias="my_light", features=features
|
||||
)
|
||||
light = device.modules[Module.Light]
|
||||
light_effect = device.modules[Module.LightEffect]
|
||||
@ -977,8 +1035,15 @@ async def test_scene_effect_light(
|
||||
domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
features = [
|
||||
_mocked_feature("brightness", value=50),
|
||||
_mocked_feature("hsv", value=(10, 30, 5)),
|
||||
_mocked_feature(
|
||||
"color_temp", value=4000, minimum_value=4000, maximum_value=9000
|
||||
),
|
||||
]
|
||||
device = _mocked_device(
|
||||
modules=[Module.Light, Module.LightEffect], alias="my_light"
|
||||
modules=[Module.Light, Module.LightEffect], alias="my_light", features=features
|
||||
)
|
||||
light_effect = device.modules[Module.LightEffect]
|
||||
light_effect.effect = LightEffect.LIGHT_EFFECTS_OFF
|
||||
|
@ -129,7 +129,7 @@ async def test_color_light_no_emeter(hass: HomeAssistant) -> None:
|
||||
)
|
||||
already_migrated_config_entry.add_to_hass(hass)
|
||||
bulb = _mocked_device(alias="my_bulb", modules=[Module.Light])
|
||||
bulb.has_emeter = False
|
||||
|
||||
with _patch_discovery(device=bulb), _patch_connect(device=bulb):
|
||||
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
Loading…
x
Reference in New Issue
Block a user