Decouple Hyperion entitites and clear source when light is off (#80478)
* Remove Hyperion Priority Light * Remove coupling between light entity and led device * Merge HyperionLight and HyperionBaseLight as we will only have one light entity * Set state based on whether priority channel is open or not * Remove leftover variable from Priority Light * Remove external sources from light entity; use switch entities instead * Remove external effects from effects to show dropdown * Remove workaround for hyperion.ng issue 992 --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
ee4459f41e
commit
c1953b0ae4
6 changed files with 105 additions and 966 deletions
|
@ -447,7 +447,7 @@ class HyperionOptionsFlow(OptionsFlow):
|
|||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
|
||||
effects = {source: source for source in const.KEY_COMPONENTID_EXTERNAL_SOURCES}
|
||||
effects = {}
|
||||
async with self._create_client() as hyperion_client:
|
||||
if not hyperion_client:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
|
|
@ -21,8 +21,6 @@ HYPERION_MODEL_NAME = f"{HYPERION_MANUFACTURER_NAME}-NG"
|
|||
HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases"
|
||||
HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9"
|
||||
|
||||
NAME_SUFFIX_HYPERION_LIGHT = ""
|
||||
NAME_SUFFIX_HYPERION_PRIORITY_LIGHT = "Priority"
|
||||
NAME_SUFFIX_HYPERION_COMPONENT_SWITCH = "Component"
|
||||
NAME_SUFFIX_HYPERION_CAMERA = ""
|
||||
|
||||
|
@ -32,5 +30,4 @@ SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}"
|
|||
|
||||
TYPE_HYPERION_CAMERA = "hyperion_camera"
|
||||
TYPE_HYPERION_LIGHT = "hyperion_light"
|
||||
TYPE_HYPERION_PRIORITY_LIGHT = "hyperion_priority_light"
|
||||
TYPE_HYPERION_COMPONENT_SWITCH_BASE = "hyperion_component_switch"
|
||||
|
|
|
@ -41,24 +41,19 @@ from .const import (
|
|||
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.
|
||||
# brightness), we need to persist the effect that is in flight, so
|
||||
# subsequent calls to turn_on will know to 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
|
||||
|
@ -75,7 +70,6 @@ DEFAULT_EFFECT_LIST: list[str] = []
|
|||
|
||||
ICON_LIGHTBULB = "mdi:lightbulb"
|
||||
ICON_EFFECT = "mdi:lava-lamp"
|
||||
ICON_EXTERNAL_SOURCE = "mdi:television-ambient-light"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -102,7 +96,6 @@ async def async_setup_entry(
|
|||
async_add_entities(
|
||||
[
|
||||
HyperionLight(*args),
|
||||
HyperionPriorityLight(*args),
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -110,19 +103,18 @@ async def async_setup_entry(
|
|||
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)
|
||||
),
|
||||
)
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
SIGNAL_ENTITY_REMOVE.format(
|
||||
get_hyperion_unique_id(server_id, instance_num, TYPE_HYPERION_LIGHT)
|
||||
),
|
||||
)
|
||||
|
||||
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
|
||||
|
||||
|
||||
class HyperionBaseLight(LightEntity):
|
||||
"""A Hyperion light base class."""
|
||||
class HyperionLight(LightEntity):
|
||||
"""A Hyperion light that acts as a client for the configured priority."""
|
||||
|
||||
_attr_color_mode = ColorMode.HS
|
||||
_attr_should_poll = False
|
||||
|
@ -151,11 +143,6 @@ class HyperionBaseLight(LightEntity):
|
|||
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]] = {
|
||||
|
@ -168,11 +155,11 @@ class HyperionBaseLight(LightEntity):
|
|||
|
||||
def _compute_unique_id(self, server_id: str, instance_num: int) -> str:
|
||||
"""Compute a unique id for this instance."""
|
||||
raise NotImplementedError
|
||||
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."""
|
||||
raise NotImplementedError
|
||||
return f"{instance_name}".strip()
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
|
@ -198,12 +185,6 @@ class HyperionBaseLight(LightEntity):
|
|||
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
|
||||
|
@ -247,6 +228,11 @@ class HyperionBaseLight(LightEntity):
|
|||
}
|
||||
return self._options.get(key, defaults[key])
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on. Light is considered on when there is a source at the configured HA priority."""
|
||||
return self._get_priority_entry_that_dictates_state() is not None
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
# == Get key parameters ==
|
||||
|
@ -279,55 +265,8 @@ class HyperionBaseLight(LightEntity):
|
|||
):
|
||||
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 effect and effect != KEY_EFFECT_SOLID:
|
||||
if not await self._client.async_send_set_effect(
|
||||
**{
|
||||
const.KEY_PRIORITY: self._get_option(CONF_PRIORITY),
|
||||
|
@ -336,6 +275,7 @@ class HyperionBaseLight(LightEntity):
|
|||
}
|
||||
):
|
||||
return
|
||||
|
||||
# == Set a color
|
||||
elif not await self._client.async_send_set_color(
|
||||
**{
|
||||
|
@ -346,6 +286,13 @@ class HyperionBaseLight(LightEntity):
|
|||
):
|
||||
return
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light i.e. clear the configured priority."""
|
||||
if not await self._client.async_send_clear(
|
||||
**{const.KEY_PRIORITY: self._get_option(CONF_PRIORITY)}
|
||||
):
|
||||
return
|
||||
|
||||
def _set_internal_state(
|
||||
self,
|
||||
brightness: int | None = None,
|
||||
|
@ -383,24 +330,15 @@ class HyperionBaseLight(LightEntity):
|
|||
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:
|
||||
if priority:
|
||||
component_id = priority.get(const.KEY_COMPONENTID)
|
||||
if component_id == 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:
|
||||
elif component_id == const.KEY_COMPONENTID_COLOR:
|
||||
self._set_internal_state(
|
||||
rgb_color=priority[const.KEY_VALUE][const.KEY_RGB],
|
||||
effect=KEY_EFFECT_SOLID,
|
||||
|
@ -469,172 +407,10 @@ class HyperionBaseLight(LightEntity):
|
|||
"""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
|
||||
|
||||
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 whether or not the HA priority is among the active priorities.
|
||||
for priority in self._client.priorities or []:
|
||||
if priority.get(const.KEY_PRIORITY) == self._get_option(CONF_PRIORITY):
|
||||
return priority
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ TEST_PRIORITY = 180
|
|||
TEST_ENTITY_ID_1 = "light.test_instance_1"
|
||||
TEST_ENTITY_ID_2 = "light.test_instance_2"
|
||||
TEST_ENTITY_ID_3 = "light.test_instance_3"
|
||||
TEST_PRIORITY_LIGHT_ENTITY_ID_1 = "light.test_instance_1_priority"
|
||||
TEST_TITLE = f"{TEST_HOST}:{TEST_PORT}"
|
||||
|
||||
TEST_TOKEN = "sekr1t"
|
||||
|
|
|
@ -795,10 +795,8 @@ async def test_options_effect_show_list(hass: HomeAssistant) -> None:
|
|||
await hass.async_block_till_done()
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
|
||||
# effect1 and effect3 only, so effect2 & external sources are hidden.
|
||||
assert result["data"][CONF_EFFECT_HIDE_LIST] == sorted(
|
||||
["effect2"] + const.KEY_COMPONENTID_EXTERNAL_SOURCES
|
||||
)
|
||||
# effect1 and effect3 only, so effect2 is hidden.
|
||||
assert result["data"][CONF_EFFECT_HIDE_LIST] == ["effect2"]
|
||||
|
||||
|
||||
async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistant) -> None:
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Tests for the Hyperion integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, Mock, call, patch
|
||||
|
||||
from hyperion import const
|
||||
|
@ -17,7 +16,6 @@ from homeassistant.components.hyperion.const import (
|
|||
DOMAIN,
|
||||
HYPERION_MANUFACTURER_NAME,
|
||||
HYPERION_MODEL_NAME,
|
||||
TYPE_HYPERION_PRIORITY_LIGHT,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
|
@ -27,12 +25,7 @@ from homeassistant.components.light import (
|
|||
ColorMode,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
RELOAD_AFTER_UPDATE_DELAY,
|
||||
SOURCE_REAUTH,
|
||||
ConfigEntry,
|
||||
ConfigEntryState,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_HOST,
|
||||
|
@ -44,8 +37,6 @@ from homeassistant.const import (
|
|||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from . import (
|
||||
TEST_AUTH_NOT_REQUIRED_RESP,
|
||||
|
@ -62,19 +53,13 @@ from . import (
|
|||
TEST_INSTANCE_3,
|
||||
TEST_PORT,
|
||||
TEST_PRIORITY,
|
||||
TEST_PRIORITY_LIGHT_ENTITY_ID_1,
|
||||
TEST_SYSINFO_ID,
|
||||
add_test_config_entry,
|
||||
call_registered_callback,
|
||||
create_mock_client,
|
||||
register_test_entity,
|
||||
setup_test_config_entry,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
COLOR_BLACK = color_util.COLORS["black"]
|
||||
|
||||
|
||||
def _get_config_entry_from_unique_id(
|
||||
hass: HomeAssistant, unique_id: str
|
||||
|
@ -247,6 +232,7 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistant) -> None
|
|||
async def test_light_basic_properties(hass: HomeAssistant) -> None:
|
||||
"""Test the basic properties."""
|
||||
client = create_mock_client()
|
||||
client.priorities = [{const.KEY_PRIORITY: TEST_PRIORITY}]
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
|
@ -257,8 +243,8 @@ async def test_light_basic_properties(hass: HomeAssistant) -> None:
|
|||
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
|
||||
assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
|
||||
|
||||
# By default the effect list is the 3 external sources + 'Solid'.
|
||||
assert len(entity_state.attributes["effect_list"]) == 4
|
||||
# By default the effect list contains only 'Solid'.
|
||||
assert len(entity_state.attributes["effect_list"]) == 1
|
||||
|
||||
assert entity_state.attributes["color_mode"] == ColorMode.HS
|
||||
assert entity_state.attributes["supported_color_modes"] == [ColorMode.HS]
|
||||
|
@ -268,6 +254,7 @@ async def test_light_basic_properties(hass: HomeAssistant) -> None:
|
|||
async def test_light_async_turn_on(hass: HomeAssistant) -> None:
|
||||
"""Test turning the light on."""
|
||||
client = create_mock_client()
|
||||
client.priorities = [{const.KEY_PRIORITY: TEST_PRIORITY}]
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
# On (=), 100% (=), solid (=), [255,255,255] (=)
|
||||
|
@ -345,10 +332,13 @@ async def test_light_async_turn_on(hass: HomeAssistant) -> None:
|
|||
)
|
||||
|
||||
# Simulate a state callback from Hyperion.
|
||||
client.visible_priority = {
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)},
|
||||
}
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)},
|
||||
}
|
||||
]
|
||||
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
|
@ -385,54 +375,6 @@ async def test_light_async_turn_on(hass: HomeAssistant) -> None:
|
|||
assert entity_state
|
||||
assert entity_state.attributes["brightness"] == brightness
|
||||
|
||||
# On (=), 100% (=), "USB Capture (!), [0,255,255] (=)
|
||||
component = "V4L"
|
||||
effect = const.KEY_COMPONENTID_TO_NAME[component]
|
||||
client.async_send_clear = AsyncMock(return_value=True)
|
||||
client.async_send_set_component = AsyncMock(return_value=True)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: effect},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert client.async_send_clear.call_args == call(
|
||||
**{const.KEY_PRIORITY: TEST_PRIORITY}
|
||||
)
|
||||
assert client.async_send_set_component.call_args_list == [
|
||||
call(
|
||||
**{
|
||||
const.KEY_COMPONENTSTATE: {
|
||||
const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[0],
|
||||
const.KEY_STATE: False,
|
||||
}
|
||||
}
|
||||
),
|
||||
call(
|
||||
**{
|
||||
const.KEY_COMPONENTSTATE: {
|
||||
const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[1],
|
||||
const.KEY_STATE: False,
|
||||
}
|
||||
}
|
||||
),
|
||||
call(
|
||||
**{
|
||||
const.KEY_COMPONENTSTATE: {
|
||||
const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[2],
|
||||
const.KEY_STATE: True,
|
||||
}
|
||||
}
|
||||
),
|
||||
]
|
||||
client.visible_priority = {const.KEY_COMPONENTID: component}
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
|
||||
assert entity_state.attributes["effect"] == effect
|
||||
|
||||
# On (=), 100% (=), "Warm Blobs" (!), [0,255,255] (=)
|
||||
effect = "Warm Blobs"
|
||||
client.async_send_clear = AsyncMock(return_value=True)
|
||||
|
@ -445,9 +387,6 @@ async def test_light_async_turn_on(hass: HomeAssistant) -> None:
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
assert client.async_send_clear.call_args == call(
|
||||
**{const.KEY_PRIORITY: TEST_PRIORITY}
|
||||
)
|
||||
assert client.async_send_set_effect.call_args == call(
|
||||
**{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
|
@ -455,10 +394,13 @@ async def test_light_async_turn_on(hass: HomeAssistant) -> None:
|
|||
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||
}
|
||||
)
|
||||
client.visible_priority = {
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
|
||||
const.KEY_OWNER: effect,
|
||||
}
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
|
||||
const.KEY_OWNER: effect,
|
||||
}
|
||||
]
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
|
@ -484,10 +426,13 @@ async def test_light_async_turn_on(hass: HomeAssistant) -> None:
|
|||
}
|
||||
)
|
||||
# Simulate a state callback from Hyperion.
|
||||
client.visible_priority = {
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (0, 0, 255)},
|
||||
}
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (0, 0, 255)},
|
||||
}
|
||||
]
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
|
@ -509,82 +454,6 @@ async def test_light_async_turn_on(hass: HomeAssistant) -> None:
|
|||
assert not client.async_send_set_effect.called
|
||||
|
||||
|
||||
async def test_light_async_turn_on_fail_async_send_set_component(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test set_component failure when turning the light on."""
|
||||
client = create_mock_client()
|
||||
client.async_send_set_component = AsyncMock(return_value=False)
|
||||
client.is_on = Mock(return_value=False)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True
|
||||
)
|
||||
assert client.method_calls[-1] == call.async_send_set_component(
|
||||
componentstate={"component": "ALL", "state": True}
|
||||
)
|
||||
|
||||
|
||||
async def test_light_async_turn_on_fail_async_send_set_component_source(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_send_set_component failure when selecting the source."""
|
||||
client = create_mock_client()
|
||||
client.async_send_clear = AsyncMock(return_value=True)
|
||||
client.async_send_set_component = AsyncMock(return_value=False)
|
||||
client.is_on = Mock(return_value=True)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: TEST_ENTITY_ID_1,
|
||||
ATTR_EFFECT: const.KEY_COMPONENTID_TO_NAME["V4L"],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert client.method_calls[-1] == call.async_send_set_component(
|
||||
componentstate={"component": "BOBLIGHTSERVER", "state": False}
|
||||
)
|
||||
|
||||
|
||||
async def test_light_async_turn_on_fail_async_send_clear_source(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_send_clear failure when turning the light on."""
|
||||
client = create_mock_client()
|
||||
client.is_on = Mock(return_value=True)
|
||||
client.async_send_clear = AsyncMock(return_value=False)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: TEST_ENTITY_ID_1,
|
||||
ATTR_EFFECT: const.KEY_COMPONENTID_TO_NAME["V4L"],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert client.method_calls[-1] == call.async_send_clear(priority=180)
|
||||
|
||||
|
||||
async def test_light_async_turn_on_fail_async_send_clear_effect(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_send_clear failure when turning on an effect."""
|
||||
client = create_mock_client()
|
||||
client.is_on = Mock(return_value=True)
|
||||
client.async_send_clear = AsyncMock(return_value=False)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: "Warm Mood Blobs"},
|
||||
blocking=True,
|
||||
)
|
||||
assert client.method_calls[-1] == call.async_send_clear(priority=180)
|
||||
|
||||
|
||||
async def test_light_async_turn_on_fail_async_send_set_effect(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
|
@ -625,12 +494,12 @@ async def test_light_async_turn_on_fail_async_send_set_color(
|
|||
)
|
||||
|
||||
|
||||
async def test_light_async_turn_off_fail_async_send_set_component(
|
||||
async def test_light_async_turn_off_fail_async_send_send_clear(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_send_set_component failure when turning off the light."""
|
||||
"""Test async_send_clear failure when turning off the light."""
|
||||
client = create_mock_client()
|
||||
client.async_send_set_component = AsyncMock(return_value=False)
|
||||
client.async_send_clear = AsyncMock(return_value=False)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
await hass.services.async_call(
|
||||
|
@ -639,30 +508,7 @@ async def test_light_async_turn_off_fail_async_send_set_component(
|
|||
{ATTR_ENTITY_ID: TEST_ENTITY_ID_1},
|
||||
blocking=True,
|
||||
)
|
||||
assert client.method_calls[-1] == call.async_send_set_component(
|
||||
componentstate={"component": "LEDDEVICE", "state": False}
|
||||
)
|
||||
|
||||
|
||||
async def test_priority_light_async_turn_off_fail_async_send_clear(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_send_clear failure when turning off a priority light."""
|
||||
client = create_mock_client()
|
||||
client.async_send_clear = AsyncMock(return_value=False)
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default"
|
||||
) as enabled_by_default_mock:
|
||||
enabled_by_default_mock.return_value = True
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1},
|
||||
blocking=True,
|
||||
)
|
||||
assert client.method_calls[-1] == call.async_send_clear(priority=180)
|
||||
assert client.method_calls[-1] == call.async_send_clear(priority=TEST_PRIORITY)
|
||||
|
||||
|
||||
async def test_light_async_turn_off(hass: HomeAssistant) -> None:
|
||||
|
@ -670,7 +516,7 @@ async def test_light_async_turn_off(hass: HomeAssistant) -> None:
|
|||
client = create_mock_client()
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
client.async_send_set_component = AsyncMock(return_value=True)
|
||||
client.async_send_clear = AsyncMock(return_value=True)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
|
@ -678,40 +524,18 @@ async def test_light_async_turn_off(hass: HomeAssistant) -> None:
|
|||
blocking=True,
|
||||
)
|
||||
|
||||
assert client.async_send_set_component.call_args == call(
|
||||
**{
|
||||
const.KEY_COMPONENTSTATE: {
|
||||
const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
|
||||
const.KEY_STATE: False,
|
||||
}
|
||||
}
|
||||
assert client.async_send_clear.called
|
||||
assert client.async_send_clear.call_args == call(
|
||||
**{const.KEY_PRIORITY: TEST_PRIORITY}
|
||||
)
|
||||
|
||||
call_registered_callback(client, "components-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
|
||||
|
||||
# No calls if no state loaded.
|
||||
client.has_loaded_state = False
|
||||
client.async_send_set_component = AsyncMock(return_value=True)
|
||||
call_registered_callback(client, "client-update", {"loaded-state": False})
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID_1},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert not client.async_send_set_component.called
|
||||
|
||||
|
||||
async def test_light_async_updates_from_hyperion_client(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test receiving a variety of Hyperion client callbacks."""
|
||||
client = create_mock_client()
|
||||
client.priorities = [{const.KEY_PRIORITY: TEST_PRIORITY}]
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
# Bright change gets accepted.
|
||||
|
@ -720,6 +544,7 @@ async def test_light_async_updates_from_hyperion_client(
|
|||
call_registered_callback(client, "adjustment-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "on"
|
||||
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
|
||||
|
||||
# Broken brightness value is ignored.
|
||||
|
@ -728,40 +553,18 @@ async def test_light_async_updates_from_hyperion_client(
|
|||
call_registered_callback(client, "adjustment-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
|
||||
|
||||
# Update components.
|
||||
client.is_on.return_value = True
|
||||
call_registered_callback(client, "components-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "on"
|
||||
|
||||
client.is_on.return_value = False
|
||||
call_registered_callback(client, "components-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "off"
|
||||
|
||||
# Update priorities (V4L)
|
||||
client.is_on.return_value = True
|
||||
client.visible_priority = {const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L}
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
|
||||
assert entity_state.attributes["hs_color"] == (0.0, 0.0)
|
||||
assert (
|
||||
entity_state.attributes["effect"]
|
||||
== const.KEY_COMPONENTID_TO_NAME[const.KEY_COMPONENTID_V4L]
|
||||
)
|
||||
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
|
||||
|
||||
# Update priorities (Effect)
|
||||
effect = "foo"
|
||||
client.visible_priority = {
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
|
||||
const.KEY_OWNER: effect,
|
||||
}
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
|
||||
const.KEY_OWNER: effect,
|
||||
}
|
||||
]
|
||||
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
|
@ -772,10 +575,13 @@ async def test_light_async_updates_from_hyperion_client(
|
|||
|
||||
# Update priorities (Color)
|
||||
rgb = (0, 100, 100)
|
||||
client.visible_priority = {
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: rgb},
|
||||
}
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: rgb},
|
||||
}
|
||||
]
|
||||
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
|
@ -785,7 +591,7 @@ async def test_light_async_updates_from_hyperion_client(
|
|||
assert entity_state.attributes["hs_color"] == (180.0, 100.0)
|
||||
|
||||
# Update priorities (None)
|
||||
client.visible_priority = None
|
||||
client.priorities = []
|
||||
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
|
@ -800,12 +606,7 @@ async def test_light_async_updates_from_hyperion_client(
|
|||
assert entity_state
|
||||
assert entity_state.attributes["effect_list"] == [
|
||||
hyperion_light.KEY_EFFECT_SOLID
|
||||
] + [
|
||||
const.KEY_COMPONENTID_TO_NAME[component]
|
||||
for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES
|
||||
] + [
|
||||
effect[const.KEY_NAME] for effect in effects
|
||||
]
|
||||
] + [effect[const.KEY_NAME] for effect in effects]
|
||||
|
||||
# Update connection status (e.g. disconnection).
|
||||
|
||||
|
@ -818,10 +619,13 @@ async def test_light_async_updates_from_hyperion_client(
|
|||
|
||||
# Update connection status (e.g. re-connection)
|
||||
client.has_loaded_state = True
|
||||
client.visible_priority = {
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: rgb},
|
||||
}
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: rgb},
|
||||
}
|
||||
]
|
||||
call_registered_callback(client, "client-update", {"loaded-state": True})
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
|
@ -835,10 +639,13 @@ async def test_full_state_loaded_on_start(hass: HomeAssistant) -> None:
|
|||
# Update full state (should call all update methods).
|
||||
brightness = 25
|
||||
client.adjustment = [{const.KEY_BRIGHTNESS: brightness}]
|
||||
client.visible_priority = {
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (0, 100, 100)},
|
||||
}
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (0, 100, 100)},
|
||||
}
|
||||
]
|
||||
client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
|
||||
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
@ -940,358 +747,6 @@ async def test_setup_entry_bad_token_reauth(hass: HomeAssistant) -> None:
|
|||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_priority_light_async_updates(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test receiving a variety of Hyperion client callbacks to a HyperionPriorityLight."""
|
||||
priority_template = {
|
||||
const.KEY_ACTIVE: True,
|
||||
const.KEY_VISIBLE: True,
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)},
|
||||
}
|
||||
|
||||
client = create_mock_client()
|
||||
client.priorities = [{**priority_template}]
|
||||
|
||||
register_test_entity(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
TYPE_HYPERION_PRIORITY_LIGHT,
|
||||
TEST_PRIORITY_LIGHT_ENTITY_ID_1,
|
||||
)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
# == Scenario: Color at HA priority will show light as on.
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "on"
|
||||
assert entity_state.attributes["hs_color"] == (0.0, 0.0)
|
||||
|
||||
# == Scenario: Color going to black shows the light as off.
|
||||
client.priorities = [
|
||||
{
|
||||
**priority_template,
|
||||
const.KEY_VALUE: {const.KEY_RGB: COLOR_BLACK},
|
||||
}
|
||||
]
|
||||
client.visible_priority = client.priorities[0]
|
||||
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "off"
|
||||
|
||||
# == Scenario: Lower priority than HA priority should have no impact on what HA
|
||||
# shows when the HA priority is present.
|
||||
client.priorities = [
|
||||
{**priority_template, const.KEY_PRIORITY: TEST_PRIORITY - 1},
|
||||
{
|
||||
**priority_template,
|
||||
const.KEY_VALUE: {const.KEY_RGB: COLOR_BLACK},
|
||||
},
|
||||
]
|
||||
client.visible_priority = client.priorities[0]
|
||||
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "off"
|
||||
|
||||
# == Scenario: Fresh color at HA priority should turn HA entity on (even though
|
||||
# there's a lower priority enabled/visible in Hyperion).
|
||||
client.priorities = [
|
||||
{**priority_template, const.KEY_PRIORITY: TEST_PRIORITY - 1},
|
||||
{
|
||||
**priority_template,
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 150)},
|
||||
},
|
||||
]
|
||||
client.visible_priority = client.priorities[0]
|
||||
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "on"
|
||||
assert entity_state.attributes["hs_color"] == (240.0, 33.333)
|
||||
|
||||
# == Scenario: V4L at a higher priority, with no other HA priority at all, should
|
||||
# have no effect.
|
||||
|
||||
# Emulate HA turning the light off with black at the HA priority.
|
||||
client.priorities = []
|
||||
client.visible_priority = None
|
||||
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "off"
|
||||
|
||||
# Emulate V4L turning on.
|
||||
client.priorities = [
|
||||
{
|
||||
**priority_template,
|
||||
const.KEY_PRIORITY: 240,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 150)},
|
||||
},
|
||||
]
|
||||
client.visible_priority = client.priorities[0]
|
||||
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "off"
|
||||
|
||||
# == Scenario: A lower priority input (lower priority than HA) should have no effect.
|
||||
|
||||
client.priorities = [
|
||||
{
|
||||
**priority_template,
|
||||
const.KEY_VISIBLE: True,
|
||||
const.KEY_PRIORITY: TEST_PRIORITY - 1,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (255, 0, 0)},
|
||||
},
|
||||
{
|
||||
**priority_template,
|
||||
const.KEY_PRIORITY: 240,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 150)},
|
||||
const.KEY_VISIBLE: False,
|
||||
},
|
||||
]
|
||||
|
||||
client.visible_priority = client.priorities[0]
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "off"
|
||||
|
||||
# == Scenario: A non-active priority is ignored.
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_ACTIVE: False,
|
||||
const.KEY_VISIBLE: False,
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)},
|
||||
}
|
||||
]
|
||||
client.visible_priority = None
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "off"
|
||||
|
||||
# == Scenario: A priority with no ... priority ... is ignored.
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_ACTIVE: True,
|
||||
const.KEY_VISIBLE: True,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)},
|
||||
}
|
||||
]
|
||||
client.visible_priority = None
|
||||
call_registered_callback(client, "priorities-update")
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "off"
|
||||
|
||||
|
||||
async def test_priority_light_async_updates_off_sets_black(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test turning the HyperionPriorityLight off."""
|
||||
client = create_mock_client()
|
||||
client.priorities = [
|
||||
{
|
||||
const.KEY_ACTIVE: True,
|
||||
const.KEY_VISIBLE: True,
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)},
|
||||
}
|
||||
]
|
||||
|
||||
register_test_entity(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
TYPE_HYPERION_PRIORITY_LIGHT,
|
||||
TEST_PRIORITY_LIGHT_ENTITY_ID_1,
|
||||
)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
client.async_send_clear = AsyncMock(return_value=True)
|
||||
client.async_send_set_color = AsyncMock(return_value=True)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert client.async_send_clear.call_args == call(
|
||||
**{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
}
|
||||
)
|
||||
|
||||
assert client.async_send_set_color.call_args == call(
|
||||
**{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COLOR: COLOR_BLACK,
|
||||
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def test_priority_light_prior_color_preserved_after_black(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that color is preserved in an on->off->on cycle for a HyperionPriorityLight.
|
||||
|
||||
For a HyperionPriorityLight the color black is used to indicate off. This test
|
||||
ensures that a cycle through 'off' will preserve the original color.
|
||||
"""
|
||||
priority_template = {
|
||||
const.KEY_ACTIVE: True,
|
||||
const.KEY_VISIBLE: True,
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||
}
|
||||
|
||||
client = create_mock_client()
|
||||
client.async_send_set_color = AsyncMock(return_value=True)
|
||||
client.async_send_clear = AsyncMock(return_value=True)
|
||||
client.priorities = []
|
||||
client.visible_priority = None
|
||||
|
||||
register_test_entity(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
TYPE_HYPERION_PRIORITY_LIGHT,
|
||||
TEST_PRIORITY_LIGHT_ENTITY_ID_1,
|
||||
)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
# Turn the light on full green...
|
||||
# On (=), 100% (=), solid (=), [0,0,255] (=)
|
||||
hs_color = (240.0, 100.0)
|
||||
rgb_color = (0, 0, 255)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1, ATTR_HS_COLOR: hs_color},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert client.async_send_set_color.call_args == call(
|
||||
**{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COLOR: rgb_color,
|
||||
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||
}
|
||||
)
|
||||
|
||||
client.priorities = [
|
||||
{
|
||||
**priority_template,
|
||||
const.KEY_VALUE: {const.KEY_RGB: rgb_color},
|
||||
}
|
||||
]
|
||||
client.visible_priority = client.priorities[0]
|
||||
call_registered_callback(client, "priorities-update")
|
||||
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "on"
|
||||
assert entity_state.attributes["hs_color"] == hs_color
|
||||
|
||||
# Then turn it off.
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert client.async_send_set_color.call_args == call(
|
||||
**{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COLOR: COLOR_BLACK,
|
||||
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||
}
|
||||
)
|
||||
|
||||
client.priorities = [
|
||||
{
|
||||
**priority_template,
|
||||
const.KEY_VALUE: {const.KEY_RGB: COLOR_BLACK},
|
||||
}
|
||||
]
|
||||
client.visible_priority = client.priorities[0]
|
||||
call_registered_callback(client, "priorities-update")
|
||||
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "off"
|
||||
|
||||
# Then turn it back on and ensure it's still green.
|
||||
# On (=), 100% (=), solid (=), [0,0,255] (=)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert client.async_send_set_color.call_args == call(
|
||||
**{
|
||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||
const.KEY_COLOR: rgb_color,
|
||||
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||
}
|
||||
)
|
||||
|
||||
client.priorities = [
|
||||
{
|
||||
**priority_template,
|
||||
const.KEY_VALUE: {const.KEY_RGB: rgb_color},
|
||||
}
|
||||
]
|
||||
client.visible_priority = client.priorities[0]
|
||||
call_registered_callback(client, "priorities-update")
|
||||
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.state == "on"
|
||||
assert entity_state.attributes["hs_color"] == hs_color
|
||||
|
||||
|
||||
async def test_priority_light_has_no_external_sources(hass: HomeAssistant) -> None:
|
||||
"""Ensure a HyperionPriorityLight does not list external sources."""
|
||||
client = create_mock_client()
|
||||
client.priorities = []
|
||||
|
||||
register_test_entity(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
TYPE_HYPERION_PRIORITY_LIGHT,
|
||||
TEST_PRIORITY_LIGHT_ENTITY_ID_1,
|
||||
)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.attributes["effect_list"] == [hyperion_light.KEY_EFFECT_SOLID]
|
||||
|
||||
|
||||
async def test_light_option_effect_hide_list(hass: HomeAssistant) -> None:
|
||||
"""Test the effect_hide_list option."""
|
||||
client = create_mock_client()
|
||||
|
@ -1300,15 +755,13 @@ async def test_light_option_effect_hide_list(hass: HomeAssistant) -> None:
|
|||
await setup_test_config_entry(
|
||||
hass,
|
||||
hyperion_client=client,
|
||||
options={CONF_EFFECT_HIDE_LIST: ["Two", "USB Capture"]},
|
||||
options={CONF_EFFECT_HIDE_LIST: ["Two", "Three"]},
|
||||
)
|
||||
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.attributes["effect_list"] == [
|
||||
"Solid",
|
||||
"Boblight Server",
|
||||
"Platform Capture",
|
||||
"One",
|
||||
]
|
||||
|
||||
|
@ -1317,12 +770,6 @@ async def test_device_info(hass: HomeAssistant) -> None:
|
|||
"""Verify device information includes expected details."""
|
||||
client = create_mock_client()
|
||||
|
||||
register_test_entity(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
TYPE_HYPERION_PRIORITY_LIGHT,
|
||||
TEST_PRIORITY_LIGHT_ENTITY_ID_1,
|
||||
)
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
device_id = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE)
|
||||
|
@ -1341,82 +788,4 @@ async def test_device_info(hass: HomeAssistant) -> None:
|
|||
entry.entity_id
|
||||
for entry in er.async_entries_for_device(entity_registry, device.id)
|
||||
]
|
||||
assert TEST_PRIORITY_LIGHT_ENTITY_ID_1 in entities_from_device
|
||||
assert TEST_ENTITY_ID_1 in entities_from_device
|
||||
|
||||
|
||||
async def test_lights_can_be_enabled(hass: HomeAssistant) -> None:
|
||||
"""Verify lights can be enabled."""
|
||||
client = create_mock_client()
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
entry = entity_registry.async_get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entry
|
||||
assert entry.disabled
|
||||
assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert not entity_state
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.client.HyperionClient",
|
||||
return_value=client,
|
||||
):
|
||||
updated_entry = entity_registry.async_update_entity(
|
||||
TEST_PRIORITY_LIGHT_ENTITY_ID_1, disabled_by=None
|
||||
)
|
||||
assert not updated_entry.disabled
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
|
||||
|
||||
async def test_deprecated_effect_names(
|
||||
caplog: pytest.LogCaptureFixture, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test deprecated effects function and issue a warning."""
|
||||
client = create_mock_client()
|
||||
client.async_send_clear = AsyncMock(return_value=True)
|
||||
client.async_send_set_component = AsyncMock(return_value=True)
|
||||
|
||||
await setup_test_config_entry(hass, hyperion_client=client)
|
||||
|
||||
for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: component},
|
||||
blocking=True,
|
||||
)
|
||||
assert f"Use of Hyperion effect '{component}' is deprecated" in caplog.text
|
||||
|
||||
# Simulate a state callback from Hyperion.
|
||||
client.visible_priority = {
|
||||
const.KEY_COMPONENTID: component,
|
||||
}
|
||||
call_registered_callback(client, "priorities-update")
|
||||
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert (
|
||||
entity_state.attributes["effect"]
|
||||
== const.KEY_COMPONENTID_TO_NAME[component]
|
||||
)
|
||||
|
||||
|
||||
async def test_deprecated_effect_names_not_in_effect_list(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test deprecated effects are not in shown effect list."""
|
||||
await setup_test_config_entry(hass)
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
|
||||
assert component not in entity_state.attributes["effect_list"]
|
||||
|
|
Loading…
Add table
Reference in a new issue