Use Hyperion human-readable effect names instead of API identifiers (#45763)

This commit is contained in:
Dermot Duffy 2021-04-19 14:46:18 -07:00 committed by GitHub
parent 8acc3f0b03
commit 1560c00db1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 238 additions and 74 deletions

View File

@ -1,29 +1,5 @@
"""Constants for Hyperion integration."""
from hyperion.const import (
KEY_COMPONENTID_ALL,
KEY_COMPONENTID_BLACKBORDER,
KEY_COMPONENTID_BOBLIGHTSERVER,
KEY_COMPONENTID_FORWARDER,
KEY_COMPONENTID_GRABBER,
KEY_COMPONENTID_LEDDEVICE,
KEY_COMPONENTID_SMOOTHING,
KEY_COMPONENTID_V4L,
)
# Maps between Hyperion API component names to Hyperion UI names. This allows Home
# Assistant to use names that match what Hyperion users may expect from the Hyperion UI.
COMPONENT_TO_NAME = {
KEY_COMPONENTID_ALL: "All",
KEY_COMPONENTID_SMOOTHING: "Smoothing",
KEY_COMPONENTID_BLACKBORDER: "Blackbar Detection",
KEY_COMPONENTID_FORWARDER: "Forwarder",
KEY_COMPONENTID_BOBLIGHTSERVER: "Boblight Server",
KEY_COMPONENTID_GRABBER: "Platform Capture",
KEY_COMPONENTID_LEDDEVICE: "LED Device",
KEY_COMPONENTID_V4L: "USB Capture",
}
CONF_AUTH_ID = "auth_id"
CONF_CREATE_TOKEN = "create_token"
CONF_INSTANCE = "instance"

View File

@ -147,7 +147,10 @@ class HyperionBaseLight(LightEntity):
self._static_effect_list: list[str] = [KEY_EFFECT_SOLID]
if self._support_external_effects:
self._static_effect_list += list(const.KEY_COMPONENTID_EXTERNAL_SOURCES)
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]] = {
@ -195,7 +198,11 @@ class HyperionBaseLight(LightEntity):
def icon(self) -> str:
"""Return state specific icon."""
if self.is_on:
if self.effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
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
@ -280,8 +287,21 @@ class HyperionBaseLight(LightEntity):
if (
effect
and self._support_external_effects
and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES
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(
@ -295,7 +315,7 @@ class HyperionBaseLight(LightEntity):
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: key,
const.KEY_STATE: effect == key,
const.KEY_STATE: component == key,
}
}
):
@ -371,8 +391,12 @@ class HyperionBaseLight(LightEntity):
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=componentid)
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
@ -594,9 +618,10 @@ class HyperionPriorityLight(HyperionBaseLight):
@classmethod
def _is_priority_entry_black(cls, priority: dict[str, Any] | None) -> bool:
"""Determine if a given priority entry is the color black."""
if not priority:
return False
if priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR:
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

View File

@ -5,7 +5,7 @@
"domain": "hyperion",
"name": "Hyperion",
"quality_scale": "platinum",
"requirements": ["hyperion-py==0.7.0"],
"requirements": ["hyperion-py==0.7.2"],
"ssdp": [
{
"manufacturer": "Hyperion Open Source Ambient Lighting",

View File

@ -14,6 +14,7 @@ from hyperion.const import (
KEY_COMPONENTID_GRABBER,
KEY_COMPONENTID_LEDDEVICE,
KEY_COMPONENTID_SMOOTHING,
KEY_COMPONENTID_TO_NAME,
KEY_COMPONENTID_V4L,
KEY_COMPONENTS,
KEY_COMPONENTSTATE,
@ -39,7 +40,6 @@ from . import (
listen_for_instance_updates,
)
from .const import (
COMPONENT_TO_NAME,
CONF_INSTANCE_CLIENTS,
DOMAIN,
HYPERION_MANUFACTURER_NAME,
@ -67,7 +67,7 @@ def _component_to_unique_id(server_id: str, component: str, instance_num: int) -
server_id,
instance_num,
slugify(
f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}"
f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {KEY_COMPONENTID_TO_NAME[component]}"
),
)
@ -77,7 +77,7 @@ def _component_to_switch_name(component: str, instance_name: str) -> str:
return (
f"{instance_name} "
f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} "
f"{COMPONENT_TO_NAME.get(component, component.capitalize())}"
f"{KEY_COMPONENTID_TO_NAME.get(component, component.capitalize())}"
)

View File

@ -793,7 +793,7 @@ huisbaasje-client==0.1.0
hydrawiser==0.2
# homeassistant.components.hyperion
hyperion-py==0.7.0
hyperion-py==0.7.2
# homeassistant.components.bh1750
# homeassistant.components.bme280

View File

@ -448,7 +448,7 @@ huawei-lte-api==1.4.17
huisbaasje-client==0.1.0
# homeassistant.components.hyperion
hyperion-py==0.7.0
hyperion-py==0.7.2
# homeassistant.components.iaqualink
iaqualink==0.3.4

View File

@ -382,8 +382,9 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
assert entity_state
assert entity_state.attributes["brightness"] == brightness
# On (=), 100% (=), V4L (!), [0,255,255] (=)
effect = const.KEY_COMPONENTID_EXTERNAL_SOURCES[2] # V4L
# 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(
@ -422,7 +423,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
}
),
]
client.visible_priority = {const.KEY_COMPONENTID: effect}
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
@ -505,30 +506,126 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
assert not client.async_send_set_effect.called
async def test_light_async_turn_on_error_conditions(hass: HomeAssistantType) -> None:
"""Test error conditions when turning the light on."""
async def test_light_async_turn_on_fail_async_send_set_component(
hass: HomeAssistantType,
) -> 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)
# On (=), 100% (=), solid (=), [255,255,255] (=)
await hass.services.async_call(
LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True
)
assert client.async_send_set_component.call_args == call(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL,
const.KEY_STATE: True,
}
}
assert client.method_calls[-1] == call.async_send_set_component(
componentstate={"component": "ALL", "state": True}
)
async def test_light_async_turn_off_error_conditions(hass: HomeAssistantType) -> None:
"""Test error conditions when turning the light off."""
async def test_light_async_turn_on_fail_async_send_set_component_source(
hass: HomeAssistantType,
) -> 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: HomeAssistantType,
) -> 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: HomeAssistantType,
) -> 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: HomeAssistantType,
) -> None:
"""Test async_send_set_effect failure when turning on the light."""
client = create_mock_client()
client.is_on = Mock(return_value=True)
client.async_send_clear = AsyncMock(return_value=True)
client.async_send_set_effect = 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_set_effect(
priority=180, effect={"name": "Warm Mood Blobs"}, origin="Home Assistant"
)
async def test_light_async_turn_on_fail_async_send_set_color(
hass: HomeAssistantType,
) -> None:
"""Test async_send_set_color failure when turning on the light."""
client = create_mock_client()
client.is_on = Mock(return_value=True)
client.async_send_clear = AsyncMock(return_value=True)
client.async_send_set_color = 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_HS_COLOR: (240.0, 100.0)},
blocking=True,
)
assert client.method_calls[-1] == call.async_send_set_color(
priority=180, color=(0, 0, 255), origin="Home Assistant"
)
async def test_light_async_turn_off_fail_async_send_set_component(
hass: HomeAssistantType,
) -> None:
"""Test async_send_set_component failure when turning off the light."""
client = create_mock_client()
client.async_send_set_component = AsyncMock(return_value=False)
await setup_test_config_entry(hass, hyperion_client=client)
@ -539,17 +636,32 @@ async def test_light_async_turn_off_error_conditions(hass: HomeAssistantType) ->
{ATTR_ENTITY_ID: TEST_ENTITY_ID_1},
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.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: HomeAssistantType,
) -> 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)
async def test_light_async_turn_off(hass: HomeAssistantType) -> None:
"""Test turning the light off."""
client = create_mock_client()
@ -636,7 +748,10 @@ async def test_light_async_updates_from_hyperion_client(
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_V4L
assert (
entity_state.attributes["effect"]
== const.KEY_COMPONENTID_TO_NAME[const.KEY_COMPONENTID_V4L]
)
# Update priorities (Effect)
effect = "foo"
@ -682,7 +797,10 @@ 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_EXTERNAL_SOURCES + [
] + [
const.KEY_COMPONENTID_TO_NAME[component]
for component in const.KEY_COMPONENTID_EXTERNAL_SOURCES
] + [
effect[const.KEY_NAME] for effect in effects
]
@ -1171,15 +1289,17 @@ async def test_light_option_effect_hide_list(hass: HomeAssistantType) -> None:
client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
await setup_test_config_entry(
hass, hyperion_client=client, options={CONF_EFFECT_HIDE_LIST: ["Two", "V4L"]}
hass,
hyperion_client=client,
options={CONF_EFFECT_HIDE_LIST: ["Two", "USB Capture"]},
)
entity_state = hass.states.get(TEST_ENTITY_ID_1)
assert entity_state
assert entity_state.attributes["effect_list"] == [
"Solid",
"BOBLIGHTSERVER",
"GRABBER",
"Boblight Server",
"Platform Capture",
"One",
]
@ -1247,3 +1367,45 @@ async def test_lights_can_be_enabled(hass: HomeAssistantType) -> None:
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
assert entity_state
async def test_deprecated_effect_names(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def]
"""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 "Use of Hyperion effect '%s' is deprecated" % component 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: HomeAssistantType,
) -> 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"]

View File

@ -5,13 +5,13 @@ from unittest.mock import AsyncMock, call, patch
from hyperion.const import (
KEY_COMPONENT,
KEY_COMPONENTID_ALL,
KEY_COMPONENTID_TO_NAME,
KEY_COMPONENTSTATE,
KEY_STATE,
)
from homeassistant.components.hyperion import get_hyperion_device_id
from homeassistant.components.hyperion.const import (
COMPONENT_TO_NAME,
DOMAIN,
HYPERION_MANUFACTURER_NAME,
HYPERION_MODEL_NAME,
@ -128,7 +128,7 @@ async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None:
# Setup component switch.
for component in TEST_COMPONENTS:
name = slugify(COMPONENT_TO_NAME[str(component["name"])])
name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])])
register_test_entity(
hass,
SWITCH_DOMAIN,
@ -138,7 +138,7 @@ async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None:
await setup_test_config_entry(hass, hyperion_client=client)
for component in TEST_COMPONENTS:
name = slugify(COMPONENT_TO_NAME[str(component["name"])])
name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])])
entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name
entity_state = hass.states.get(entity_id)
assert entity_state, f"Couldn't find entity: {entity_id}"
@ -150,13 +150,14 @@ async def test_device_info(hass: HomeAssistantType) -> None:
client.components = TEST_COMPONENTS
for component in TEST_COMPONENTS:
name = slugify(COMPONENT_TO_NAME[str(component["name"])])
name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])])
register_test_entity(
hass,
SWITCH_DOMAIN,
f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE}_{name}",
f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_{name}",
)
await setup_test_config_entry(hass, hyperion_client=client)
assert hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) is not None
@ -178,7 +179,7 @@ async def test_device_info(hass: HomeAssistantType) -> None:
]
for component in TEST_COMPONENTS:
name = slugify(COMPONENT_TO_NAME[str(component["name"])])
name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])])
entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name
assert entity_id in entities_from_device
@ -192,7 +193,7 @@ async def test_switches_can_be_enabled(hass: HomeAssistantType) -> None:
entity_registry = er.async_get(hass)
for component in TEST_COMPONENTS:
name = slugify(COMPONENT_TO_NAME[str(component["name"])])
name = slugify(KEY_COMPONENTID_TO_NAME[str(component["name"])])
entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name
entry = entity_registry.async_get(entity_id)