Skip to content

Commit

Permalink
Add Twinkly effects (#82861)
Browse files Browse the repository at this point in the history
* Add Twinkly effects

* Remove spurious comment
  • Loading branch information
Olen committed Nov 29, 2022
1 parent 9f8dea1 commit 33cd59d
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 9 deletions.
65 changes: 60 additions & 5 deletions homeassistant/components/twinkly/light.py
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

import asyncio
from collections.abc import Mapping
import logging
from typing import Any

Expand All @@ -10,10 +11,12 @@

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
Expand Down Expand Up @@ -91,6 +94,8 @@ def __init__(
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:
Expand Down Expand Up @@ -127,19 +132,41 @@ def device_info(self) -> DeviceInfo | None:
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:
Expand All @@ -160,15 +187,16 @@ async def async_turn_on(self, **kwargs: Any) -> None:
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:
Expand All @@ -177,9 +205,20 @@ async def async_turn_on(self, **kwargs: Any) -> None:
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()

Expand Down Expand Up @@ -232,6 +271,9 @@ async def async_update(self) -> None:
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)

Expand All @@ -245,3 +287,16 @@ async def async_update(self) -> None:
"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
2 changes: 1 addition & 1 deletion homeassistant/components/twinkly/manifest.json
Expand Up @@ -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_*" }],
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions tests/components/twinkly/__init__.py
Expand Up @@ -22,6 +22,9 @@ def __init__(self) -> None:
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 = {
Expand Down Expand Up @@ -81,3 +84,22 @@ async def set_static_colour(self, colour) -> None:

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
29 changes: 28 additions & 1 deletion tests/components/twinkly/test_light.py
Expand Up @@ -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):
Expand All @@ -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):
Expand Down

0 comments on commit 33cd59d

Please sign in to comment.