mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Add support for setting RGB and RGBW values for Twinkly lights (#62337)
* Change library to ttls * Add rgbw support * Add client session to config flow * Fix config flow * Adjust tests 1 * Fix more tests * Fix last tests * Add new tests * Update test for coverage * Update test for coverage 2 * Update test for coverage 3 * Change brightness to attribute * Set RGBW mode only when available * Add RGB support
This commit is contained in:
parent
5f9a351889
commit
49a32c398c
@ -1,28 +1,41 @@
|
|||||||
"""The twinkly component."""
|
"""The twinkly component."""
|
||||||
|
|
||||||
import twinkly_client
|
import asyncio
|
||||||
|
|
||||||
|
from aiohttp import ClientError
|
||||||
|
from ttls.client import Twinkly
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DOMAIN
|
from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DATA_CLIENT, DATA_DEVICE_INFO, DOMAIN
|
||||||
|
|
||||||
PLATFORMS = [Platform.LIGHT]
|
PLATFORMS = [Platform.LIGHT]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up entries from config flow."""
|
"""Set up entries from config flow."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
# We setup the client here so if at some point we add any other entity for this device,
|
# We setup the client here so if at some point we add any other entity for this device,
|
||||||
# we will be able to properly share the connection.
|
# we will be able to properly share the connection.
|
||||||
uuid = entry.data[CONF_ENTRY_ID]
|
uuid = entry.data[CONF_ENTRY_ID]
|
||||||
host = entry.data[CONF_ENTRY_HOST]
|
host = entry.data[CONF_ENTRY_HOST]
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[uuid] = twinkly_client.TwinklyClient(
|
hass.data[DOMAIN].setdefault(uuid, {})
|
||||||
host, async_get_clientsession(hass)
|
|
||||||
)
|
client = Twinkly(host, async_get_clientsession(hass))
|
||||||
|
|
||||||
|
try:
|
||||||
|
device_info = await client.get_details()
|
||||||
|
except (asyncio.TimeoutError, ClientError) as exception:
|
||||||
|
raise ConfigEntryNotReady from exception
|
||||||
|
|
||||||
|
hass.data[DOMAIN][uuid][DATA_CLIENT] = client
|
||||||
|
hass.data[DOMAIN][uuid][DATA_DEVICE_INFO] = device_info
|
||||||
|
|
||||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
@ -6,12 +6,13 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
import twinkly_client
|
from ttls.client import Twinkly
|
||||||
from voluptuous import Required, Schema
|
from voluptuous import Required, Schema
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow
|
from homeassistant import config_entries, data_entry_flow
|
||||||
from homeassistant.components import dhcp
|
from homeassistant.components import dhcp
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ENTRY_HOST,
|
CONF_ENTRY_HOST,
|
||||||
@ -45,7 +46,9 @@ class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
if host is not None:
|
if host is not None:
|
||||||
try:
|
try:
|
||||||
device_info = await twinkly_client.TwinklyClient(host).get_device_info()
|
device_info = await Twinkly(
|
||||||
|
host, async_get_clientsession(self.hass)
|
||||||
|
).get_details()
|
||||||
|
|
||||||
await self.async_set_unique_id(device_info[DEV_ID])
|
await self.async_set_unique_id(device_info[DEV_ID])
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
@ -65,9 +68,9 @@ class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
) -> data_entry_flow.FlowResult:
|
) -> data_entry_flow.FlowResult:
|
||||||
"""Handle dhcp discovery for twinkly."""
|
"""Handle dhcp discovery for twinkly."""
|
||||||
self._async_abort_entries_match({CONF_ENTRY_HOST: discovery_info.ip})
|
self._async_abort_entries_match({CONF_ENTRY_HOST: discovery_info.ip})
|
||||||
device_info = await twinkly_client.TwinklyClient(
|
device_info = await Twinkly(
|
||||||
discovery_info.ip
|
discovery_info.ip, async_get_clientsession(self.hass)
|
||||||
).get_device_info()
|
).get_details()
|
||||||
await self.async_set_unique_id(device_info[DEV_ID])
|
await self.async_set_unique_id(device_info[DEV_ID])
|
||||||
self._abort_if_unique_id_configured(
|
self._abort_if_unique_id_configured(
|
||||||
updates={CONF_ENTRY_HOST: discovery_info.ip}
|
updates={CONF_ENTRY_HOST: discovery_info.ip}
|
||||||
|
@ -15,6 +15,13 @@ ATTR_HOST = "host"
|
|||||||
DEV_ID = "uuid"
|
DEV_ID = "uuid"
|
||||||
DEV_NAME = "device_name"
|
DEV_NAME = "device_name"
|
||||||
DEV_MODEL = "product_code"
|
DEV_MODEL = "product_code"
|
||||||
|
DEV_LED_PROFILE = "led_profile"
|
||||||
|
|
||||||
|
DEV_PROFILE_RGB = "RGB"
|
||||||
|
DEV_PROFILE_RGBW = "RGBW"
|
||||||
|
|
||||||
|
DATA_CLIENT = "client"
|
||||||
|
DATA_DEVICE_INFO = "device_info"
|
||||||
|
|
||||||
HIDDEN_DEV_VALUES = (
|
HIDDEN_DEV_VALUES = (
|
||||||
"code", # This is the internal status code of the API response
|
"code", # This is the internal status code of the API response
|
||||||
|
@ -5,10 +5,15 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
|
from ttls.client import Twinkly
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
SUPPORT_BRIGHTNESS,
|
ATTR_RGB_COLOR,
|
||||||
|
ATTR_RGBW_COLOR,
|
||||||
|
COLOR_MODE_BRIGHTNESS,
|
||||||
|
COLOR_MODE_RGB,
|
||||||
|
COLOR_MODE_RGBW,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -22,8 +27,13 @@ from .const import (
|
|||||||
CONF_ENTRY_ID,
|
CONF_ENTRY_ID,
|
||||||
CONF_ENTRY_MODEL,
|
CONF_ENTRY_MODEL,
|
||||||
CONF_ENTRY_NAME,
|
CONF_ENTRY_NAME,
|
||||||
|
DATA_CLIENT,
|
||||||
|
DATA_DEVICE_INFO,
|
||||||
|
DEV_LED_PROFILE,
|
||||||
DEV_MODEL,
|
DEV_MODEL,
|
||||||
DEV_NAME,
|
DEV_NAME,
|
||||||
|
DEV_PROFILE_RGB,
|
||||||
|
DEV_PROFILE_RGBW,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
HIDDEN_DEV_VALUES,
|
HIDDEN_DEV_VALUES,
|
||||||
)
|
)
|
||||||
@ -38,7 +48,10 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Setups an entity from a config entry (UI config flow)."""
|
"""Setups an entity from a config entry (UI config flow)."""
|
||||||
|
|
||||||
entity = TwinklyLight(config_entry, hass)
|
client = hass.data[DOMAIN][config_entry.data[CONF_ENTRY_ID]][DATA_CLIENT]
|
||||||
|
device_info = hass.data[DOMAIN][config_entry.data[CONF_ENTRY_ID]][DATA_DEVICE_INFO]
|
||||||
|
|
||||||
|
entity = TwinklyLight(config_entry, client, device_info)
|
||||||
|
|
||||||
async_add_entities([entity], update_before_add=True)
|
async_add_entities([entity], update_before_add=True)
|
||||||
|
|
||||||
@ -49,34 +62,38 @@ class TwinklyLight(LightEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
conf: ConfigEntry,
|
conf: ConfigEntry,
|
||||||
hass: HomeAssistant,
|
client: Twinkly,
|
||||||
|
device_info,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a TwinklyLight entity."""
|
"""Initialize a TwinklyLight entity."""
|
||||||
self._id = conf.data[CONF_ENTRY_ID]
|
self._id = conf.data[CONF_ENTRY_ID]
|
||||||
self._hass = hass
|
|
||||||
self._conf = conf
|
self._conf = conf
|
||||||
|
|
||||||
|
if device_info.get(DEV_LED_PROFILE) == DEV_PROFILE_RGBW:
|
||||||
|
self._attr_supported_color_modes = {COLOR_MODE_RGBW}
|
||||||
|
self._attr_color_mode = COLOR_MODE_RGBW
|
||||||
|
self._attr_rgbw_color = (255, 255, 255, 0)
|
||||||
|
elif device_info.get(DEV_LED_PROFILE) == DEV_PROFILE_RGB:
|
||||||
|
self._attr_supported_color_modes = {COLOR_MODE_RGB}
|
||||||
|
self._attr_color_mode = COLOR_MODE_RGB
|
||||||
|
self._attr_rgb_color = (255, 255, 255)
|
||||||
|
else:
|
||||||
|
self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS}
|
||||||
|
self._attr_color_mode = COLOR_MODE_BRIGHTNESS
|
||||||
|
|
||||||
# Those are saved in the config entry in order to have meaningful values even
|
# Those are saved in the config entry in order to have meaningful values even
|
||||||
# if the device is currently offline.
|
# if the device is currently offline.
|
||||||
# They are expected to be updated using the device_info.
|
# They are expected to be updated using the device_info.
|
||||||
self.__name = conf.data[CONF_ENTRY_NAME]
|
self.__name = conf.data[CONF_ENTRY_NAME]
|
||||||
self.__model = conf.data[CONF_ENTRY_MODEL]
|
self.__model = conf.data[CONF_ENTRY_MODEL]
|
||||||
|
|
||||||
self._client = hass.data.get(DOMAIN, {}).get(self._id)
|
self._client = client
|
||||||
if self._client is None:
|
|
||||||
raise ValueError(f"Client for {self._id} has not been configured.")
|
|
||||||
|
|
||||||
# Set default state before any update
|
# Set default state before any update
|
||||||
self._is_on = False
|
self._is_on = False
|
||||||
self._brightness = 0
|
|
||||||
self._is_available = False
|
self._is_available = False
|
||||||
self._attributes = {ATTR_HOST: self._client.host}
|
self._attributes = {ATTR_HOST: self._client.host}
|
||||||
|
|
||||||
@property
|
|
||||||
def supported_features(self):
|
|
||||||
"""Get the features supported by this entity."""
|
|
||||||
return SUPPORT_BRIGHTNESS
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self) -> bool:
|
def should_poll(self) -> bool:
|
||||||
"""Get a boolean which indicates if this entity should be polled."""
|
"""Get a boolean which indicates if this entity should be polled."""
|
||||||
@ -126,11 +143,6 @@ class TwinklyLight(LightEntity):
|
|||||||
"""Return true if light is on."""
|
"""Return true if light is on."""
|
||||||
return self._is_on
|
return self._is_on
|
||||||
|
|
||||||
@property
|
|
||||||
def brightness(self) -> int | None:
|
|
||||||
"""Return the brightness of the light."""
|
|
||||||
return self._brightness
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> dict:
|
def extra_state_attributes(self) -> dict:
|
||||||
"""Return device specific state attributes."""
|
"""Return device specific state attributes."""
|
||||||
@ -139,7 +151,7 @@ class TwinklyLight(LightEntity):
|
|||||||
|
|
||||||
# Make sure to update any normalized property
|
# Make sure to update any normalized property
|
||||||
attributes[ATTR_HOST] = self._client.host
|
attributes[ATTR_HOST] = self._client.host
|
||||||
attributes[ATTR_BRIGHTNESS] = self._brightness
|
attributes[ATTR_BRIGHTNESS] = self._attr_brightness
|
||||||
|
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
@ -151,31 +163,62 @@ class TwinklyLight(LightEntity):
|
|||||||
# If brightness is 0, the twinkly will only "disable" the brightness,
|
# If brightness is 0, the twinkly will only "disable" the brightness,
|
||||||
# which means that it will be 100%.
|
# which means that it will be 100%.
|
||||||
if brightness == 0:
|
if brightness == 0:
|
||||||
await self._client.set_is_on(False)
|
await self._client.turn_off()
|
||||||
return
|
return
|
||||||
|
|
||||||
await self._client.set_brightness(brightness)
|
await self._client.set_brightness(brightness)
|
||||||
|
|
||||||
await self._client.set_is_on(True)
|
if ATTR_RGBW_COLOR in kwargs:
|
||||||
|
if kwargs[ATTR_RGBW_COLOR] != self._attr_rgbw_color:
|
||||||
|
self._attr_rgbw_color = kwargs[ATTR_RGBW_COLOR]
|
||||||
|
|
||||||
|
if isinstance(self._attr_rgbw_color, tuple):
|
||||||
|
|
||||||
|
await self._client.interview()
|
||||||
|
# Reagarrange from rgbw to wrgb
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
|
if kwargs[ATTR_RGB_COLOR] != self._attr_rgb_color:
|
||||||
|
self._attr_rgb_color = kwargs[ATTR_RGB_COLOR]
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if not self._is_on:
|
||||||
|
await self._client.turn_on()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs) -> None:
|
async def async_turn_off(self, **kwargs) -> None:
|
||||||
"""Turn device off."""
|
"""Turn device off."""
|
||||||
await self._client.set_is_on(False)
|
await self._client.turn_off()
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Asynchronously updates the device properties."""
|
"""Asynchronously updates the device properties."""
|
||||||
_LOGGER.info("Updating '%s'", self._client.host)
|
_LOGGER.info("Updating '%s'", self._client.host)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._is_on = await self._client.get_is_on()
|
self._is_on = await self._client.is_on()
|
||||||
|
|
||||||
self._brightness = (
|
brightness = await self._client.get_brightness()
|
||||||
int(round((await self._client.get_brightness()) * 2.55))
|
brightness_value = (
|
||||||
if self._is_on
|
int(brightness["value"]) if brightness["mode"] == "enabled" else 100
|
||||||
else 0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
device_info = await self._client.get_device_info()
|
self._attr_brightness = (
|
||||||
|
int(round(brightness_value * 2.55)) if self._is_on else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
device_info = await self._client.get_details()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
DEV_NAME in device_info
|
DEV_NAME in device_info
|
||||||
@ -191,7 +234,7 @@ class TwinklyLight(LightEntity):
|
|||||||
if self._conf is not None:
|
if self._conf is not None:
|
||||||
# If the name has changed, persist it in conf entry,
|
# If the name has changed, persist it in conf entry,
|
||||||
# so we will be able to restore this new name if hass is started while the LED string is offline.
|
# so we will be able to restore this new name if hass is started while the LED string is offline.
|
||||||
self._hass.config_entries.async_update_entry(
|
self.hass.config_entries.async_update_entry(
|
||||||
self._conf,
|
self._conf,
|
||||||
data={
|
data={
|
||||||
CONF_ENTRY_HOST: self._client.host, # this cannot change
|
CONF_ENTRY_HOST: self._client.host, # this cannot change
|
||||||
|
@ -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": ["twinkly-client==0.0.2"],
|
"requirements": ["ttls==1.4.2"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@dr1rrb"],
|
"codeowners": ["@dr1rrb"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
@ -2375,6 +2375,9 @@ tp-connected==0.0.4
|
|||||||
# homeassistant.components.transmission
|
# homeassistant.components.transmission
|
||||||
transmissionrpc==0.11
|
transmissionrpc==0.11
|
||||||
|
|
||||||
|
# homeassistant.components.twinkly
|
||||||
|
ttls==1.4.2
|
||||||
|
|
||||||
# homeassistant.components.tuya
|
# homeassistant.components.tuya
|
||||||
tuya-iot-py-sdk==0.6.6
|
tuya-iot-py-sdk==0.6.6
|
||||||
|
|
||||||
@ -2384,9 +2387,6 @@ twentemilieu==0.5.0
|
|||||||
# homeassistant.components.twilio
|
# homeassistant.components.twilio
|
||||||
twilio==6.32.0
|
twilio==6.32.0
|
||||||
|
|
||||||
# homeassistant.components.twinkly
|
|
||||||
twinkly-client==0.0.2
|
|
||||||
|
|
||||||
# homeassistant.components.rainforest_eagle
|
# homeassistant.components.rainforest_eagle
|
||||||
uEagle==0.0.2
|
uEagle==0.0.2
|
||||||
|
|
||||||
|
@ -1433,6 +1433,9 @@ total_connect_client==2021.12
|
|||||||
# homeassistant.components.transmission
|
# homeassistant.components.transmission
|
||||||
transmissionrpc==0.11
|
transmissionrpc==0.11
|
||||||
|
|
||||||
|
# homeassistant.components.twinkly
|
||||||
|
ttls==1.4.2
|
||||||
|
|
||||||
# homeassistant.components.tuya
|
# homeassistant.components.tuya
|
||||||
tuya-iot-py-sdk==0.6.6
|
tuya-iot-py-sdk==0.6.6
|
||||||
|
|
||||||
@ -1442,9 +1445,6 @@ twentemilieu==0.5.0
|
|||||||
# homeassistant.components.twilio
|
# homeassistant.components.twilio
|
||||||
twilio==6.32.0
|
twilio==6.32.0
|
||||||
|
|
||||||
# homeassistant.components.twinkly
|
|
||||||
twinkly-client==0.0.2
|
|
||||||
|
|
||||||
# homeassistant.components.rainforest_eagle
|
# homeassistant.components.rainforest_eagle
|
||||||
uEagle==0.0.2
|
uEagle==0.0.2
|
||||||
|
|
||||||
|
@ -14,13 +14,14 @@ TEST_MODEL = "twinkly_test_device_model"
|
|||||||
|
|
||||||
|
|
||||||
class ClientMock:
|
class ClientMock:
|
||||||
"""A mock of the twinkly_client.TwinklyClient."""
|
"""A mock of the ttls.client.Twinkly."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Create a mocked client."""
|
"""Create a mocked client."""
|
||||||
self.is_offline = False
|
self.is_offline = False
|
||||||
self.is_on = True
|
self.state = True
|
||||||
self.brightness = 10
|
self.brightness = {"mode": "enabled", "value": 10}
|
||||||
|
self.color = None
|
||||||
|
|
||||||
self.id = str(uuid4())
|
self.id = str(uuid4())
|
||||||
self.device_info = {
|
self.device_info = {
|
||||||
@ -34,23 +35,29 @@ class ClientMock:
|
|||||||
"""Get the mocked host."""
|
"""Get the mocked host."""
|
||||||
return TEST_HOST
|
return TEST_HOST
|
||||||
|
|
||||||
async def get_device_info(self):
|
async def get_details(self):
|
||||||
"""Get the mocked device info."""
|
"""Get the mocked device info."""
|
||||||
if self.is_offline:
|
if self.is_offline:
|
||||||
raise ClientConnectionError()
|
raise ClientConnectionError()
|
||||||
return self.device_info
|
return self.device_info
|
||||||
|
|
||||||
async def get_is_on(self) -> bool:
|
async def is_on(self) -> bool:
|
||||||
"""Get the mocked on/off state."""
|
"""Get the mocked on/off state."""
|
||||||
if self.is_offline:
|
if self.is_offline:
|
||||||
raise ClientConnectionError()
|
raise ClientConnectionError()
|
||||||
return self.is_on
|
return self.state
|
||||||
|
|
||||||
async def set_is_on(self, is_on: bool) -> None:
|
async def turn_on(self) -> None:
|
||||||
"""Set the mocked on/off state."""
|
"""Set the mocked on state."""
|
||||||
if self.is_offline:
|
if self.is_offline:
|
||||||
raise ClientConnectionError()
|
raise ClientConnectionError()
|
||||||
self.is_on = is_on
|
self.state = True
|
||||||
|
|
||||||
|
async def turn_off(self) -> None:
|
||||||
|
"""Set the mocked off state."""
|
||||||
|
if self.is_offline:
|
||||||
|
raise ClientConnectionError()
|
||||||
|
self.state = False
|
||||||
|
|
||||||
async def get_brightness(self) -> int:
|
async def get_brightness(self) -> int:
|
||||||
"""Get the mocked brightness."""
|
"""Get the mocked brightness."""
|
||||||
@ -62,8 +69,15 @@ class ClientMock:
|
|||||||
"""Set the mocked brightness."""
|
"""Set the mocked brightness."""
|
||||||
if self.is_offline:
|
if self.is_offline:
|
||||||
raise ClientConnectionError()
|
raise ClientConnectionError()
|
||||||
self.brightness = brightness
|
self.brightness = {"mode": "enabled", "value": brightness}
|
||||||
|
|
||||||
def change_name(self, new_name: str) -> None:
|
def change_name(self, new_name: str) -> None:
|
||||||
"""Change the name of this virtual device."""
|
"""Change the name of this virtual device."""
|
||||||
self.device_info[DEV_NAME] = new_name
|
self.device_info[DEV_NAME] = new_name
|
||||||
|
|
||||||
|
async def set_static_colour(self, colour) -> None:
|
||||||
|
"""Set static color."""
|
||||||
|
self.color = colour
|
||||||
|
|
||||||
|
async def interview(self) -> None:
|
||||||
|
"""Interview."""
|
||||||
|
@ -20,7 +20,9 @@ async def test_invalid_host(hass):
|
|||||||
"""Test the failure when invalid host provided."""
|
"""Test the failure when invalid host provided."""
|
||||||
client = ClientMock()
|
client = ClientMock()
|
||||||
client.is_offline = True
|
client.is_offline = True
|
||||||
with patch("twinkly_client.TwinklyClient", return_value=client):
|
with patch(
|
||||||
|
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER}
|
TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
@ -40,7 +42,9 @@ async def test_invalid_host(hass):
|
|||||||
async def test_success_flow(hass):
|
async def test_success_flow(hass):
|
||||||
"""Test that an entity is created when the flow completes."""
|
"""Test that an entity is created when the flow completes."""
|
||||||
client = ClientMock()
|
client = ClientMock()
|
||||||
with patch("twinkly_client.TwinklyClient", return_value=client):
|
with patch(
|
||||||
|
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
|
||||||
|
), patch("homeassistant.components.twinkly.async_setup_entry", return_value=True):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER}
|
TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
@ -67,7 +71,9 @@ async def test_success_flow(hass):
|
|||||||
async def test_dhcp_can_confirm(hass):
|
async def test_dhcp_can_confirm(hass):
|
||||||
"""Test DHCP discovery flow can confirm right away."""
|
"""Test DHCP discovery flow can confirm right away."""
|
||||||
client = ClientMock()
|
client = ClientMock()
|
||||||
with patch("twinkly_client.TwinklyClient", return_value=client):
|
with patch(
|
||||||
|
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
TWINKLY_DOMAIN,
|
TWINKLY_DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_DHCP},
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
@ -86,7 +92,9 @@ async def test_dhcp_can_confirm(hass):
|
|||||||
async def test_dhcp_success(hass):
|
async def test_dhcp_success(hass):
|
||||||
"""Test DHCP discovery flow success."""
|
"""Test DHCP discovery flow success."""
|
||||||
client = ClientMock()
|
client = ClientMock()
|
||||||
with patch("twinkly_client.TwinklyClient", return_value=client):
|
with patch(
|
||||||
|
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
|
||||||
|
), patch("homeassistant.components.twinkly.async_setup_entry", return_value=True):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
TWINKLY_DOMAIN,
|
TWINKLY_DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_DHCP},
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
@ -129,7 +137,9 @@ async def test_dhcp_already_exists(hass):
|
|||||||
)
|
)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
with patch("twinkly_client.TwinklyClient", return_value=client):
|
with patch(
|
||||||
|
"homeassistant.components.twinkly.config_flow.Twinkly", return_value=client
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
TWINKLY_DOMAIN,
|
TWINKLY_DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_DHCP},
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
|
@ -11,14 +11,21 @@ from homeassistant.components.twinkly.const import (
|
|||||||
CONF_ENTRY_NAME,
|
CONF_ENTRY_NAME,
|
||||||
DOMAIN as TWINKLY_DOMAIN,
|
DOMAIN as TWINKLY_DOMAIN,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.components.twinkly import TEST_HOST, TEST_MODEL, TEST_NAME_ORIGINAL
|
from tests.components.twinkly import (
|
||||||
|
TEST_HOST,
|
||||||
|
TEST_MODEL,
|
||||||
|
TEST_NAME_ORIGINAL,
|
||||||
|
ClientMock,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_entry(hass: HomeAssistant):
|
async def test_setup_entry(hass: HomeAssistant):
|
||||||
"""Validate that setup entry also configure the client."""
|
"""Validate that setup entry also configure the client."""
|
||||||
|
client = ClientMock()
|
||||||
|
|
||||||
id = str(uuid4())
|
id = str(uuid4())
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
@ -38,7 +45,7 @@ async def test_setup_entry(hass: HomeAssistant):
|
|||||||
with patch(
|
with patch(
|
||||||
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
|
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
|
||||||
side_effect=setup_mock,
|
side_effect=setup_mock,
|
||||||
):
|
), patch("homeassistant.components.twinkly.Twinkly", return_value=client):
|
||||||
await async_setup_entry(hass, config_entry)
|
await async_setup_entry(hass, config_entry)
|
||||||
|
|
||||||
assert hass.data[TWINKLY_DOMAIN][id] is not None
|
assert hass.data[TWINKLY_DOMAIN][id] is not None
|
||||||
@ -65,3 +72,26 @@ async def test_unload_entry(hass: HomeAssistant):
|
|||||||
await async_unload_entry(hass, config_entry)
|
await async_unload_entry(hass, config_entry)
|
||||||
|
|
||||||
assert hass.data[TWINKLY_DOMAIN].get(id) is None
|
assert hass.data[TWINKLY_DOMAIN].get(id) is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_entry_not_ready(hass: HomeAssistant):
|
||||||
|
"""Validate that config entry is retried."""
|
||||||
|
client = ClientMock()
|
||||||
|
client.is_offline = True
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=TWINKLY_DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_ENTRY_HOST: TEST_HOST,
|
||||||
|
CONF_ENTRY_ID: id,
|
||||||
|
CONF_ENTRY_NAME: TEST_NAME_ORIGINAL,
|
||||||
|
CONF_ENTRY_MODEL: TEST_MODEL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.twinkly.Twinkly", return_value=client):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
|
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
@ -10,7 +10,6 @@ from homeassistant.components.twinkly.const import (
|
|||||||
CONF_ENTRY_NAME,
|
CONF_ENTRY_NAME,
|
||||||
DOMAIN as TWINKLY_DOMAIN,
|
DOMAIN as TWINKLY_DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.components.twinkly.light import TwinklyLight
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.device_registry import DeviceEntry
|
from homeassistant.helpers.device_registry import DeviceEntry
|
||||||
@ -19,31 +18,12 @@ from homeassistant.helpers.entity_registry import RegistryEntry
|
|||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.components.twinkly import (
|
from tests.components.twinkly import (
|
||||||
TEST_HOST,
|
TEST_HOST,
|
||||||
TEST_ID,
|
|
||||||
TEST_MODEL,
|
TEST_MODEL,
|
||||||
TEST_NAME_ORIGINAL,
|
TEST_NAME_ORIGINAL,
|
||||||
ClientMock,
|
ClientMock,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_missing_client(hass: HomeAssistant):
|
|
||||||
"""Validate that if client has not been setup, it fails immediately in setup."""
|
|
||||||
try:
|
|
||||||
config_entry = MockConfigEntry(
|
|
||||||
data={
|
|
||||||
CONF_ENTRY_HOST: TEST_HOST,
|
|
||||||
CONF_ENTRY_ID: TEST_ID,
|
|
||||||
CONF_ENTRY_NAME: TEST_NAME_ORIGINAL,
|
|
||||||
CONF_ENTRY_MODEL: TEST_MODEL,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
TwinklyLight(config_entry, hass)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
|
|
||||||
assert False
|
|
||||||
|
|
||||||
|
|
||||||
async def test_initial_state(hass: HomeAssistant):
|
async def test_initial_state(hass: HomeAssistant):
|
||||||
"""Validate that entity and device states are updated on startup."""
|
"""Validate that entity and device states are updated on startup."""
|
||||||
entity, device, _ = await _create_entries(hass)
|
entity, device, _ = await _create_entries(hass)
|
||||||
@ -69,32 +49,11 @@ async def test_initial_state(hass: HomeAssistant):
|
|||||||
assert device.manufacturer == "LEDWORKS"
|
assert device.manufacturer == "LEDWORKS"
|
||||||
|
|
||||||
|
|
||||||
async def test_initial_state_offline(hass: HomeAssistant):
|
async def test_turn_on_off(hass: HomeAssistant):
|
||||||
"""Validate that entity and device are restored from config is offline on startup."""
|
|
||||||
client = ClientMock()
|
|
||||||
client.is_offline = True
|
|
||||||
entity, device, _ = await _create_entries(hass, client)
|
|
||||||
|
|
||||||
state = hass.states.get(entity.entity_id)
|
|
||||||
|
|
||||||
assert state.name == TEST_NAME_ORIGINAL
|
|
||||||
assert state.state == "unavailable"
|
|
||||||
assert state.attributes["friendly_name"] == TEST_NAME_ORIGINAL
|
|
||||||
assert state.attributes["icon"] == "mdi:string-lights"
|
|
||||||
|
|
||||||
assert entity.original_name == TEST_NAME_ORIGINAL
|
|
||||||
assert entity.original_icon == "mdi:string-lights"
|
|
||||||
|
|
||||||
assert device.name == TEST_NAME_ORIGINAL
|
|
||||||
assert device.model == TEST_MODEL
|
|
||||||
assert device.manufacturer == "LEDWORKS"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_on(hass: HomeAssistant):
|
|
||||||
"""Test support of the light.turn_on service."""
|
"""Test support of the light.turn_on service."""
|
||||||
client = ClientMock()
|
client = ClientMock()
|
||||||
client.is_on = False
|
client.state = False
|
||||||
client.brightness = 20
|
client.brightness = {"mode": "enabled", "value": 20}
|
||||||
entity, _, _ = await _create_entries(hass, client)
|
entity, _, _ = await _create_entries(hass, client)
|
||||||
|
|
||||||
assert hass.states.get(entity.entity_id).state == "off"
|
assert hass.states.get(entity.entity_id).state == "off"
|
||||||
@ -113,8 +72,8 @@ async def test_turn_on(hass: HomeAssistant):
|
|||||||
async def test_turn_on_with_brightness(hass: HomeAssistant):
|
async def test_turn_on_with_brightness(hass: HomeAssistant):
|
||||||
"""Test support of the light.turn_on service with a brightness parameter."""
|
"""Test support of the light.turn_on service with a brightness parameter."""
|
||||||
client = ClientMock()
|
client = ClientMock()
|
||||||
client.is_on = False
|
client.state = False
|
||||||
client.brightness = 20
|
client.brightness = {"mode": "enabled", "value": 20}
|
||||||
entity, _, _ = await _create_entries(hass, client)
|
entity, _, _ = await _create_entries(hass, client)
|
||||||
|
|
||||||
assert hass.states.get(entity.entity_id).state == "off"
|
assert hass.states.get(entity.entity_id).state == "off"
|
||||||
@ -131,6 +90,64 @@ async def test_turn_on_with_brightness(hass: HomeAssistant):
|
|||||||
assert state.state == "on"
|
assert state.state == "on"
|
||||||
assert state.attributes["brightness"] == 255
|
assert state.attributes["brightness"] == 255
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
service_data={"entity_id": entity.entity_id, "brightness": 1},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity.entity_id)
|
||||||
|
|
||||||
|
assert state.state == "off"
|
||||||
|
assert state.attributes["brightness"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_on_with_color_rgbw(hass: HomeAssistant):
|
||||||
|
"""Test support of the light.turn_on service with a brightness parameter."""
|
||||||
|
client = ClientMock()
|
||||||
|
client.state = False
|
||||||
|
client.device_info["led_profile"] = "RGBW"
|
||||||
|
client.brightness = {"mode": "enabled", "value": 255}
|
||||||
|
entity, _, _ = await _create_entries(hass, client)
|
||||||
|
|
||||||
|
assert hass.states.get(entity.entity_id).state == "off"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
service_data={"entity_id": entity.entity_id, "rgbw_color": (128, 64, 32, 0)},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity.entity_id)
|
||||||
|
|
||||||
|
assert state.state == "on"
|
||||||
|
assert client.color == (0, 128, 64, 32)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_on_with_color_rgb(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"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"light",
|
||||||
|
"turn_on",
|
||||||
|
service_data={"entity_id": entity.entity_id, "rgb_color": (128, 64, 32)},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity.entity_id)
|
||||||
|
|
||||||
|
assert state.state == "on"
|
||||||
|
assert client.color == (128, 64, 32)
|
||||||
|
|
||||||
|
|
||||||
async def test_turn_off(hass: HomeAssistant):
|
async def test_turn_off(hass: HomeAssistant):
|
||||||
"""Test support of the light.turn_off service."""
|
"""Test support of the light.turn_off service."""
|
||||||
@ -194,10 +211,7 @@ async def _create_entries(
|
|||||||
) -> tuple[RegistryEntry, DeviceEntry, ClientMock]:
|
) -> tuple[RegistryEntry, DeviceEntry, ClientMock]:
|
||||||
client = ClientMock() if client is None else client
|
client = ClientMock() if client is None else client
|
||||||
|
|
||||||
def get_client_mock(client, _):
|
with patch("homeassistant.components.twinkly.Twinkly", return_value=client):
|
||||||
return client
|
|
||||||
|
|
||||||
with patch("twinkly_client.TwinklyClient", side_effect=get_client_mock):
|
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
domain=TWINKLY_DOMAIN,
|
domain=TWINKLY_DOMAIN,
|
||||||
data={
|
data={
|
||||||
|
Loading…
x
Reference in New Issue
Block a user