Skip to content

Commit

Permalink
Use feature checks in tplink integration (#133795)
Browse files Browse the repository at this point in the history
Clean up to use new upstream API:

* Use Feature attributes to check for supported

* Use color_temp range and update tests
  • Loading branch information
sdb9696 authored Dec 22, 2024
1 parent 26d5c55 commit 3cc75c3
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 67 deletions.
28 changes: 16 additions & 12 deletions homeassistant/components/tplink/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,13 @@ def __init__(
# 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:
Expand Down Expand Up @@ -270,15 +269,17 @@ async def _async_set_color_temp(
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,
Expand Down Expand Up @@ -325,8 +326,11 @@ def _determine_color_mode(self) -> ColorMode:
# 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

Expand All @@ -335,7 +339,7 @@ def _async_update_attrs(self) -> None:
"""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
Expand Down
30 changes: 17 additions & 13 deletions tests/components/tplink/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -289,6 +296,7 @@ def _mocked_device(
device.protocol = _mock_protocol()
device.config = device_config
device.credentials_hash = credentials_hash

return device


Expand All @@ -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.
Expand Down Expand Up @@ -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


Expand All @@ -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, *_, **__):
Expand Down Expand Up @@ -389,7 +394,6 @@ async def _set_color_temp(temp, *_, **__):

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"]
Expand Down
4 changes: 3 additions & 1 deletion tests/components/tplink/fixtures/features.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions tests/components/tplink/snapshots/test_climate.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}),
Expand Down
16 changes: 8 additions & 8 deletions tests/components/tplink/snapshots/test_number.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
}),
'area_id': None,
'capabilities': dict({
'max': 65536,
'max': 60,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
Expand Down Expand Up @@ -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,
Expand All @@ -96,7 +96,7 @@
}),
'area_id': None,
'capabilities': dict({
'max': 65536,
'max': 60,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
Expand Down Expand Up @@ -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,
Expand All @@ -151,7 +151,7 @@
}),
'area_id': None,
'capabilities': dict({
'max': 65536,
'max': 10,
'min': -10,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
Expand Down Expand Up @@ -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,
Expand All @@ -206,7 +206,7 @@
}),
'area_id': None,
'capabilities': dict({
'max': 65536,
'max': 60,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 17 additions & 2 deletions tests/components/tplink/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
MAC_ADDRESS,
MODEL,
_mocked_device,
_mocked_feature,
_patch_connect,
_patch_discovery,
_patch_single_discovery,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Loading

0 comments on commit 3cc75c3

Please sign in to comment.