mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01: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
|
||||
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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user