Add Twinkly effects (#82861)

* Add Twinkly effects

* Remove spurious comment
This commit is contained in:
Olen 2022-11-29 11:15:30 +01:00 committed by GitHub
parent 9f8dea10f7
commit 33cd59d3c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 112 additions and 8 deletions

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Mapping
import logging
from typing import Any
@ -10,10 +11,12 @@ from ttls.client import Twinkly
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_EFFECT,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ColorMode,
LightEntity,
LightEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL
@ -91,6 +94,8 @@ class TwinklyLight(LightEntity):
self._is_on = False
self._is_available = False
self._attributes: dict[Any, Any] = {}
self._current_movie: dict[Any, Any] = {}
self._movies: list[Any] = []
@property
def available(self) -> bool:
@ -127,19 +132,41 @@ class TwinklyLight(LightEntity):
name=self.name,
)
@property
def supported_features(self) -> LightEntityFeature:
"""Return supported features."""
return LightEntityFeature.EFFECT
@property
def is_on(self) -> bool:
"""Return true if light is on."""
return self._is_on
@property
def extra_state_attributes(self) -> dict:
def extra_state_attributes(self) -> Mapping[str, Any]:
"""Return device specific state attributes."""
attributes = self._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:
"""Turn device on."""
if ATTR_BRIGHTNESS in kwargs:
@ -160,15 +187,16 @@ class TwinklyLight(LightEntity):
if isinstance(self._attr_rgbw_color, tuple):
await self._client.interview()
# Reagarrange from rgbw to wrgb
# Static color only supports rgb
await self._client.set_static_colour(
(
self._attr_rgbw_color[3],
self._attr_rgbw_color[0],
self._attr_rgbw_color[1],
self._attr_rgbw_color[2],
)
)
await self._client.set_mode("color")
self._client.default_mode = "color"
if ATTR_RGB_COLOR in kwargs:
if kwargs[ATTR_RGB_COLOR] != self._attr_rgb_color:
@ -177,9 +205,20 @@ class TwinklyLight(LightEntity):
if isinstance(self._attr_rgb_color, tuple):
await self._client.interview()
# Reagarrange from rgbw to wrgb
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:
await self._client.turn_on()
@ -232,6 +271,9 @@ class TwinklyLight(LightEntity):
if key not in HIDDEN_DEV_VALUES:
self._attributes[key] = value
await self.async_update_movies()
await self.async_update_current_movie()
if not self._is_available:
_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
)
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

View file

@ -2,7 +2,7 @@
"domain": "twinkly",
"name": "Twinkly",
"documentation": "https://www.home-assistant.io/integrations/twinkly",
"requirements": ["ttls==1.4.3"],
"requirements": ["ttls==1.5.1"],
"codeowners": ["@dr1rrb", "@Robbie1221"],
"config_flow": true,
"dhcp": [{ "hostname": "twinkly_*" }],

View file

@ -2457,7 +2457,7 @@ tp-connected==0.0.4
transmissionrpc==0.11
# homeassistant.components.twinkly
ttls==1.4.3
ttls==1.5.1
# homeassistant.components.tuya
tuya-iot-py-sdk==0.6.6

View file

@ -1694,7 +1694,7 @@ total_connect_client==2022.10
transmissionrpc==0.11
# homeassistant.components.twinkly
ttls==1.4.3
ttls==1.5.1
# homeassistant.components.tuya
tuya-iot-py-sdk==0.6.6

View file

@ -22,6 +22,9 @@ class ClientMock:
self.state = True
self.brightness = {"mode": "enabled", "value": 10}
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.device_info = {
@ -81,3 +84,22 @@ class ClientMock:
async def interview(self) -> None:
"""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

View file

@ -118,7 +118,8 @@ async def test_turn_on_with_color_rgbw(hass: HomeAssistant):
state = hass.states.get(entity.entity_id)
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):
@ -142,6 +143,32 @@ async def test_turn_on_with_color_rgb(hass: HomeAssistant):
assert state.state == "on"
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):