Skip to content
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

Use feature checks in tplink integration #133795

Merged
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
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