Add Hyperion device support (#47881)

* Add Hyperion device support.

* Update to the new typing annotations.

* Add device cleanup logic.

* Fixes based on the excellent feedback from emontnemery
This commit is contained in:
Dermot Duffy 2021-04-13 01:35:38 -07:00 committed by GitHub
parent 5bf3469ffc
commit 63d42867e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 389 additions and 129 deletions

View File

@ -15,14 +15,11 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
) )
from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry,
async_get_registry,
)
from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import ( from .const import (
@ -72,6 +69,11 @@ def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str:
return f"{server_id}_{instance}_{name}" return f"{server_id}_{instance}_{name}"
def get_hyperion_device_id(server_id: str, instance: int) -> str:
"""Get an id for a Hyperion device/instance."""
return f"{server_id}_{instance}"
def split_hyperion_unique_id(unique_id: str) -> tuple[str, int, str] | None: def split_hyperion_unique_id(unique_id: str) -> tuple[str, int, str] | None:
"""Split a unique_id into a (server_id, instance, type) tuple.""" """Split a unique_id into a (server_id, instance, type) tuple."""
data = tuple(unique_id.split("_", 2)) data = tuple(unique_id.split("_", 2))
@ -202,7 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def async_instances_to_clients_raw(instances: list[dict[str, Any]]) -> None: async def async_instances_to_clients_raw(instances: list[dict[str, Any]]) -> None:
"""Convert instances to Hyperion clients.""" """Convert instances to Hyperion clients."""
registry = await async_get_registry(hass) device_registry = dr.async_get(hass)
running_instances: set[int] = set() running_instances: set[int] = set()
stopped_instances: set[int] = set() stopped_instances: set[int] = set()
existing_instances = hass.data[DOMAIN][config_entry.entry_id][ existing_instances = hass.data[DOMAIN][config_entry.entry_id][
@ -249,15 +251,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
hass, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num hass, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num
) )
# Deregister entities that belong to removed instances. # Ensure every device associated with this config entry is still in the list of
for entry in async_entries_for_config_entry(registry, config_entry.entry_id): # motionEye cameras, otherwise remove the device (and thus entities).
data = split_hyperion_unique_id(entry.unique_id) known_devices = {
if not data: get_hyperion_device_id(server_id, instance_num)
continue for instance_num in running_instances | stopped_instances
if data[0] == server_id and ( }
data[1] not in running_instances and data[1] not in stopped_instances for device_entry in dr.async_entries_for_config_entry(
): device_registry, config_entry.entry_id
registry.async_remove(entry.entity_id) ):
for (kind, key) in device_entry.identifiers:
if kind == DOMAIN and key in known_devices:
break
else:
device_registry.async_remove_device(device_entry.id)
hyperion_client.set_callbacks( hyperion_client.set_callbacks(
{ {

View File

@ -40,6 +40,8 @@ DEFAULT_PRIORITY = 128
DOMAIN = "hyperion" DOMAIN = "hyperion"
HYPERION_MANUFACTURER_NAME = "Hyperion"
HYPERION_MODEL_NAME = f"{HYPERION_MANUFACTURER_NAME}-NG"
HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases"
HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9"

View File

@ -26,7 +26,11 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from . import get_hyperion_unique_id, listen_for_instance_updates from . import (
get_hyperion_device_id,
get_hyperion_unique_id,
listen_for_instance_updates,
)
from .const import ( from .const import (
CONF_EFFECT_HIDE_LIST, CONF_EFFECT_HIDE_LIST,
CONF_INSTANCE_CLIENTS, CONF_INSTANCE_CLIENTS,
@ -34,6 +38,8 @@ from .const import (
DEFAULT_ORIGIN, DEFAULT_ORIGIN,
DEFAULT_PRIORITY, DEFAULT_PRIORITY,
DOMAIN, DOMAIN,
HYPERION_MANUFACTURER_NAME,
HYPERION_MODEL_NAME,
NAME_SUFFIX_HYPERION_LIGHT, NAME_SUFFIX_HYPERION_LIGHT,
NAME_SUFFIX_HYPERION_PRIORITY_LIGHT, NAME_SUFFIX_HYPERION_PRIORITY_LIGHT,
SIGNAL_ENTITY_REMOVE, SIGNAL_ENTITY_REMOVE,
@ -85,24 +91,17 @@ async def async_setup_entry(
def instance_add(instance_num: int, instance_name: str) -> None: def instance_add(instance_num: int, instance_name: str) -> None:
"""Add entities for a new Hyperion instance.""" """Add entities for a new Hyperion instance."""
assert server_id assert server_id
args = (
server_id,
instance_num,
instance_name,
config_entry.options,
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
)
async_add_entities( async_add_entities(
[ [
HyperionLight( HyperionLight(*args),
get_hyperion_unique_id( HyperionPriorityLight(*args),
server_id, instance_num, TYPE_HYPERION_LIGHT
),
f"{instance_name} {NAME_SUFFIX_HYPERION_LIGHT}",
config_entry.options,
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
),
HyperionPriorityLight(
get_hyperion_unique_id(
server_id, instance_num, TYPE_HYPERION_PRIORITY_LIGHT
),
f"{instance_name} {NAME_SUFFIX_HYPERION_PRIORITY_LIGHT}",
config_entry.options,
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
),
] ]
) )
@ -127,14 +126,17 @@ class HyperionBaseLight(LightEntity):
def __init__( def __init__(
self, self,
unique_id: str, server_id: str,
name: str, instance_num: int,
instance_name: str,
options: MappingProxyType[str, Any], options: MappingProxyType[str, Any],
hyperion_client: client.HyperionClient, hyperion_client: client.HyperionClient,
) -> None: ) -> None:
"""Initialize the light.""" """Initialize the light."""
self._unique_id = unique_id self._unique_id = self._compute_unique_id(server_id, instance_num)
self._name = name 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._options = options
self._client = hyperion_client self._client = hyperion_client
@ -156,6 +158,14 @@ class HyperionBaseLight(LightEntity):
f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client, 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 @property
def entity_registry_enabled_default(self) -> bool: def entity_registry_enabled_default(self) -> bool:
"""Whether or not the entity is enabled by default.""" """Whether or not the entity is enabled by default."""
@ -216,6 +226,16 @@ class HyperionBaseLight(LightEntity):
"""Return a unique id for this instance.""" """Return a unique id for this instance."""
return self._unique_id return self._unique_id
@property
def device_info(self) -> dict[str, Any] | None:
"""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: def _get_option(self, key: str) -> Any:
"""Get a value from the provided options.""" """Get a value from the provided options."""
defaults = { defaults = {
@ -412,7 +432,7 @@ class HyperionBaseLight(LightEntity):
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, self.hass,
SIGNAL_ENTITY_REMOVE.format(self._unique_id), SIGNAL_ENTITY_REMOVE.format(self.unique_id),
functools.partial(self.async_remove, force_remove=True), functools.partial(self.async_remove, force_remove=True),
) )
) )
@ -455,6 +475,14 @@ class HyperionLight(HyperionBaseLight):
shown state rather than exclusively the HA priority. 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 @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if light is on.""" """Return true if light is on."""
@ -504,6 +532,16 @@ class HyperionLight(HyperionBaseLight):
class HyperionPriorityLight(HyperionBaseLight): class HyperionPriorityLight(HyperionBaseLight):
"""A Hyperion light that only acts on a single Hyperion priority.""" """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 @property
def entity_registry_enabled_default(self) -> bool: def entity_registry_enabled_default(self) -> bool:
"""Whether or not the entity is enabled by default.""" """Whether or not the entity is enabled by default."""

View File

@ -33,11 +33,17 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import slugify from homeassistant.util import slugify
from . import get_hyperion_unique_id, listen_for_instance_updates from . import (
get_hyperion_device_id,
get_hyperion_unique_id,
listen_for_instance_updates,
)
from .const import ( from .const import (
COMPONENT_TO_NAME, COMPONENT_TO_NAME,
CONF_INSTANCE_CLIENTS, CONF_INSTANCE_CLIENTS,
DOMAIN, DOMAIN,
HYPERION_MANUFACTURER_NAME,
HYPERION_MODEL_NAME,
NAME_SUFFIX_HYPERION_COMPONENT_SWITCH, NAME_SUFFIX_HYPERION_COMPONENT_SWITCH,
SIGNAL_ENTITY_REMOVE, SIGNAL_ENTITY_REMOVE,
TYPE_HYPERION_COMPONENT_SWITCH_BASE, TYPE_HYPERION_COMPONENT_SWITCH_BASE,
@ -55,6 +61,26 @@ COMPONENT_SWITCHES = [
] ]
def _component_to_unique_id(server_id: str, component: str, instance_num: int) -> str:
"""Convert a component to a unique_id."""
return get_hyperion_unique_id(
server_id,
instance_num,
slugify(
f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}"
),
)
def _component_to_switch_name(component: str, instance_name: str) -> str:
"""Convert a component to a switch name."""
return (
f"{instance_name} "
f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} "
f"{COMPONENT_TO_NAME.get(component, component.capitalize())}"
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
) -> bool: ) -> bool:
@ -62,27 +88,6 @@ async def async_setup_entry(
entry_data = hass.data[DOMAIN][config_entry.entry_id] entry_data = hass.data[DOMAIN][config_entry.entry_id]
server_id = config_entry.unique_id server_id = config_entry.unique_id
def component_to_switch_type(component: str) -> str:
"""Convert a component to a switch type string."""
return slugify(
f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}"
)
def component_to_unique_id(component: str, instance_num: int) -> str:
"""Convert a component to a unique_id."""
assert server_id
return get_hyperion_unique_id(
server_id, instance_num, component_to_switch_type(component)
)
def component_to_switch_name(component: str, instance_name: str) -> str:
"""Convert a component to a switch name."""
return (
f"{instance_name} "
f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} "
f"{COMPONENT_TO_NAME.get(component, component.capitalize())}"
)
@callback @callback
def instance_add(instance_num: int, instance_name: str) -> None: def instance_add(instance_num: int, instance_name: str) -> None:
"""Add entities for a new Hyperion instance.""" """Add entities for a new Hyperion instance."""
@ -91,8 +96,9 @@ async def async_setup_entry(
for component in COMPONENT_SWITCHES: for component in COMPONENT_SWITCHES:
switches.append( switches.append(
HyperionComponentSwitch( HyperionComponentSwitch(
component_to_unique_id(component, instance_num), server_id,
component_to_switch_name(component, instance_name), instance_num,
instance_name,
component, component,
entry_data[CONF_INSTANCE_CLIENTS][instance_num], entry_data[CONF_INSTANCE_CLIENTS][instance_num],
), ),
@ -107,7 +113,7 @@ async def async_setup_entry(
async_dispatcher_send( async_dispatcher_send(
hass, hass,
SIGNAL_ENTITY_REMOVE.format( SIGNAL_ENTITY_REMOVE.format(
component_to_unique_id(component, instance_num), _component_to_unique_id(server_id, component, instance_num),
), ),
) )
@ -120,14 +126,19 @@ class HyperionComponentSwitch(SwitchEntity):
def __init__( def __init__(
self, self,
unique_id: str, server_id: str,
name: str, instance_num: int,
instance_name: str,
component_name: str, component_name: str,
hyperion_client: client.HyperionClient, hyperion_client: client.HyperionClient,
) -> None: ) -> None:
"""Initialize the switch.""" """Initialize the switch."""
self._unique_id = unique_id self._unique_id = _component_to_unique_id(
self._name = name server_id, component_name, instance_num
)
self._device_id = get_hyperion_device_id(server_id, instance_num)
self._name = _component_to_switch_name(component_name, instance_name)
self._instance_name = instance_name
self._component_name = component_name self._component_name = component_name
self._client = hyperion_client self._client = hyperion_client
self._client_callbacks = { self._client_callbacks = {
@ -168,6 +179,16 @@ class HyperionComponentSwitch(SwitchEntity):
"""Return server availability.""" """Return server availability."""
return bool(self._client.has_loaded_state) return bool(self._client.has_loaded_state)
@property
def device_info(self) -> dict[str, Any] | None:
"""Return device information."""
return {
"identifiers": {(DOMAIN, self._device_id)},
"name": self._instance_name,
"manufacturer": HYPERION_MANUFACTURER_NAME,
"model": HYPERION_MODEL_NAME,
}
async def _async_send_set_component(self, value: bool) -> None: async def _async_send_set_component(self, value: bool) -> None:
"""Send a component control request.""" """Send a component control request."""
await self._client.async_send_set_component( await self._client.async_send_set_component(

View File

@ -7,9 +7,11 @@ from unittest.mock import AsyncMock, Mock, patch
from hyperion import const from hyperion import const
from homeassistant.components.hyperion import get_hyperion_unique_id
from homeassistant.components.hyperion.const import CONF_PRIORITY, DOMAIN from homeassistant.components.hyperion.const import CONF_PRIORITY, DOMAIN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -20,7 +22,7 @@ TEST_PORT_UI = const.DEFAULT_PORT_UI + 1
TEST_INSTANCE = 1 TEST_INSTANCE = 1
TEST_ID = "default" TEST_ID = "default"
TEST_SYSINFO_ID = "f9aab089-f85a-55cf-b7c1-222a72faebe9" TEST_SYSINFO_ID = "f9aab089-f85a-55cf-b7c1-222a72faebe9"
TEST_SYSINFO_VERSION = "2.0.0-alpha.8" TEST_SYSINFO_VERSION = "2.0.0-alpha.9"
TEST_PRIORITY = 180 TEST_PRIORITY = 180
TEST_ENTITY_ID_1 = "light.test_instance_1" TEST_ENTITY_ID_1 = "light.test_instance_1"
TEST_ENTITY_ID_2 = "light.test_instance_2" TEST_ENTITY_ID_2 = "light.test_instance_2"
@ -168,3 +170,20 @@ def call_registered_callback(
for call in client.add_callbacks.call_args_list: for call in client.add_callbacks.call_args_list:
if key in call[0][0]: if key in call[0][0]:
call[0][0][key](*args, **kwargs) call[0][0][key](*args, **kwargs)
def register_test_entity(
hass: HomeAssistantType, domain: str, type_name: str, entity_id: str
) -> None:
"""Register a test entity."""
unique_id = get_hyperion_unique_id(TEST_SYSINFO_ID, TEST_INSTANCE, type_name)
entity_id = entity_id.split(".")[1]
entity_registry = er.async_get(hass)
entity_registry.async_get_or_create(
domain,
DOMAIN,
unique_id,
suggested_object_id=entity_id,
disabled_by=None,
)

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from typing import Any from typing import Any, Awaitable
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from hyperion import const from hyperion import const
@ -419,13 +419,13 @@ async def test_auth_create_token_approval_declined_task_canceled(
class CanceledAwaitableMock(AsyncMock): class CanceledAwaitableMock(AsyncMock):
"""A canceled awaitable mock.""" """A canceled awaitable mock."""
def __await__(self): def __await__(self) -> None:
raise asyncio.CancelledError raise asyncio.CancelledError
mock_task = CanceledAwaitableMock() mock_task = CanceledAwaitableMock()
task_coro = None task_coro: Awaitable | None = None
def create_task(arg): def create_task(arg: Any) -> CanceledAwaitableMock:
nonlocal task_coro nonlocal task_coro
task_coro = arg task_coro = arg
return mock_task return mock_task
@ -453,6 +453,7 @@ async def test_auth_create_token_approval_declined_task_canceled(
result = await _configure_flow(hass, result) result = await _configure_flow(hass, result)
# This await will advance to the next step. # This await will advance to the next step.
assert task_coro
await task_coro await task_coro
# Assert that cancel is called on the task. # Assert that cancel is called on the task.

View File

@ -1,15 +1,22 @@
"""Tests for the Hyperion integration.""" """Tests for the Hyperion integration."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from unittest.mock import AsyncMock, Mock, call, patch from unittest.mock import AsyncMock, Mock, call, patch
from hyperion import const from hyperion import const
from homeassistant.components.hyperion import light as hyperion_light from homeassistant.components.hyperion import (
get_hyperion_device_id,
light as hyperion_light,
)
from homeassistant.components.hyperion.const import ( from homeassistant.components.hyperion.const import (
CONF_EFFECT_HIDE_LIST, CONF_EFFECT_HIDE_LIST,
DEFAULT_ORIGIN, DEFAULT_ORIGIN,
DOMAIN, DOMAIN,
HYPERION_MANUFACTURER_NAME,
HYPERION_MODEL_NAME,
TYPE_HYPERION_PRIORITY_LIGHT,
) )
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -19,6 +26,7 @@ from homeassistant.components.light import (
) )
from homeassistant.config_entries import ( from homeassistant.config_entries import (
ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_ERROR,
RELOAD_AFTER_UPDATE_DELAY,
SOURCE_REAUTH, SOURCE_REAUTH,
ConfigEntry, ConfigEntry,
) )
@ -31,18 +39,21 @@ from homeassistant.const import (
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
) )
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from . import ( from . import (
TEST_AUTH_NOT_REQUIRED_RESP, TEST_AUTH_NOT_REQUIRED_RESP,
TEST_AUTH_REQUIRED_RESP, TEST_AUTH_REQUIRED_RESP,
TEST_CONFIG_ENTRY_ID,
TEST_ENTITY_ID_1, TEST_ENTITY_ID_1,
TEST_ENTITY_ID_2, TEST_ENTITY_ID_2,
TEST_ENTITY_ID_3, TEST_ENTITY_ID_3,
TEST_HOST, TEST_HOST,
TEST_ID, TEST_ID,
TEST_INSTANCE,
TEST_INSTANCE_1, TEST_INSTANCE_1,
TEST_INSTANCE_2, TEST_INSTANCE_2,
TEST_INSTANCE_3, TEST_INSTANCE_3,
@ -53,9 +64,12 @@ from . import (
add_test_config_entry, add_test_config_entry,
call_registered_callback, call_registered_callback,
create_mock_client, create_mock_client,
register_test_entity,
setup_test_config_entry, setup_test_config_entry,
) )
from tests.common import async_fire_time_changed
COLOR_BLACK = color_util.COLORS["black"] COLOR_BLACK = color_util.COLORS["black"]
@ -814,11 +828,13 @@ async def test_priority_light_async_updates(
client = create_mock_client() client = create_mock_client()
client.priorities = [{**priority_template}] client.priorities = [{**priority_template}]
with patch( register_test_entity(
"homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" hass,
) as enabled_by_default_mock: LIGHT_DOMAIN,
enabled_by_default_mock.return_value = True TYPE_HYPERION_PRIORITY_LIGHT,
await setup_test_config_entry(hass, hyperion_client=client) 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. # == Scenario: Color at HA priority will show light as on.
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
@ -974,11 +990,13 @@ async def test_priority_light_async_updates_off_sets_black(
} }
] ]
with patch( register_test_entity(
"homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" hass,
) as enabled_by_default_mock: LIGHT_DOMAIN,
enabled_by_default_mock.return_value = True TYPE_HYPERION_PRIORITY_LIGHT,
await setup_test_config_entry(hass, hyperion_client=client) 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_clear = AsyncMock(return_value=True)
client.async_send_set_color = AsyncMock(return_value=True) client.async_send_set_color = AsyncMock(return_value=True)
@ -1026,11 +1044,13 @@ async def test_priority_light_prior_color_preserved_after_black(
client.priorities = [] client.priorities = []
client.visible_priority = None client.visible_priority = None
with patch( register_test_entity(
"homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" hass,
) as enabled_by_default_mock: LIGHT_DOMAIN,
enabled_by_default_mock.return_value = True TYPE_HYPERION_PRIORITY_LIGHT,
await setup_test_config_entry(hass, hyperion_client=client) TEST_PRIORITY_LIGHT_ENTITY_ID_1,
)
await setup_test_config_entry(hass, hyperion_client=client)
# Turn the light on full green... # Turn the light on full green...
# On (=), 100% (=), solid (=), [0,0,255] (=) # On (=), 100% (=), solid (=), [0,0,255] (=)
@ -1132,11 +1152,13 @@ async def test_priority_light_has_no_external_sources(hass: HomeAssistantType) -
client = create_mock_client() client = create_mock_client()
client.priorities = [] client.priorities = []
with patch( register_test_entity(
"homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" hass,
) as enabled_by_default_mock: LIGHT_DOMAIN,
enabled_by_default_mock.return_value = True TYPE_HYPERION_PRIORITY_LIGHT,
await setup_test_config_entry(hass, hyperion_client=client) 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) entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
assert entity_state assert entity_state
@ -1153,9 +1175,75 @@ async def test_light_option_effect_hide_list(hass: HomeAssistantType) -> None:
) )
entity_state = hass.states.get(TEST_ENTITY_ID_1) entity_state = hass.states.get(TEST_ENTITY_ID_1)
assert entity_state
assert entity_state.attributes["effect_list"] == [ assert entity_state.attributes["effect_list"] == [
"Solid", "Solid",
"BOBLIGHTSERVER", "BOBLIGHTSERVER",
"GRABBER", "GRABBER",
"One", "One",
] ]
async def test_device_info(hass: HomeAssistantType) -> 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)
device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, device_id)})
assert device
assert device.config_entries == {TEST_CONFIG_ENTRY_ID}
assert device.identifiers == {(DOMAIN, device_id)}
assert device.manufacturer == HYPERION_MANUFACTURER_NAME
assert device.model == HYPERION_MODEL_NAME
assert device.name == TEST_INSTANCE_1["friendly_name"]
entity_registry = await er.async_get_registry(hass)
entities_from_device = [
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: HomeAssistantType) -> 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 == "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( # type: ignore[no-untyped-call]
hass,
dt.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

View File

@ -1,27 +1,41 @@
"""Tests for the Hyperion integration.""" """Tests for the Hyperion integration."""
from datetime import timedelta
from unittest.mock import AsyncMock, call, patch from unittest.mock import AsyncMock, call, patch
from hyperion.const import ( from hyperion.const import (
KEY_COMPONENT, KEY_COMPONENT,
KEY_COMPONENTID_ALL, KEY_COMPONENTID_ALL,
KEY_COMPONENTID_BLACKBORDER,
KEY_COMPONENTID_BOBLIGHTSERVER,
KEY_COMPONENTID_FORWARDER,
KEY_COMPONENTID_GRABBER,
KEY_COMPONENTID_LEDDEVICE,
KEY_COMPONENTID_SMOOTHING,
KEY_COMPONENTID_V4L,
KEY_COMPONENTSTATE, KEY_COMPONENTSTATE,
KEY_STATE, KEY_STATE,
) )
from homeassistant.components.hyperion.const import COMPONENT_TO_NAME 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,
TYPE_HYPERION_COMPONENT_SWITCH_BASE,
)
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import slugify from homeassistant.util import dt, slugify
from . import call_registered_callback, create_mock_client, setup_test_config_entry from . import (
TEST_CONFIG_ENTRY_ID,
TEST_INSTANCE,
TEST_INSTANCE_1,
TEST_SYSINFO_ID,
call_registered_callback,
create_mock_client,
register_test_entity,
setup_test_config_entry,
)
from tests.common import async_fire_time_changed
TEST_COMPONENTS = [ TEST_COMPONENTS = [
{"enabled": True, "name": "ALL"}, {"enabled": True, "name": "ALL"},
@ -45,11 +59,13 @@ async def test_switch_turn_on_off(hass: HomeAssistantType) -> None:
client.components = TEST_COMPONENTS client.components = TEST_COMPONENTS
# Setup component switch. # Setup component switch.
with patch( register_test_entity(
"homeassistant.components.hyperion.switch.HyperionComponentSwitch.entity_registry_enabled_default" hass,
) as enabled_by_default_mock: SWITCH_DOMAIN,
enabled_by_default_mock.return_value = True f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE}_all",
await setup_test_config_entry(hass, hyperion_client=client) TEST_SWITCH_COMPONENT_ALL_ENTITY_ID,
)
await setup_test_config_entry(hass, hyperion_client=client)
# Verify switch is on (as per TEST_COMPONENTS above). # Verify switch is on (as per TEST_COMPONENTS above).
entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID)
@ -111,28 +127,96 @@ async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None:
client.components = TEST_COMPONENTS client.components = TEST_COMPONENTS
# Setup component switch. # Setup component switch.
with patch( for component in TEST_COMPONENTS:
"homeassistant.components.hyperion.switch.HyperionComponentSwitch.entity_registry_enabled_default" name = slugify(COMPONENT_TO_NAME[str(component["name"])])
) as enabled_by_default_mock: register_test_entity(
enabled_by_default_mock.return_value = True hass,
await setup_test_config_entry(hass, hyperion_client=client) SWITCH_DOMAIN,
f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE}_{name}",
entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_{name}",
for component in (
KEY_COMPONENTID_ALL,
KEY_COMPONENTID_SMOOTHING,
KEY_COMPONENTID_BLACKBORDER,
KEY_COMPONENTID_FORWARDER,
KEY_COMPONENTID_BOBLIGHTSERVER,
KEY_COMPONENTID_GRABBER,
KEY_COMPONENTID_LEDDEVICE,
KEY_COMPONENTID_V4L,
):
entity_id = (
TEST_SWITCH_COMPONENT_BASE_ENTITY_ID
+ "_"
+ slugify(COMPONENT_TO_NAME[component])
) )
await setup_test_config_entry(hass, hyperion_client=client)
for component in TEST_COMPONENTS:
name = slugify(COMPONENT_TO_NAME[str(component["name"])])
entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name
entity_state = hass.states.get(entity_id) entity_state = hass.states.get(entity_id)
assert entity_state, f"Couldn't find entity: {entity_id}" assert entity_state, f"Couldn't find entity: {entity_id}"
async def test_device_info(hass: HomeAssistantType) -> None:
"""Verify device information includes expected details."""
client = create_mock_client()
client.components = TEST_COMPONENTS
for component in TEST_COMPONENTS:
name = slugify(COMPONENT_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
device_identifer = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE)
device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, device_identifer)})
assert device
assert device.config_entries == {TEST_CONFIG_ENTRY_ID}
assert device.identifiers == {(DOMAIN, device_identifer)}
assert device.manufacturer == HYPERION_MANUFACTURER_NAME
assert device.model == HYPERION_MODEL_NAME
assert device.name == TEST_INSTANCE_1["friendly_name"]
entity_registry = await er.async_get_registry(hass)
entities_from_device = [
entry.entity_id
for entry in er.async_entries_for_device(entity_registry, device.id)
]
for component in TEST_COMPONENTS:
name = slugify(COMPONENT_TO_NAME[str(component["name"])])
entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name
assert entity_id in entities_from_device
async def test_switches_can_be_enabled(hass: HomeAssistantType) -> None:
"""Verify switches can be enabled."""
client = create_mock_client()
client.components = TEST_COMPONENTS
await setup_test_config_entry(hass, hyperion_client=client)
entity_registry = er.async_get(hass)
for component in TEST_COMPONENTS:
name = slugify(COMPONENT_TO_NAME[str(component["name"])])
entity_id = TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + "_" + name
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.disabled
assert entry.disabled_by == "integration"
entity_state = hass.states.get(entity_id)
assert not entity_state
with patch(
"homeassistant.components.hyperion.client.HyperionClient",
return_value=client,
):
updated_entry = entity_registry.async_update_entity(
entity_id, disabled_by=None
)
assert not updated_entry.disabled
await hass.async_block_till_done()
async_fire_time_changed( # type: ignore[no-untyped-call]
hass,
dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done()
entity_state = hass.states.get(entity_id)
assert entity_state