Add Twinkly effects (#82861)
* Add Twinkly effects * Remove spurious comment
This commit is contained in:
parent
9f8dea10f7
commit
33cd59d3c2
6 changed files with 112 additions and 8 deletions
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@ -10,10 +11,12 @@ from ttls.client import Twinkly
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
|
ATTR_EFFECT,
|
||||||
ATTR_RGB_COLOR,
|
ATTR_RGB_COLOR,
|
||||||
ATTR_RGBW_COLOR,
|
ATTR_RGBW_COLOR,
|
||||||
ColorMode,
|
ColorMode,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
|
LightEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_MODEL
|
from homeassistant.const import CONF_MODEL
|
||||||
|
@ -91,6 +94,8 @@ class TwinklyLight(LightEntity):
|
||||||
self._is_on = False
|
self._is_on = False
|
||||||
self._is_available = False
|
self._is_available = False
|
||||||
self._attributes: dict[Any, Any] = {}
|
self._attributes: dict[Any, Any] = {}
|
||||||
|
self._current_movie: dict[Any, Any] = {}
|
||||||
|
self._movies: list[Any] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
|
@ -127,19 +132,41 @@ class TwinklyLight(LightEntity):
|
||||||
name=self.name,
|
name=self.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> LightEntityFeature:
|
||||||
|
"""Return supported features."""
|
||||||
|
return LightEntityFeature.EFFECT
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return true if light is on."""
|
"""Return true if light is on."""
|
||||||
return self._is_on
|
return self._is_on
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> dict:
|
def extra_state_attributes(self) -> Mapping[str, Any]:
|
||||||
"""Return device specific state attributes."""
|
"""Return device specific state attributes."""
|
||||||
|
|
||||||
attributes = self._attributes
|
attributes = self._attributes
|
||||||
|
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect(self) -> str | None:
|
||||||
|
"""Return the current effect."""
|
||||||
|
if "name" in self._current_movie:
|
||||||
|
_LOGGER.debug("Current effect '%s'", self._current_movie["name"])
|
||||||
|
return f"{self._current_movie['id']} {self._current_movie['name']}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect_list(self) -> list[str]:
|
||||||
|
"""Return the list of saved effects."""
|
||||||
|
effect_list = []
|
||||||
|
for movie in self._movies:
|
||||||
|
effect_list.append(f"{movie['id']} {movie['name']}")
|
||||||
|
_LOGGER.debug("Effect list '%s'", effect_list)
|
||||||
|
return effect_list
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn device on."""
|
"""Turn device on."""
|
||||||
if ATTR_BRIGHTNESS in kwargs:
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
|
@ -160,15 +187,16 @@ class TwinklyLight(LightEntity):
|
||||||
if isinstance(self._attr_rgbw_color, tuple):
|
if isinstance(self._attr_rgbw_color, tuple):
|
||||||
|
|
||||||
await self._client.interview()
|
await self._client.interview()
|
||||||
# Reagarrange from rgbw to wrgb
|
# Static color only supports rgb
|
||||||
await self._client.set_static_colour(
|
await self._client.set_static_colour(
|
||||||
(
|
(
|
||||||
self._attr_rgbw_color[3],
|
|
||||||
self._attr_rgbw_color[0],
|
self._attr_rgbw_color[0],
|
||||||
self._attr_rgbw_color[1],
|
self._attr_rgbw_color[1],
|
||||||
self._attr_rgbw_color[2],
|
self._attr_rgbw_color[2],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
await self._client.set_mode("color")
|
||||||
|
self._client.default_mode = "color"
|
||||||
|
|
||||||
if ATTR_RGB_COLOR in kwargs:
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
if kwargs[ATTR_RGB_COLOR] != self._attr_rgb_color:
|
if kwargs[ATTR_RGB_COLOR] != self._attr_rgb_color:
|
||||||
|
@ -177,9 +205,20 @@ class TwinklyLight(LightEntity):
|
||||||
if isinstance(self._attr_rgb_color, tuple):
|
if isinstance(self._attr_rgb_color, tuple):
|
||||||
|
|
||||||
await self._client.interview()
|
await self._client.interview()
|
||||||
# Reagarrange from rgbw to wrgb
|
|
||||||
await self._client.set_static_colour(self._attr_rgb_color)
|
await self._client.set_static_colour(self._attr_rgb_color)
|
||||||
|
await self._client.set_mode("color")
|
||||||
|
self._client.default_mode = "color"
|
||||||
|
|
||||||
|
if ATTR_EFFECT in kwargs:
|
||||||
|
_LOGGER.debug("Setting effect '%s'", kwargs[ATTR_EFFECT])
|
||||||
|
movie_id = kwargs[ATTR_EFFECT].split(" ")[0]
|
||||||
|
if "id" not in self._current_movie or int(movie_id) != int(
|
||||||
|
self._current_movie["id"]
|
||||||
|
):
|
||||||
|
await self._client.interview()
|
||||||
|
await self._client.set_current_movie(int(movie_id))
|
||||||
|
await self._client.set_mode("movie")
|
||||||
|
self._client.default_mode = "movie"
|
||||||
if not self._is_on:
|
if not self._is_on:
|
||||||
await self._client.turn_on()
|
await self._client.turn_on()
|
||||||
|
|
||||||
|
@ -232,6 +271,9 @@ class TwinklyLight(LightEntity):
|
||||||
if key not in HIDDEN_DEV_VALUES:
|
if key not in HIDDEN_DEV_VALUES:
|
||||||
self._attributes[key] = value
|
self._attributes[key] = value
|
||||||
|
|
||||||
|
await self.async_update_movies()
|
||||||
|
await self.async_update_current_movie()
|
||||||
|
|
||||||
if not self._is_available:
|
if not self._is_available:
|
||||||
_LOGGER.info("Twinkly '%s' is now available", self._client.host)
|
_LOGGER.info("Twinkly '%s' is now available", self._client.host)
|
||||||
|
|
||||||
|
@ -245,3 +287,16 @@ class TwinklyLight(LightEntity):
|
||||||
"Twinkly '%s' is not reachable (client error)", self._client.host
|
"Twinkly '%s' is not reachable (client error)", self._client.host
|
||||||
)
|
)
|
||||||
self._is_available = False
|
self._is_available = False
|
||||||
|
|
||||||
|
async def async_update_movies(self) -> None:
|
||||||
|
"""Update the list of movies (effects)."""
|
||||||
|
movies = await self._client.get_saved_movies()
|
||||||
|
_LOGGER.debug("Movies: %s", movies)
|
||||||
|
if "movies" in movies:
|
||||||
|
self._movies = movies["movies"]
|
||||||
|
|
||||||
|
async def async_update_current_movie(self) -> None:
|
||||||
|
"""Update the current active movie."""
|
||||||
|
current_movie = await self._client.get_current_movie()
|
||||||
|
if "id" in current_movie:
|
||||||
|
self._current_movie = current_movie
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"domain": "twinkly",
|
"domain": "twinkly",
|
||||||
"name": "Twinkly",
|
"name": "Twinkly",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/twinkly",
|
"documentation": "https://www.home-assistant.io/integrations/twinkly",
|
||||||
"requirements": ["ttls==1.4.3"],
|
"requirements": ["ttls==1.5.1"],
|
||||||
"codeowners": ["@dr1rrb", "@Robbie1221"],
|
"codeowners": ["@dr1rrb", "@Robbie1221"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dhcp": [{ "hostname": "twinkly_*" }],
|
"dhcp": [{ "hostname": "twinkly_*" }],
|
||||||
|
|
|
@ -2457,7 +2457,7 @@ tp-connected==0.0.4
|
||||||
transmissionrpc==0.11
|
transmissionrpc==0.11
|
||||||
|
|
||||||
# homeassistant.components.twinkly
|
# homeassistant.components.twinkly
|
||||||
ttls==1.4.3
|
ttls==1.5.1
|
||||||
|
|
||||||
# homeassistant.components.tuya
|
# homeassistant.components.tuya
|
||||||
tuya-iot-py-sdk==0.6.6
|
tuya-iot-py-sdk==0.6.6
|
||||||
|
|
|
@ -1694,7 +1694,7 @@ total_connect_client==2022.10
|
||||||
transmissionrpc==0.11
|
transmissionrpc==0.11
|
||||||
|
|
||||||
# homeassistant.components.twinkly
|
# homeassistant.components.twinkly
|
||||||
ttls==1.4.3
|
ttls==1.5.1
|
||||||
|
|
||||||
# homeassistant.components.tuya
|
# homeassistant.components.tuya
|
||||||
tuya-iot-py-sdk==0.6.6
|
tuya-iot-py-sdk==0.6.6
|
||||||
|
|
|
@ -22,6 +22,9 @@ class ClientMock:
|
||||||
self.state = True
|
self.state = True
|
||||||
self.brightness = {"mode": "enabled", "value": 10}
|
self.brightness = {"mode": "enabled", "value": 10}
|
||||||
self.color = None
|
self.color = None
|
||||||
|
self.movies = [{"id": 1, "name": "Rainbow"}, {"id": 2, "name": "Flare"}]
|
||||||
|
self.current_movie = {}
|
||||||
|
self.default_mode = "movie"
|
||||||
|
|
||||||
self.id = str(uuid4())
|
self.id = str(uuid4())
|
||||||
self.device_info = {
|
self.device_info = {
|
||||||
|
@ -81,3 +84,22 @@ class ClientMock:
|
||||||
|
|
||||||
async def interview(self) -> None:
|
async def interview(self) -> None:
|
||||||
"""Interview."""
|
"""Interview."""
|
||||||
|
|
||||||
|
async def get_saved_movies(self) -> dict:
|
||||||
|
"""Get saved movies."""
|
||||||
|
return self.movies
|
||||||
|
|
||||||
|
async def get_current_movie(self) -> dict:
|
||||||
|
"""Get current movie."""
|
||||||
|
return self.current_movie
|
||||||
|
|
||||||
|
async def set_current_movie(self, movie_id: int) -> dict:
|
||||||
|
"""Set current movie."""
|
||||||
|
self.current_movie = {"id": movie_id}
|
||||||
|
|
||||||
|
async def set_mode(self, mode: str) -> None:
|
||||||
|
"""Set mode."""
|
||||||
|
if mode == "off":
|
||||||
|
self.turn_off
|
||||||
|
else:
|
||||||
|
self.turn_on
|
||||||
|
|
|
@ -118,7 +118,8 @@ async def test_turn_on_with_color_rgbw(hass: HomeAssistant):
|
||||||
state = hass.states.get(entity.entity_id)
|
state = hass.states.get(entity.entity_id)
|
||||||
|
|
||||||
assert state.state == "on"
|
assert state.state == "on"
|
||||||
assert client.color == (0, 128, 64, 32)
|
assert client.color == (128, 64, 32)
|
||||||
|
assert client.default_mode == "color"
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_on_with_color_rgb(hass: HomeAssistant):
|
async def test_turn_on_with_color_rgb(hass: HomeAssistant):
|
||||||
|
@ -142,6 +143,32 @@ async def test_turn_on_with_color_rgb(hass: HomeAssistant):
|
||||||
|
|
||||||
assert state.state == "on"
|
assert state.state == "on"
|
||||||
assert client.color == (128, 64, 32)
|
assert client.color == (128, 64, 32)
|
||||||
|
assert client.default_mode == "color"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_on_with_effect(hass: HomeAssistant):
|
||||||
|
"""Test support of the light.turn_on service with a brightness parameter."""
|
||||||
|
client = ClientMock()
|
||||||
|
client.state = False
|
||||||
|
client.device_info["led_profile"] = "RGB"
|
||||||
|
client.brightness = {"mode": "enabled", "value": 255}
|
||||||
|
entity, _, _, _ = await _create_entries(hass, client)
|
||||||
|
|
||||||
|
assert hass.states.get(entity.entity_id).state == "off"
|
||||||
|
assert client.current_movie == {}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
service_data={"entity_id": entity.entity_id, "effect": "1 Rainbow"},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity.entity_id)
|
||||||
|
|
||||||
|
assert state.state == "on"
|
||||||
|
assert client.current_movie["id"] == 1
|
||||||
|
assert client.default_mode == "movie"
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_off(hass: HomeAssistant):
|
async def test_turn_off(hass: HomeAssistant):
|
||||||
|
|
Loading…
Add table
Reference in a new issue