From 33cd59d3c2e1f945c16b39d929349e3eeb4cfb9a Mon Sep 17 00:00:00 2001 From: Olen Date: Tue, 29 Nov 2022 11:15:30 +0100 Subject: [PATCH] Add Twinkly effects (#82861) * Add Twinkly effects * Remove spurious comment --- homeassistant/components/twinkly/light.py | 63 +++++++++++++++++-- .../components/twinkly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/twinkly/__init__.py | 22 +++++++ tests/components/twinkly/test_light.py | 29 ++++++++- 6 files changed, 112 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index ba6ac7cf492..05f24411423 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -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 diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index e3b97e9385b..b41d9bc1d0a 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -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_*" }], diff --git a/requirements_all.txt b/requirements_all.txt index ad71455ddab..0a5ea93e512 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40e42afd3fe..97b6dd736d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/twinkly/__init__.py b/tests/components/twinkly/__init__.py index d5440ddb74a..351f5509aa2 100644 --- a/tests/components/twinkly/__init__.py +++ b/tests/components/twinkly/__init__.py @@ -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 diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index 40fea31a6ba..f6fe9e297f6 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -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):