mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 02:37:08 +00:00
Add support for effects to tplink light strips (#65166)
This commit is contained in:
parent
cb011570e8
commit
06ebb0b8b3
@ -96,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
device: SmartDevice = hass_data[entry.entry_id].device
|
device: SmartDevice = hass_data[entry.entry_id].device
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
hass_data.pop(entry.entry_id)
|
hass_data.pop(entry.entry_id)
|
||||||
await device.protocol.close() # type: ignore[no-untyped-call]
|
await device.protocol.close()
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,17 +4,19 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from kasa import SmartBulb
|
from kasa import SmartBulb, SmartLightStrip
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP,
|
||||||
|
ATTR_EFFECT,
|
||||||
ATTR_HS_COLOR,
|
ATTR_HS_COLOR,
|
||||||
ATTR_TRANSITION,
|
ATTR_TRANSITION,
|
||||||
COLOR_MODE_BRIGHTNESS,
|
COLOR_MODE_BRIGHTNESS,
|
||||||
COLOR_MODE_COLOR_TEMP,
|
COLOR_MODE_COLOR_TEMP,
|
||||||
COLOR_MODE_HS,
|
COLOR_MODE_HS,
|
||||||
COLOR_MODE_ONOFF,
|
COLOR_MODE_ONOFF,
|
||||||
|
SUPPORT_EFFECT,
|
||||||
SUPPORT_TRANSITION,
|
SUPPORT_TRANSITION,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
@ -42,7 +44,9 @@ async def async_setup_entry(
|
|||||||
"""Set up switches."""
|
"""Set up switches."""
|
||||||
coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
device = cast(SmartBulb, coordinator.device)
|
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)])
|
async_add_entities([TPLinkSmartBulb(device, coordinator)])
|
||||||
|
|
||||||
|
|
||||||
@ -59,14 +63,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||||||
"""Initialize the switch."""
|
"""Initialize the switch."""
|
||||||
super().__init__(device, coordinator)
|
super().__init__(device, coordinator)
|
||||||
# For backwards compat with pyHS100
|
# For backwards compat with pyHS100
|
||||||
if self.device.is_dimmer:
|
if device.is_dimmer:
|
||||||
# Dimmers used to use the switch format since
|
# Dimmers used to use the switch format since
|
||||||
# pyHS100 treated them as SmartPlug but the old code
|
# pyHS100 treated them as SmartPlug but the old code
|
||||||
# created them as lights
|
# created them as lights
|
||||||
# https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86
|
# https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86
|
||||||
self._attr_unique_id = legacy_device_id(device)
|
self._attr_unique_id = legacy_device_id(device)
|
||||||
else:
|
else:
|
||||||
self._attr_unique_id = self.device.mac.replace(":", "").upper()
|
self._attr_unique_id = device.mac.replace(":", "").upper()
|
||||||
|
|
||||||
@async_refresh_after
|
@async_refresh_after
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
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)
|
await self.device.set_hsv(hue, sat, brightness, transition=transition)
|
||||||
return
|
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
|
# Fallback to adjusting brightness or turning the bulb on
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
await self.device.set_brightness(brightness, transition=transition)
|
await self.device.set_brightness(brightness, transition=transition)
|
||||||
@ -175,3 +184,28 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||||||
return COLOR_MODE_COLOR_TEMP
|
return COLOR_MODE_COLOR_TEMP
|
||||||
|
|
||||||
return COLOR_MODE_BRIGHTNESS
|
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
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "TP-Link Kasa Smart",
|
"name": "TP-Link Kasa Smart",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/tplink",
|
"documentation": "https://www.home-assistant.io/integrations/tplink",
|
||||||
"requirements": ["python-kasa==0.4.1"],
|
"requirements": ["python-kasa==0.4.2"],
|
||||||
"codeowners": ["@rytilahti", "@thegardenmonkey"],
|
"codeowners": ["@rytilahti", "@thegardenmonkey"],
|
||||||
"dependencies": ["network"],
|
"dependencies": ["network"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
|
@ -1891,7 +1891,7 @@ python-join-api==0.0.6
|
|||||||
python-juicenet==1.1.0
|
python-juicenet==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.tplink
|
# homeassistant.components.tplink
|
||||||
python-kasa==0.4.1
|
python-kasa==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.lirc
|
# homeassistant.components.lirc
|
||||||
# python-lirc==1.2.3
|
# python-lirc==1.2.3
|
||||||
|
@ -1221,7 +1221,7 @@ python-izone==1.2.3
|
|||||||
python-juicenet==1.1.0
|
python-juicenet==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.tplink
|
# homeassistant.components.tplink
|
||||||
python-kasa==0.4.1
|
python-kasa==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.xiaomi_miio
|
# homeassistant.components.xiaomi_miio
|
||||||
python-miio==0.5.11
|
python-miio==0.5.11
|
||||||
|
@ -2,7 +2,14 @@
|
|||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
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.exceptions import SmartDeviceException
|
||||||
from kasa.protocol import TPLinkSmartHomeProtocol
|
from kasa.protocol import TPLinkSmartHomeProtocol
|
||||||
|
|
||||||
@ -40,6 +47,10 @@ def _mocked_bulb() -> SmartBulb:
|
|||||||
bulb.is_strip = False
|
bulb.is_strip = False
|
||||||
bulb.is_plug = False
|
bulb.is_plug = False
|
||||||
bulb.is_dimmer = 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.hsv = (10, 30, 5)
|
||||||
bulb.device_id = MAC_ADDRESS
|
bulb.device_id = MAC_ADDRESS
|
||||||
bulb.valid_temperature_range.min = 4000
|
bulb.valid_temperature_range.min = 4000
|
||||||
@ -54,6 +65,47 @@ def _mocked_bulb() -> SmartBulb:
|
|||||||
return bulb
|
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:
|
def _mocked_dimmer() -> SmartDimmer:
|
||||||
dimmer = MagicMock(auto_spec=SmartDimmer, name="Mocked dimmer")
|
dimmer = MagicMock(auto_spec=SmartDimmer, name="Mocked dimmer")
|
||||||
dimmer.update = AsyncMock()
|
dimmer.update = AsyncMock()
|
||||||
@ -67,6 +119,9 @@ def _mocked_dimmer() -> SmartDimmer:
|
|||||||
dimmer.is_strip = False
|
dimmer.is_strip = False
|
||||||
dimmer.is_plug = False
|
dimmer.is_plug = False
|
||||||
dimmer.is_dimmer = True
|
dimmer.is_dimmer = True
|
||||||
|
dimmer.is_light_strip = False
|
||||||
|
dimmer.effect = None
|
||||||
|
dimmer.effect_list = None
|
||||||
dimmer.hsv = (10, 30, 5)
|
dimmer.hsv = (10, 30, 5)
|
||||||
dimmer.device_id = MAC_ADDRESS
|
dimmer.device_id = MAC_ADDRESS
|
||||||
dimmer.valid_temperature_range.min = 4000
|
dimmer.valid_temperature_range.min = 4000
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Tests for light platform."""
|
"""Tests for light platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from unittest.mock import PropertyMock
|
from unittest.mock import PropertyMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -10,6 +11,8 @@ from homeassistant.components.light import (
|
|||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_COLOR_MODE,
|
ATTR_COLOR_MODE,
|
||||||
ATTR_COLOR_TEMP,
|
ATTR_COLOR_TEMP,
|
||||||
|
ATTR_EFFECT,
|
||||||
|
ATTR_EFFECT_LIST,
|
||||||
ATTR_HS_COLOR,
|
ATTR_HS_COLOR,
|
||||||
ATTR_MAX_MIREDS,
|
ATTR_MAX_MIREDS,
|
||||||
ATTR_MIN_MIREDS,
|
ATTR_MIN_MIREDS,
|
||||||
@ -20,14 +23,21 @@ from homeassistant.components.light import (
|
|||||||
DOMAIN as LIGHT_DOMAIN,
|
DOMAIN as LIGHT_DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.components.tplink.const import 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.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
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:
|
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.assert_called_once_with(transition=1)
|
||||||
bulb.turn_on.reset_mock()
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user