Add support for effects to tplink light strips (#65166)

This commit is contained in:
J. Nick Koston 2022-03-21 20:20:40 -10:00 committed by GitHub
parent cb011570e8
commit 06ebb0b8b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 156 additions and 12 deletions

View File

@ -96,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device: SmartDevice = hass_data[entry.entry_id].device
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass_data.pop(entry.entry_id)
await device.protocol.close() # type: ignore[no-untyped-call]
await device.protocol.close()
return unload_ok

View File

@ -4,17 +4,19 @@ from __future__ import annotations
import logging
from typing import Any, cast
from kasa import SmartBulb
from kasa import SmartBulb, SmartLightStrip
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
ATTR_HS_COLOR,
ATTR_TRANSITION,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_HS,
COLOR_MODE_ONOFF,
SUPPORT_EFFECT,
SUPPORT_TRANSITION,
LightEntity,
)
@ -42,7 +44,9 @@ async def async_setup_entry(
"""Set up switches."""
coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
device = cast(SmartBulb, coordinator.device)
if device.is_bulb or device.is_light_strip or device.is_dimmer:
if device.is_light_strip:
async_add_entities([TPLinkSmartLightStrip(device, coordinator)])
elif device.is_bulb or device.is_dimmer:
async_add_entities([TPLinkSmartBulb(device, coordinator)])
@ -59,14 +63,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
"""Initialize the switch."""
super().__init__(device, coordinator)
# For backwards compat with pyHS100
if self.device.is_dimmer:
if device.is_dimmer:
# Dimmers used to use the switch format since
# pyHS100 treated them as SmartPlug but the old code
# created them as lights
# https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86
self._attr_unique_id = legacy_device_id(device)
else:
self._attr_unique_id = self.device.mac.replace(":", "").upper()
self._attr_unique_id = device.mac.replace(":", "").upper()
@async_refresh_after
async def async_turn_on(self, **kwargs: Any) -> None:
@ -104,6 +108,11 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
await self.device.set_hsv(hue, sat, brightness, transition=transition)
return
if ATTR_EFFECT in kwargs:
assert isinstance(self.device, SmartLightStrip)
await self.device.set_effect(kwargs[ATTR_EFFECT])
return
# Fallback to adjusting brightness or turning the bulb on
if brightness is not None:
await self.device.set_brightness(brightness, transition=transition)
@ -175,3 +184,28 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
return COLOR_MODE_COLOR_TEMP
return COLOR_MODE_BRIGHTNESS
class TPLinkSmartLightStrip(TPLinkSmartBulb):
"""Representation of a TPLink Smart Light Strip."""
device: SmartLightStrip
@property
def supported_features(self) -> int:
"""Flag supported features."""
return super().supported_features | SUPPORT_EFFECT
@property
def effect_list(self) -> list[str] | None:
"""Return the list of available effects."""
if effect_list := self.device.effect_list:
return cast(list[str], effect_list)
return None
@property
def effect(self) -> str | None:
"""Return the current effect."""
if (effect := self.device.effect) and effect["enable"]:
return cast(str, effect["name"])
return None

View File

@ -3,7 +3,7 @@
"name": "TP-Link Kasa Smart",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tplink",
"requirements": ["python-kasa==0.4.1"],
"requirements": ["python-kasa==0.4.2"],
"codeowners": ["@rytilahti", "@thegardenmonkey"],
"dependencies": ["network"],
"quality_scale": "platinum",

View File

@ -1891,7 +1891,7 @@ python-join-api==0.0.6
python-juicenet==1.1.0
# homeassistant.components.tplink
python-kasa==0.4.1
python-kasa==0.4.2
# homeassistant.components.lirc
# python-lirc==1.2.3

View File

@ -1221,7 +1221,7 @@ python-izone==1.2.3
python-juicenet==1.1.0
# homeassistant.components.tplink
python-kasa==0.4.1
python-kasa==0.4.2
# homeassistant.components.xiaomi_miio
python-miio==0.5.11

View File

@ -2,7 +2,14 @@
from unittest.mock import AsyncMock, MagicMock, patch
from kasa import SmartBulb, SmartDevice, SmartDimmer, SmartPlug, SmartStrip
from kasa import (
SmartBulb,
SmartDevice,
SmartDimmer,
SmartLightStrip,
SmartPlug,
SmartStrip,
)
from kasa.exceptions import SmartDeviceException
from kasa.protocol import TPLinkSmartHomeProtocol
@ -40,6 +47,10 @@ def _mocked_bulb() -> SmartBulb:
bulb.is_strip = False
bulb.is_plug = False
bulb.is_dimmer = False
bulb.is_light_strip = False
bulb.has_effects = False
bulb.effect = None
bulb.effect_list = None
bulb.hsv = (10, 30, 5)
bulb.device_id = MAC_ADDRESS
bulb.valid_temperature_range.min = 4000
@ -54,6 +65,47 @@ def _mocked_bulb() -> SmartBulb:
return bulb
class MockedSmartLightStrip(SmartLightStrip):
"""Mock a SmartLightStrip."""
def __new__(cls, *args, **kwargs):
"""Mock a SmartLightStrip that will pass an isinstance check."""
return MagicMock(spec=cls)
def _mocked_smart_light_strip() -> SmartLightStrip:
strip = MockedSmartLightStrip()
strip.update = AsyncMock()
strip.mac = MAC_ADDRESS
strip.alias = ALIAS
strip.model = MODEL
strip.host = IP_ADDRESS
strip.brightness = 50
strip.color_temp = 4000
strip.is_color = True
strip.is_strip = False
strip.is_plug = False
strip.is_dimmer = False
strip.is_light_strip = True
strip.has_effects = True
strip.effect = {"name": "Effect1", "enable": 1}
strip.effect_list = ["Effect1", "Effect2"]
strip.hsv = (10, 30, 5)
strip.device_id = MAC_ADDRESS
strip.valid_temperature_range.min = 4000
strip.valid_temperature_range.max = 9000
strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"}
strip.turn_off = AsyncMock()
strip.turn_on = AsyncMock()
strip.set_brightness = AsyncMock()
strip.set_hsv = AsyncMock()
strip.set_color_temp = AsyncMock()
strip.set_effect = AsyncMock()
strip.set_custom_effect = AsyncMock()
strip.protocol = _mock_protocol()
return strip
def _mocked_dimmer() -> SmartDimmer:
dimmer = MagicMock(auto_spec=SmartDimmer, name="Mocked dimmer")
dimmer.update = AsyncMock()
@ -67,6 +119,9 @@ def _mocked_dimmer() -> SmartDimmer:
dimmer.is_strip = False
dimmer.is_plug = False
dimmer.is_dimmer = True
dimmer.is_light_strip = False
dimmer.effect = None
dimmer.effect_list = None
dimmer.hsv = (10, 30, 5)
dimmer.device_id = MAC_ADDRESS
dimmer.valid_temperature_range.min = 4000

View File

@ -1,6 +1,7 @@
"""Tests for light platform."""
from __future__ import annotations
from datetime import timedelta
from unittest.mock import PropertyMock
import pytest
@ -10,6 +11,8 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
ATTR_EFFECT_LIST,
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
@ -20,14 +23,21 @@ from homeassistant.components.light import (
DOMAIN as LIGHT_DOMAIN,
)
from homeassistant.components.tplink.const import DOMAIN
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from . import MAC_ADDRESS, _mocked_bulb, _patch_discovery, _patch_single_discovery
from . import (
MAC_ADDRESS,
_mocked_bulb,
_mocked_smart_light_strip,
_patch_discovery,
_patch_single_discovery,
)
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_light_unique_id(hass: HomeAssistant) -> None:
@ -375,3 +385,48 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None:
)
bulb.turn_on.assert_called_once_with(transition=1)
bulb.turn_on.reset_mock()
async def test_smart_strip_effects(hass: HomeAssistant) -> None:
"""Test smart strip effects."""
already_migrated_config_entry = MockConfigEntry(
domain=DOMAIN, data={}, unique_id=MAC_ADDRESS
)
already_migrated_config_entry.add_to_hass(hass)
strip = _mocked_smart_light_strip()
with _patch_discovery(device=strip), _patch_single_discovery(device=strip):
await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done()
entity_id = "light.my_bulb"
state = hass.states.get(entity_id)
assert state.state == STATE_ON
assert state.attributes[ATTR_EFFECT] == "Effect1"
assert state.attributes[ATTR_EFFECT_LIST] == ["Effect1", "Effect2"]
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Effect2"},
blocking=True,
)
strip.set_effect.assert_called_once_with("Effect2")
strip.set_effect.reset_mock()
strip.effect = {"name": "Effect1", "enable": 0}
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
assert ATTR_EFFECT not in state.attributes
strip.effect_list = None
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
assert state.attributes[ATTR_EFFECT_LIST] is None