2020-11-19 17:22:12 +00:00
|
|
|
"""The Twinkly light component."""
|
2021-03-18 13:43:52 +00:00
|
|
|
from __future__ import annotations
|
2020-11-19 17:22:12 +00:00
|
|
|
|
|
|
|
import asyncio
|
|
|
|
import logging
|
|
|
|
|
|
|
|
from aiohttp import ClientError
|
|
|
|
|
|
|
|
from homeassistant.components.light import (
|
|
|
|
ATTR_BRIGHTNESS,
|
|
|
|
SUPPORT_BRIGHTNESS,
|
|
|
|
LightEntity,
|
|
|
|
)
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2021-04-22 14:53:57 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
2021-04-30 21:21:39 +00:00
|
|
|
from homeassistant.helpers.entity import DeviceInfo
|
2020-11-19 17:22:12 +00:00
|
|
|
|
|
|
|
from .const import (
|
|
|
|
ATTR_HOST,
|
|
|
|
CONF_ENTRY_HOST,
|
|
|
|
CONF_ENTRY_ID,
|
|
|
|
CONF_ENTRY_MODEL,
|
|
|
|
CONF_ENTRY_NAME,
|
|
|
|
DEV_MODEL,
|
|
|
|
DEV_NAME,
|
|
|
|
DOMAIN,
|
|
|
|
HIDDEN_DEV_VALUES,
|
|
|
|
)
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(
|
2021-04-22 14:53:57 +00:00
|
|
|
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
|
2020-11-19 17:22:12 +00:00
|
|
|
) -> None:
|
|
|
|
"""Setups an entity from a config entry (UI config flow)."""
|
|
|
|
|
|
|
|
entity = TwinklyLight(config_entry, hass)
|
|
|
|
|
|
|
|
async_add_entities([entity], update_before_add=True)
|
|
|
|
|
|
|
|
|
|
|
|
class TwinklyLight(LightEntity):
|
|
|
|
"""Implementation of the light for the Twinkly service."""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
conf: ConfigEntry,
|
2021-04-22 14:53:57 +00:00
|
|
|
hass: HomeAssistant,
|
2021-05-20 15:00:19 +00:00
|
|
|
) -> None:
|
2020-11-19 17:22:12 +00:00
|
|
|
"""Initialize a TwinklyLight entity."""
|
|
|
|
self._id = conf.data[CONF_ENTRY_ID]
|
|
|
|
self._hass = hass
|
|
|
|
self._conf = conf
|
|
|
|
|
|
|
|
# Those are saved in the config entry in order to have meaningful values even
|
|
|
|
# if the device is currently offline.
|
|
|
|
# They are expected to be updated using the device_info.
|
|
|
|
self.__name = conf.data[CONF_ENTRY_NAME]
|
|
|
|
self.__model = conf.data[CONF_ENTRY_MODEL]
|
|
|
|
|
|
|
|
self._client = hass.data.get(DOMAIN, {}).get(self._id)
|
|
|
|
if self._client is None:
|
|
|
|
raise ValueError(f"Client for {self._id} has not been configured.")
|
|
|
|
|
|
|
|
# Set default state before any update
|
|
|
|
self._is_on = False
|
|
|
|
self._brightness = 0
|
|
|
|
self._is_available = False
|
|
|
|
self._attributes = {ATTR_HOST: self._client.host}
|
|
|
|
|
|
|
|
@property
|
|
|
|
def supported_features(self):
|
|
|
|
"""Get the features supported by this entity."""
|
|
|
|
return SUPPORT_BRIGHTNESS
|
|
|
|
|
|
|
|
@property
|
|
|
|
def should_poll(self) -> bool:
|
|
|
|
"""Get a boolean which indicates if this entity should be polled."""
|
|
|
|
return True
|
|
|
|
|
|
|
|
@property
|
|
|
|
def available(self) -> bool:
|
|
|
|
"""Get a boolean which indicates if this entity is currently available."""
|
|
|
|
return self._is_available
|
|
|
|
|
|
|
|
@property
|
2021-03-18 13:43:52 +00:00
|
|
|
def unique_id(self) -> str | None:
|
2020-11-19 17:22:12 +00:00
|
|
|
"""Id of the device."""
|
|
|
|
return self._id
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
"""Name of the device."""
|
|
|
|
return self.__name if self.__name else "Twinkly light"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def model(self) -> str:
|
|
|
|
"""Name of the device."""
|
|
|
|
return self.__model
|
|
|
|
|
|
|
|
@property
|
|
|
|
def icon(self) -> str:
|
|
|
|
"""Icon of the device."""
|
|
|
|
return "mdi:string-lights"
|
|
|
|
|
|
|
|
@property
|
2021-04-30 21:21:39 +00:00
|
|
|
def device_info(self) -> DeviceInfo | None:
|
2020-11-19 17:22:12 +00:00
|
|
|
"""Get device specific attributes."""
|
|
|
|
return (
|
|
|
|
{
|
|
|
|
"identifiers": {(DOMAIN, self._id)},
|
|
|
|
"name": self.name,
|
|
|
|
"manufacturer": "LEDWORKS",
|
|
|
|
"model": self.model,
|
|
|
|
}
|
|
|
|
if self._id
|
|
|
|
else None # device_info is available only for entities configured from the UI
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_on(self) -> bool:
|
|
|
|
"""Return true if light is on."""
|
|
|
|
return self._is_on
|
|
|
|
|
|
|
|
@property
|
2021-03-18 13:43:52 +00:00
|
|
|
def brightness(self) -> int | None:
|
2020-11-19 17:22:12 +00:00
|
|
|
"""Return the brightness of the light."""
|
|
|
|
return self._brightness
|
|
|
|
|
|
|
|
@property
|
2021-03-21 09:38:24 +00:00
|
|
|
def extra_state_attributes(self) -> dict:
|
2020-11-19 17:22:12 +00:00
|
|
|
"""Return device specific state attributes."""
|
|
|
|
|
|
|
|
attributes = self._attributes
|
|
|
|
|
|
|
|
# Make sure to update any normalized property
|
|
|
|
attributes[ATTR_HOST] = self._client.host
|
|
|
|
attributes[ATTR_BRIGHTNESS] = self._brightness
|
|
|
|
|
|
|
|
return attributes
|
|
|
|
|
|
|
|
async def async_turn_on(self, **kwargs) -> None:
|
|
|
|
"""Turn device on."""
|
|
|
|
if ATTR_BRIGHTNESS in kwargs:
|
|
|
|
brightness = int(int(kwargs[ATTR_BRIGHTNESS]) / 2.55)
|
|
|
|
|
|
|
|
# If brightness is 0, the twinkly will only "disable" the brightness,
|
|
|
|
# which means that it will be 100%.
|
|
|
|
if brightness == 0:
|
|
|
|
await self._client.set_is_on(False)
|
|
|
|
return
|
|
|
|
|
|
|
|
await self._client.set_brightness(brightness)
|
|
|
|
|
|
|
|
await self._client.set_is_on(True)
|
|
|
|
|
|
|
|
async def async_turn_off(self, **kwargs) -> None:
|
|
|
|
"""Turn device off."""
|
|
|
|
await self._client.set_is_on(False)
|
|
|
|
|
|
|
|
async def async_update(self) -> None:
|
|
|
|
"""Asynchronously updates the device properties."""
|
|
|
|
_LOGGER.info("Updating '%s'", self._client.host)
|
|
|
|
|
|
|
|
try:
|
|
|
|
self._is_on = await self._client.get_is_on()
|
|
|
|
|
|
|
|
self._brightness = (
|
|
|
|
int(round((await self._client.get_brightness()) * 2.55))
|
|
|
|
if self._is_on
|
|
|
|
else 0
|
|
|
|
)
|
|
|
|
|
|
|
|
device_info = await self._client.get_device_info()
|
|
|
|
|
|
|
|
if (
|
|
|
|
DEV_NAME in device_info
|
|
|
|
and DEV_MODEL in device_info
|
|
|
|
and (
|
|
|
|
device_info[DEV_NAME] != self.__name
|
|
|
|
or device_info[DEV_MODEL] != self.__model
|
|
|
|
)
|
|
|
|
):
|
|
|
|
self.__name = device_info[DEV_NAME]
|
|
|
|
self.__model = device_info[DEV_MODEL]
|
|
|
|
|
|
|
|
if self._conf is not None:
|
|
|
|
# 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.
|
|
|
|
self._hass.config_entries.async_update_entry(
|
|
|
|
self._conf,
|
|
|
|
data={
|
|
|
|
CONF_ENTRY_HOST: self._client.host, # this cannot change
|
|
|
|
CONF_ENTRY_ID: self._id, # this cannot change
|
|
|
|
CONF_ENTRY_NAME: self.__name,
|
|
|
|
CONF_ENTRY_MODEL: self.__model,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
for key, value in device_info.items():
|
|
|
|
if key not in HIDDEN_DEV_VALUES:
|
|
|
|
self._attributes[key] = value
|
|
|
|
|
|
|
|
if not self._is_available:
|
|
|
|
_LOGGER.info("Twinkly '%s' is now available", self._client.host)
|
|
|
|
|
|
|
|
# We don't use the echo API to track the availability since we already have to pull
|
|
|
|
# the device to get its state.
|
|
|
|
self._is_available = True
|
|
|
|
except (asyncio.TimeoutError, ClientError):
|
|
|
|
# We log this as "info" as it's pretty common that the christmas light are not reachable in july
|
|
|
|
if self._is_available:
|
|
|
|
_LOGGER.info(
|
|
|
|
"Twinkly '%s' is not reachable (client error)", self._client.host
|
|
|
|
)
|
|
|
|
self._is_available = False
|