647 lines
23 KiB
Python
647 lines
23 KiB
Python
"""Support for Hyperion-NG remotes."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping, Sequence
|
|
import functools
|
|
import logging
|
|
from types import MappingProxyType
|
|
from typing import Any, Callable
|
|
|
|
from hyperion import client, const
|
|
|
|
from homeassistant.components.light import (
|
|
ATTR_BRIGHTNESS,
|
|
ATTR_EFFECT,
|
|
ATTR_HS_COLOR,
|
|
SUPPORT_BRIGHTNESS,
|
|
SUPPORT_COLOR,
|
|
SUPPORT_EFFECT,
|
|
LightEntity,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers.dispatcher import (
|
|
async_dispatcher_connect,
|
|
async_dispatcher_send,
|
|
)
|
|
from homeassistant.helpers.entity import DeviceInfo
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
import homeassistant.util.color as color_util
|
|
|
|
from . import (
|
|
get_hyperion_device_id,
|
|
get_hyperion_unique_id,
|
|
listen_for_instance_updates,
|
|
)
|
|
from .const import (
|
|
CONF_EFFECT_HIDE_LIST,
|
|
CONF_INSTANCE_CLIENTS,
|
|
CONF_PRIORITY,
|
|
DEFAULT_ORIGIN,
|
|
DEFAULT_PRIORITY,
|
|
DOMAIN,
|
|
HYPERION_MANUFACTURER_NAME,
|
|
HYPERION_MODEL_NAME,
|
|
NAME_SUFFIX_HYPERION_LIGHT,
|
|
NAME_SUFFIX_HYPERION_PRIORITY_LIGHT,
|
|
SIGNAL_ENTITY_REMOVE,
|
|
TYPE_HYPERION_LIGHT,
|
|
TYPE_HYPERION_PRIORITY_LIGHT,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
COLOR_BLACK = color_util.COLORS["black"]
|
|
|
|
CONF_DEFAULT_COLOR = "default_color"
|
|
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_PORT = const.DEFAULT_PORT_JSON
|
|
DEFAULT_HDMI_PRIORITY = 880
|
|
DEFAULT_EFFECT_LIST: list[str] = []
|
|
|
|
SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT
|
|
|
|
ICON_LIGHTBULB = "mdi:lightbulb"
|
|
ICON_EFFECT = "mdi:lava-lamp"
|
|
ICON_EXTERNAL_SOURCE = "mdi:television-ambient-light"
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> bool:
|
|
"""Set up a Hyperion platform from config entry."""
|
|
|
|
entry_data = hass.data[DOMAIN][config_entry.entry_id]
|
|
server_id = config_entry.unique_id
|
|
|
|
@callback
|
|
def instance_add(instance_num: int, instance_name: str) -> None:
|
|
"""Add entities for a new Hyperion instance."""
|
|
assert server_id
|
|
args = (
|
|
server_id,
|
|
instance_num,
|
|
instance_name,
|
|
config_entry.options,
|
|
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
|
|
)
|
|
async_add_entities(
|
|
[
|
|
HyperionLight(*args),
|
|
HyperionPriorityLight(*args),
|
|
]
|
|
)
|
|
|
|
@callback
|
|
def instance_remove(instance_num: int) -> None:
|
|
"""Remove entities for an old Hyperion instance."""
|
|
assert server_id
|
|
for light_type in LIGHT_TYPES:
|
|
async_dispatcher_send(
|
|
hass,
|
|
SIGNAL_ENTITY_REMOVE.format(
|
|
get_hyperion_unique_id(server_id, instance_num, light_type)
|
|
),
|
|
)
|
|
|
|
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
|
|
return True
|
|
|
|
|
|
class HyperionBaseLight(LightEntity):
|
|
"""A Hyperion light base class."""
|
|
|
|
def __init__(
|
|
self,
|
|
server_id: str,
|
|
instance_num: int,
|
|
instance_name: str,
|
|
options: MappingProxyType[str, Any],
|
|
hyperion_client: client.HyperionClient,
|
|
) -> None:
|
|
"""Initialize the light."""
|
|
self._unique_id = self._compute_unique_id(server_id, instance_num)
|
|
self._name = self._compute_name(instance_name)
|
|
self._device_id = get_hyperion_device_id(server_id, instance_num)
|
|
self._instance_name = instance_name
|
|
self._options = options
|
|
self._client = hyperion_client
|
|
|
|
# Active state representing the Hyperion instance.
|
|
self._brightness: int = 255
|
|
self._rgb_color: Sequence[int] = DEFAULT_COLOR
|
|
self._effect: str = KEY_EFFECT_SOLID
|
|
|
|
self._static_effect_list: list[str] = [KEY_EFFECT_SOLID]
|
|
if self._support_external_effects:
|
|
self._static_effect_list += [
|
|
const.KEY_COMPONENTID_TO_NAME[component]
|
|
for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES
|
|
]
|
|
self._effect_list: list[str] = self._static_effect_list[:]
|
|
|
|
self._client_callbacks: Mapping[str, Callable[[dict[str, Any]], None]] = {
|
|
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,
|
|
}
|
|
|
|
def _compute_unique_id(self, server_id: str, instance_num: int) -> str:
|
|
"""Compute a unique id for this instance."""
|
|
raise NotImplementedError
|
|
|
|
def _compute_name(self, instance_name: str) -> str:
|
|
"""Compute the name of the light."""
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def entity_registry_enabled_default(self) -> bool:
|
|
"""Whether or not the entity is enabled by default."""
|
|
return True
|
|
|
|
@property
|
|
def should_poll(self) -> bool:
|
|
"""Return whether or not this entity should be polled."""
|
|
return False
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return the name of the light."""
|
|
return self._name
|
|
|
|
@property
|
|
def brightness(self) -> int:
|
|
"""Return the brightness of this light between 0..255."""
|
|
return self._brightness
|
|
|
|
@property
|
|
def hs_color(self) -> tuple[float, float]:
|
|
"""Return last color value set."""
|
|
return color_util.color_RGB_to_hs(*self._rgb_color)
|
|
|
|
@property
|
|
def icon(self) -> str:
|
|
"""Return state specific icon."""
|
|
if self.is_on:
|
|
if (
|
|
self.effect in const.KEY_COMPONENTID_FROM_NAME
|
|
and const.KEY_COMPONENTID_FROM_NAME[self.effect]
|
|
in const.KEY_COMPONENTID_EXTERNAL_SOURCES
|
|
):
|
|
return ICON_EXTERNAL_SOURCE
|
|
if self.effect != KEY_EFFECT_SOLID:
|
|
return ICON_EFFECT
|
|
return ICON_LIGHTBULB
|
|
|
|
@property
|
|
def effect(self) -> str:
|
|
"""Return the current effect."""
|
|
return self._effect
|
|
|
|
@property
|
|
def effect_list(self) -> list[str]:
|
|
"""Return the list of supported effects."""
|
|
return self._effect_list
|
|
|
|
@property
|
|
def supported_features(self) -> int:
|
|
"""Flag supported features."""
|
|
return SUPPORT_HYPERION
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return server availability."""
|
|
return bool(self._client.has_loaded_state)
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Return a unique id for this instance."""
|
|
return self._unique_id
|
|
|
|
@property
|
|
def device_info(self) -> DeviceInfo:
|
|
"""Return device information."""
|
|
return {
|
|
"identifiers": {(DOMAIN, self._device_id)},
|
|
"name": self._instance_name,
|
|
"manufacturer": HYPERION_MANUFACTURER_NAME,
|
|
"model": HYPERION_MODEL_NAME,
|
|
}
|
|
|
|
def _get_option(self, key: str) -> Any:
|
|
"""Get a value from the provided options."""
|
|
defaults = {
|
|
CONF_PRIORITY: DEFAULT_PRIORITY,
|
|
CONF_EFFECT_HIDE_LIST: [],
|
|
}
|
|
return self._options.get(key, defaults[key])
|
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
"""Turn on the light."""
|
|
# == Get key parameters ==
|
|
if ATTR_EFFECT not in kwargs and ATTR_HS_COLOR in kwargs:
|
|
effect = KEY_EFFECT_SOLID
|
|
else:
|
|
effect = kwargs.get(ATTR_EFFECT, self._effect)
|
|
rgb_color: Sequence[int]
|
|
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 ATTR_BRIGHTNESS in kwargs:
|
|
brightness = kwargs[ATTR_BRIGHTNESS]
|
|
for item in self._client.adjustment or []:
|
|
if (
|
|
const.KEY_ID in item
|
|
and not await self._client.async_send_set_adjustment(
|
|
**{
|
|
const.KEY_ADJUSTMENT: {
|
|
const.KEY_BRIGHTNESS: int(
|
|
round((float(brightness) * 100) / 255)
|
|
),
|
|
const.KEY_ID: item[const.KEY_ID],
|
|
}
|
|
}
|
|
)
|
|
):
|
|
return
|
|
|
|
# == Set an external source
|
|
if (
|
|
effect
|
|
and self._support_external_effects
|
|
and (
|
|
effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES
|
|
or effect in const.KEY_COMPONENTID_FROM_NAME
|
|
)
|
|
):
|
|
if effect in const.KEY_COMPONENTID_FROM_NAME:
|
|
component = const.KEY_COMPONENTID_FROM_NAME[effect]
|
|
else:
|
|
_LOGGER.warning(
|
|
"Use of Hyperion effect '%s' is deprecated and will be removed "
|
|
"in a future release. Please use '%s' instead",
|
|
effect,
|
|
const.KEY_COMPONENTID_TO_NAME[effect],
|
|
)
|
|
component = effect
|
|
|
|
# Clear any color/effect.
|
|
if not await self._client.async_send_clear(
|
|
**{const.KEY_PRIORITY: self._get_option(CONF_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: component == 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._get_option(CONF_PRIORITY)}
|
|
):
|
|
return
|
|
|
|
if not await self._client.async_send_set_effect(
|
|
**{
|
|
const.KEY_PRIORITY: self._get_option(CONF_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._get_option(CONF_PRIORITY),
|
|
const.KEY_COLOR: rgb_color,
|
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
|
}
|
|
):
|
|
return
|
|
|
|
def _set_internal_state(
|
|
self,
|
|
brightness: int | None = None,
|
|
rgb_color: Sequence[int] | None = None,
|
|
effect: str | None = None,
|
|
) -> 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
|
|
|
|
@callback
|
|
def _update_components(self, _: dict[str, Any] | None = None) -> None:
|
|
"""Update Hyperion components."""
|
|
self.async_write_ha_state()
|
|
|
|
@callback
|
|
def _update_adjustment(self, _: dict[str, Any] | None = None) -> 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()
|
|
|
|
@callback
|
|
def _update_priorities(self, _: dict[str, Any] | None = None) -> None:
|
|
"""Update Hyperion priorities."""
|
|
priority = self._get_priority_entry_that_dictates_state()
|
|
if priority and self._allow_priority_update(priority):
|
|
componentid = priority.get(const.KEY_COMPONENTID)
|
|
if (
|
|
self._support_external_effects
|
|
and componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES
|
|
and componentid in const.KEY_COMPONENTID_TO_NAME
|
|
):
|
|
self._set_internal_state(
|
|
rgb_color=DEFAULT_COLOR,
|
|
effect=const.KEY_COMPONENTID_TO_NAME[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=priority[const.KEY_OWNER]
|
|
)
|
|
elif componentid == const.KEY_COMPONENTID_COLOR:
|
|
self._set_internal_state(
|
|
rgb_color=priority[const.KEY_VALUE][const.KEY_RGB],
|
|
effect=KEY_EFFECT_SOLID,
|
|
)
|
|
self.async_write_ha_state()
|
|
|
|
@callback
|
|
def _update_effect_list(self, _: dict[str, Any] | None = None) -> None:
|
|
"""Update Hyperion effects."""
|
|
if not self._client.effects:
|
|
return
|
|
effect_list: list[str] = []
|
|
hide_effects = self._get_option(CONF_EFFECT_HIDE_LIST)
|
|
|
|
for effect in self._client.effects or []:
|
|
if const.KEY_NAME in effect:
|
|
effect_name = effect[const.KEY_NAME]
|
|
if effect_name not in hide_effects:
|
|
effect_list.append(effect_name)
|
|
|
|
self._effect_list = [
|
|
effect for effect in self._static_effect_list if effect not in hide_effects
|
|
] + effect_list
|
|
self.async_write_ha_state()
|
|
|
|
@callback
|
|
def _update_full_state(self) -> None:
|
|
"""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,
|
|
)
|
|
|
|
@callback
|
|
def _update_client(self, _: dict[str, Any] | None = None) -> None:
|
|
"""Update client connection state."""
|
|
self.async_write_ha_state()
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Register callbacks when entity added to hass."""
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
SIGNAL_ENTITY_REMOVE.format(self.unique_id),
|
|
functools.partial(self.async_remove, force_remove=True),
|
|
)
|
|
)
|
|
|
|
self._client.add_callbacks(self._client_callbacks)
|
|
|
|
# Load initial state.
|
|
self._update_full_state()
|
|
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
"""Cleanup prior to hass removal."""
|
|
self._client.remove_callbacks(self._client_callbacks)
|
|
|
|
@property
|
|
def _support_external_effects(self) -> bool:
|
|
"""Whether or not to support setting external effects from the light entity."""
|
|
return True
|
|
|
|
def _get_priority_entry_that_dictates_state(self) -> dict[str, Any] | None:
|
|
"""Get the relevant Hyperion priority entry to consider."""
|
|
# Return the visible priority (whether or not it is the HA priority).
|
|
|
|
# Explicit type specifier to ensure this works when the underlying (typed)
|
|
# library is installed along with the tests. Casts would trigger a
|
|
# redundant-cast warning in this case.
|
|
priority: dict[str, Any] | None = self._client.visible_priority
|
|
return priority
|
|
|
|
# pylint: disable=no-self-use
|
|
def _allow_priority_update(self, priority: dict[str, Any] | None = None) -> bool:
|
|
"""Determine whether to allow a priority to update internal state."""
|
|
return True
|
|
|
|
|
|
class HyperionLight(HyperionBaseLight):
|
|
"""A Hyperion light that acts in absolute (vs priority) manner.
|
|
|
|
Light state is the absolute Hyperion component state (e.g. LED device on/off) rather
|
|
than color based at a particular priority, and the 'winning' priority determines
|
|
shown state rather than exclusively the HA priority.
|
|
"""
|
|
|
|
def _compute_unique_id(self, server_id: str, instance_num: int) -> str:
|
|
"""Compute a unique id for this instance."""
|
|
return get_hyperion_unique_id(server_id, instance_num, TYPE_HYPERION_LIGHT)
|
|
|
|
def _compute_name(self, instance_name: str) -> str:
|
|
"""Compute the name of the light."""
|
|
return f"{instance_name} {NAME_SUFFIX_HYPERION_LIGHT}".strip()
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return true if light is on."""
|
|
return (
|
|
bool(self._client.is_on())
|
|
and self._get_priority_entry_that_dictates_state() is not None
|
|
)
|
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
"""Turn on the light."""
|
|
# == 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 bool(self._client.is_on()):
|
|
for component in (
|
|
const.KEY_COMPONENTID_ALL,
|
|
const.KEY_COMPONENTID_LEDDEVICE,
|
|
):
|
|
if not await self._client.async_send_set_component(
|
|
**{
|
|
const.KEY_COMPONENTSTATE: {
|
|
const.KEY_COMPONENT: component,
|
|
const.KEY_STATE: True,
|
|
}
|
|
}
|
|
):
|
|
return
|
|
|
|
# Turn on the relevant Hyperion priority as usual.
|
|
await super().async_turn_on(**kwargs)
|
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
"""Turn off the light."""
|
|
if not await self._client.async_send_set_component(
|
|
**{
|
|
const.KEY_COMPONENTSTATE: {
|
|
const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
|
|
const.KEY_STATE: False,
|
|
}
|
|
}
|
|
):
|
|
return
|
|
|
|
|
|
class HyperionPriorityLight(HyperionBaseLight):
|
|
"""A Hyperion light that only acts on a single Hyperion priority."""
|
|
|
|
def _compute_unique_id(self, server_id: str, instance_num: int) -> str:
|
|
"""Compute a unique id for this instance."""
|
|
return get_hyperion_unique_id(
|
|
server_id, instance_num, TYPE_HYPERION_PRIORITY_LIGHT
|
|
)
|
|
|
|
def _compute_name(self, instance_name: str) -> str:
|
|
"""Compute the name of the light."""
|
|
return f"{instance_name} {NAME_SUFFIX_HYPERION_PRIORITY_LIGHT}".strip()
|
|
|
|
@property
|
|
def entity_registry_enabled_default(self) -> bool:
|
|
"""Whether or not the entity is enabled by default."""
|
|
return False
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return true if light is on."""
|
|
priority = self._get_priority_entry_that_dictates_state()
|
|
return (
|
|
priority is not None
|
|
and not HyperionPriorityLight._is_priority_entry_black(priority)
|
|
)
|
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
"""Turn off the light."""
|
|
if not await self._client.async_send_clear(
|
|
**{const.KEY_PRIORITY: self._get_option(CONF_PRIORITY)}
|
|
):
|
|
return
|
|
await self._client.async_send_set_color(
|
|
**{
|
|
const.KEY_PRIORITY: self._get_option(CONF_PRIORITY),
|
|
const.KEY_COLOR: COLOR_BLACK,
|
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
|
}
|
|
)
|
|
|
|
@property
|
|
def _support_external_effects(self) -> bool:
|
|
"""Whether or not to support setting external effects from the light entity."""
|
|
return False
|
|
|
|
def _get_priority_entry_that_dictates_state(self) -> dict[str, Any] | None:
|
|
"""Get the relevant Hyperion priority entry to consider."""
|
|
# Return the active priority (if any) at the configured HA priority.
|
|
for candidate in self._client.priorities or []:
|
|
if const.KEY_PRIORITY not in candidate:
|
|
continue
|
|
if candidate[const.KEY_PRIORITY] == self._get_option(
|
|
CONF_PRIORITY
|
|
) and candidate.get(const.KEY_ACTIVE, False):
|
|
# Explicit type specifier to ensure this works when the underlying
|
|
# (typed) library is installed along with the tests. Casts would trigger
|
|
# a redundant-cast warning in this case.
|
|
output: dict[str, Any] = candidate
|
|
return output
|
|
return None
|
|
|
|
@classmethod
|
|
def _is_priority_entry_black(cls, priority: dict[str, Any] | None) -> bool:
|
|
"""Determine if a given priority entry is the color black."""
|
|
if (
|
|
priority
|
|
and priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR
|
|
):
|
|
rgb_color = priority.get(const.KEY_VALUE, {}).get(const.KEY_RGB)
|
|
if rgb_color is not None and tuple(rgb_color) == COLOR_BLACK:
|
|
return True
|
|
return False
|
|
|
|
def _allow_priority_update(self, priority: dict[str, Any] | None = None) -> bool:
|
|
"""Determine whether to allow a Hyperion priority to update entity attributes."""
|
|
# Black is treated as 'off' (and Home Assistant does not support selecting black
|
|
# from the color selector). Do not set our internal attributes if the priority is
|
|
# 'off' (i.e. if black is active). Do this to ensure it seamlessly turns back on
|
|
# at the correct prior color on the next 'on' call.
|
|
return not HyperionPriorityLight._is_priority_entry_black(priority)
|
|
|
|
|
|
LIGHT_TYPES = {
|
|
TYPE_HYPERION_LIGHT: HyperionLight,
|
|
TYPE_HYPERION_PRIORITY_LIGHT: HyperionPriorityLight,
|
|
}
|