core/homeassistant/components/hyperion/light.py

378 lines
13 KiB
Python

"""Support for Hyperion-NG remotes."""
import logging
from hyperion import client, const
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_EFFECT,
ATTR_HS_COLOR,
PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_EFFECT,
LightEntity,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
CONF_DEFAULT_COLOR = "default_color"
CONF_PRIORITY = "priority"
CONF_HDMI_PRIORITY = "hdmi_priority"
CONF_EFFECT_LIST = "effect_list"
# As we want to preserve brightness control for effects (e.g. to reduce the
# brightness for V4L), we need to persist the effect that is in flight, so
# subsequent calls to turn_on will know the keep the effect enabled.
# Unfortunately the Home Assistant UI does not easily expose a way to remove a
# selected effect (there is no 'No Effect' option by default). Instead, we
# create a new fake effect ("Solid") that is always selected by default for
# showing a solid color. This is the same method used by WLED.
KEY_EFFECT_SOLID = "Solid"
DEFAULT_COLOR = [255, 255, 255]
DEFAULT_BRIGHTNESS = 255
DEFAULT_EFFECT = KEY_EFFECT_SOLID
DEFAULT_NAME = "Hyperion"
DEFAULT_ORIGIN = "Home Assistant"
DEFAULT_PORT = 19444
DEFAULT_PRIORITY = 128
DEFAULT_HDMI_PRIORITY = 880
DEFAULT_EFFECT_LIST = []
SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT
PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"),
cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"),
cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"),
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): vol.All(
list,
vol.Length(min=3, max=3),
[vol.All(vol.Coerce(int), vol.Range(min=0, max=255))],
),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int,
vol.Optional(
CONF_HDMI_PRIORITY, default=DEFAULT_HDMI_PRIORITY
): cv.positive_int,
vol.Optional(CONF_EFFECT_LIST, default=DEFAULT_EFFECT_LIST): vol.All(
cv.ensure_list, [cv.string]
),
}
),
)
ICON_LIGHTBULB = "mdi:lightbulb"
ICON_EFFECT = "mdi:lava-lamp"
ICON_EXTERNAL_SOURCE = "mdi:television-ambient-light"
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Hyperion server remote."""
name = config[CONF_NAME]
host = config[CONF_HOST]
port = config[CONF_PORT]
priority = config[CONF_PRIORITY]
hyperion_client = client.HyperionClient(host, port)
if not await hyperion_client.async_client_connect():
raise PlatformNotReady
async_add_entities([Hyperion(name, priority, hyperion_client)])
class Hyperion(LightEntity):
"""Representation of a Hyperion remote."""
def __init__(self, name, priority, hyperion_client):
"""Initialize the light."""
self._name = name
self._priority = priority
self._client = hyperion_client
# Active state representing the Hyperion instance.
self._set_internal_state(
brightness=255, rgb_color=DEFAULT_COLOR, effect=KEY_EFFECT_SOLID
)
self._effect_list = []
@property
def should_poll(self):
"""Return whether or not this entity should be polled."""
return False
@property
def name(self):
"""Return the name of the light."""
return self._name
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
@property
def hs_color(self):
"""Return last color value set."""
return color_util.color_RGB_to_hs(*self._rgb_color)
@property
def is_on(self):
"""Return true if not black."""
return self._client.is_on()
@property
def icon(self):
"""Return state specific icon."""
return self._icon
@property
def effect(self):
"""Return the current effect."""
return self._effect
@property
def effect_list(self):
"""Return the list of supported effects."""
return (
self._effect_list
+ const.KEY_COMPONENTID_EXTERNAL_SOURCES
+ [KEY_EFFECT_SOLID]
)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_HYPERION
@property
def available(self):
"""Return server availability."""
return self._client.has_loaded_state
@property
def unique_id(self):
"""Return a unique id for this instance."""
return self._client.id
async def async_turn_on(self, **kwargs):
"""Turn the lights on."""
# == Turn device on ==
# Turn on both ALL (Hyperion itself) and LEDDEVICE. It would be
# preferable to enable LEDDEVICE after the settings (e.g. brightness,
# color, effect), but this is not possible due to:
# https://github.com/hyperion-project/hyperion.ng/issues/967
if not self.is_on:
if not await self._client.async_send_set_component(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL,
const.KEY_STATE: True,
}
}
):
return
if not await self._client.async_send_set_component(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
const.KEY_STATE: True,
}
}
):
return
# == Get key parameters ==
brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness)
effect = kwargs.get(ATTR_EFFECT, self._effect)
if ATTR_HS_COLOR in kwargs:
rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
else:
rgb_color = self._rgb_color
# == Set brightness ==
if self._brightness != brightness:
if not await self._client.async_send_set_adjustment(
**{
const.KEY_ADJUSTMENT: {
const.KEY_BRIGHTNESS: int(
round((float(brightness) * 100) / 255)
)
}
}
):
return
# == Set an external source
if effect and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
# Clear any color/effect.
if not await self._client.async_send_clear(
**{const.KEY_PRIORITY: self._priority}
):
return
# Turn off all external sources, except the intended.
for key in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
if not await self._client.async_send_set_component(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: key,
const.KEY_STATE: effect == key,
}
}
):
return
# == Set an effect
elif effect and effect != KEY_EFFECT_SOLID:
# This call should not be necessary, but without it there is no priorities-update issued:
# https://github.com/hyperion-project/hyperion.ng/issues/992
if not await self._client.async_send_clear(
**{const.KEY_PRIORITY: self._priority}
):
return
if not await self._client.async_send_set_effect(
**{
const.KEY_PRIORITY: self._priority,
const.KEY_EFFECT: {const.KEY_NAME: effect},
const.KEY_ORIGIN: DEFAULT_ORIGIN,
}
):
return
# == Set a color
else:
if not await self._client.async_send_set_color(
**{
const.KEY_PRIORITY: self._priority,
const.KEY_COLOR: rgb_color,
const.KEY_ORIGIN: DEFAULT_ORIGIN,
}
):
return
async def async_turn_off(self, **kwargs):
"""Disable the LED output component."""
if not await self._client.async_send_set_component(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
const.KEY_STATE: False,
}
}
):
return
def _set_internal_state(self, brightness=None, rgb_color=None, effect=None):
"""Set the internal state."""
if brightness is not None:
self._brightness = brightness
if rgb_color is not None:
self._rgb_color = rgb_color
if effect is not None:
self._effect = effect
if effect == KEY_EFFECT_SOLID:
self._icon = ICON_LIGHTBULB
elif effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
self._icon = ICON_EXTERNAL_SOURCE
else:
self._icon = ICON_EFFECT
def _update_components(self, _=None):
"""Update Hyperion components."""
self.async_write_ha_state()
def _update_adjustment(self, _=None):
"""Update Hyperion adjustments."""
if self._client.adjustment:
brightness_pct = self._client.adjustment[0].get(
const.KEY_BRIGHTNESS, DEFAULT_BRIGHTNESS
)
if brightness_pct < 0 or brightness_pct > 100:
return
self._set_internal_state(
brightness=int(round((brightness_pct * 255) / float(100)))
)
self.async_write_ha_state()
def _update_priorities(self, _=None):
"""Update Hyperion priorities."""
visible_priority = self._client.visible_priority
if visible_priority:
componentid = visible_priority.get(const.KEY_COMPONENTID)
if componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid)
elif componentid == const.KEY_COMPONENTID_EFFECT:
# Owner is the effect name.
# See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities
self._set_internal_state(
rgb_color=DEFAULT_COLOR, effect=visible_priority[const.KEY_OWNER]
)
elif componentid == const.KEY_COMPONENTID_COLOR:
self._set_internal_state(
rgb_color=visible_priority[const.KEY_VALUE][const.KEY_RGB],
effect=KEY_EFFECT_SOLID,
)
self.async_write_ha_state()
def _update_effect_list(self, _=None):
"""Update Hyperion effects."""
if not self._client.effects:
return
effect_list = []
for effect in self._client.effects or []:
if const.KEY_NAME in effect:
effect_list.append(effect[const.KEY_NAME])
if effect_list:
self._effect_list = effect_list
self.async_write_ha_state()
def _update_full_state(self):
"""Update full Hyperion state."""
self._update_adjustment()
self._update_priorities()
self._update_effect_list()
_LOGGER.debug(
"Hyperion full state update: On=%s,Brightness=%i,Effect=%s "
"(%i effects total),Color=%s",
self.is_on,
self._brightness,
self._effect,
len(self._effect_list),
self._rgb_color,
)
def _update_client(self, json):
"""Update client connection state."""
self.async_write_ha_state()
async def async_added_to_hass(self):
"""Register callbacks when entity added to hass."""
self._client.set_callbacks(
{
f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment,
f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}": self._update_components,
f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}": self._update_effect_list,
f"{const.KEY_PRIORITIES}-{const.KEY_UPDATE}": self._update_priorities,
f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client,
}
)
# Load initial state.
self._update_full_state()
return True