378 lines
13 KiB
Python
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
|