Use runtime data for hyperion (#143461)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Marc Mueller 2025-04-23 01:24:47 +02:00 committed by GitHub
parent 0b2e5cd253
commit 2d20df37b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 67 additions and 93 deletions

View File

@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable from collections.abc import Callable
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass
import logging import logging
from typing import Any, cast from typing import Any, cast
@ -22,9 +23,6 @@ from homeassistant.helpers.dispatcher import (
) )
from .const import ( from .const import (
CONF_INSTANCE_CLIENTS,
CONF_ON_UNLOAD,
CONF_ROOT_CLIENT,
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
HYPERION_RELEASES_URL, HYPERION_RELEASES_URL,
@ -52,15 +50,15 @@ _LOGGER = logging.getLogger(__name__)
# The get_hyperion_unique_id method will create a per-entity unique id when given the # The get_hyperion_unique_id method will create a per-entity unique id when given the
# server id, an instance number and a name. # server id, an instance number and a name.
# hass.data format type HyperionConfigEntry = ConfigEntry[HyperionData]
# ================
#
# hass.data[DOMAIN] = { @dataclass
# <config_entry.entry_id>: { class HyperionData:
# "ROOT_CLIENT": <Hyperion Client>, """Hyperion runtime data."""
# "ON_UNLOAD": [<callable>, ...],
# } root_client: client.HyperionClient
# } instance_clients: dict[int, client.HyperionClient]
def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str: def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str:
@ -107,29 +105,29 @@ async def async_create_connect_hyperion_client(
@callback @callback
def listen_for_instance_updates( def listen_for_instance_updates(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, entry: HyperionConfigEntry,
add_func: Callable, add_func: Callable[[int, str], None],
remove_func: Callable, remove_func: Callable[[int], None],
) -> None: ) -> None:
"""Listen for instance additions/removals.""" """Listen for instance additions/removals."""
hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].extend( entry.async_on_unload(
[ async_dispatcher_connect(
async_dispatcher_connect( hass,
hass, SIGNAL_INSTANCE_ADD.format(entry.entry_id),
SIGNAL_INSTANCE_ADD.format(config_entry.entry_id), add_func,
add_func, )
), )
async_dispatcher_connect( entry.async_on_unload(
hass, async_dispatcher_connect(
SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), hass,
remove_func, SIGNAL_INSTANCE_REMOVE.format(entry.entry_id),
), remove_func,
] )
) )
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: HyperionConfigEntry) -> bool:
"""Set up Hyperion from a config entry.""" """Set up Hyperion from a config entry."""
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT] port = entry.data[CONF_PORT]
@ -185,12 +183,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# We need 1 root client (to manage instances being removed/added) and then 1 client # We need 1 root client (to manage instances being removed/added) and then 1 client
# per Hyperion server instance which is shared for all entities associated with # per Hyperion server instance which is shared for all entities associated with
# that instance. # that instance.
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = HyperionData(
hass.data[DOMAIN][entry.entry_id] = { root_client=hyperion_client,
CONF_ROOT_CLIENT: hyperion_client, instance_clients={},
CONF_INSTANCE_CLIENTS: {}, )
CONF_ON_UNLOAD: [],
}
async def async_instances_to_clients(response: dict[str, Any]) -> None: async def async_instances_to_clients(response: dict[str, Any]) -> None:
"""Convert instances to Hyperion clients.""" """Convert instances to Hyperion clients."""
@ -203,7 +199,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device_registry = dr.async_get(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][entry.entry_id][CONF_INSTANCE_CLIENTS] existing_instances = entry.runtime_data.instance_clients
server_id = cast(str, entry.unique_id) server_id = cast(str, entry.unique_id)
# In practice, an instance can be in 3 states as seen by this function: # In practice, an instance can be in 3 states as seen by this function:
@ -270,39 +266,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
assert hyperion_client assert hyperion_client
if hyperion_client.instances is not None: if hyperion_client.instances is not None:
await async_instances_to_clients_raw(hyperion_client.instances) await async_instances_to_clients_raw(hyperion_client.instances)
hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append( entry.async_on_unload(entry.add_update_listener(_async_entry_updated))
entry.add_update_listener(_async_entry_updated)
)
return True return True
async def _async_entry_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: async def _async_entry_updated(hass: HomeAssistant, entry: HyperionConfigEntry) -> None:
"""Handle entry updates.""" """Handle entry updates."""
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: HyperionConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
config_entry, PLATFORMS if unload_ok:
)
if unload_ok and config_entry.entry_id in hass.data[DOMAIN]:
config_data = hass.data[DOMAIN].pop(config_entry.entry_id)
for func in config_data[CONF_ON_UNLOAD]:
func()
# Disconnect the shared instance clients. # Disconnect the shared instance clients.
await asyncio.gather( await asyncio.gather(
*( *(
config_data[CONF_INSTANCE_CLIENTS][ inst.async_client_disconnect()
instance_num for inst in entry.runtime_data.instance_clients.values()
].async_client_disconnect()
for instance_num in config_data[CONF_INSTANCE_CLIENTS]
) )
) )
# Disconnect the root client. # Disconnect the root client.
root_client = config_data[CONF_ROOT_CLIENT] root_client = entry.runtime_data.root_client
await root_client.async_client_disconnect() await root_client.async_client_disconnect()
return unload_ok return unload_ok

View File

@ -25,7 +25,6 @@ from homeassistant.components.camera import (
Camera, Camera,
async_get_still_stream, async_get_still_stream,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@ -35,12 +34,12 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import ( from . import (
HyperionConfigEntry,
get_hyperion_device_id, get_hyperion_device_id,
get_hyperion_unique_id, get_hyperion_unique_id,
listen_for_instance_updates, listen_for_instance_updates,
) )
from .const import ( from .const import (
CONF_INSTANCE_CLIENTS,
DOMAIN, DOMAIN,
HYPERION_MANUFACTURER_NAME, HYPERION_MANUFACTURER_NAME,
HYPERION_MODEL_NAME, HYPERION_MODEL_NAME,
@ -53,12 +52,11 @@ IMAGE_STREAM_JPG_SENTINEL = "data:image/jpg;base64,"
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, entry: HyperionConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up a Hyperion platform from config entry.""" """Set up a Hyperion platform from config entry."""
entry_data = hass.data[DOMAIN][config_entry.entry_id] server_id = entry.unique_id
server_id = config_entry.unique_id
def camera_unique_id(instance_num: int) -> str: def camera_unique_id(instance_num: int) -> str:
"""Return the camera unique_id.""" """Return the camera unique_id."""
@ -75,7 +73,7 @@ async def async_setup_entry(
server_id, server_id,
instance_num, instance_num,
instance_name, instance_name,
entry_data[CONF_INSTANCE_CLIENTS][instance_num], entry.runtime_data.instance_clients[instance_num],
) )
] ]
) )
@ -91,7 +89,7 @@ async def async_setup_entry(
), ),
) )
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove) listen_for_instance_updates(hass, entry, instance_add, instance_remove)
# A note on Hyperion streaming semantics: # A note on Hyperion streaming semantics:

View File

@ -3,10 +3,7 @@
CONF_AUTH_ID = "auth_id" CONF_AUTH_ID = "auth_id"
CONF_CREATE_TOKEN = "create_token" CONF_CREATE_TOKEN = "create_token"
CONF_INSTANCE = "instance" CONF_INSTANCE = "instance"
CONF_INSTANCE_CLIENTS = "INSTANCE_CLIENTS"
CONF_ON_UNLOAD = "ON_UNLOAD"
CONF_PRIORITY = "priority" CONF_PRIORITY = "priority"
CONF_ROOT_CLIENT = "ROOT_CLIENT"
CONF_EFFECT_HIDE_LIST = "effect_hide_list" CONF_EFFECT_HIDE_LIST = "effect_hide_list"
CONF_EFFECT_SHOW_LIST = "effect_show_list" CONF_EFFECT_SHOW_LIST = "effect_show_list"

View File

@ -17,7 +17,6 @@ from homeassistant.components.light import (
LightEntity, LightEntity,
LightEntityFeature, LightEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@ -28,13 +27,13 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util from homeassistant.util import color as color_util
from . import ( from . import (
HyperionConfigEntry,
get_hyperion_device_id, get_hyperion_device_id,
get_hyperion_unique_id, get_hyperion_unique_id,
listen_for_instance_updates, listen_for_instance_updates,
) )
from .const import ( from .const import (
CONF_EFFECT_HIDE_LIST, CONF_EFFECT_HIDE_LIST,
CONF_INSTANCE_CLIENTS,
CONF_PRIORITY, CONF_PRIORITY,
DEFAULT_ORIGIN, DEFAULT_ORIGIN,
DEFAULT_PRIORITY, DEFAULT_PRIORITY,
@ -74,28 +73,26 @@ ICON_EFFECT = "mdi:lava-lamp"
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, entry: HyperionConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up a Hyperion platform from config entry.""" """Set up a Hyperion platform from config entry."""
entry_data = hass.data[DOMAIN][config_entry.entry_id] server_id = entry.unique_id
server_id = config_entry.unique_id
@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."""
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(*args), HyperionLight(
server_id,
instance_num,
instance_name,
entry.options,
entry.runtime_data.instance_clients[instance_num],
),
] ]
) )
@ -110,7 +107,7 @@ async def async_setup_entry(
), ),
) )
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove) listen_for_instance_updates(hass, entry, instance_add, instance_remove)
class HyperionLight(LightEntity): class HyperionLight(LightEntity):

View File

@ -19,7 +19,6 @@ from hyperion.const import (
) )
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@ -29,12 +28,12 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import ( from . import (
HyperionConfigEntry,
get_hyperion_device_id, get_hyperion_device_id,
get_hyperion_unique_id, get_hyperion_unique_id,
listen_for_instance_updates, listen_for_instance_updates,
) )
from .const import ( from .const import (
CONF_INSTANCE_CLIENTS,
DOMAIN, DOMAIN,
HYPERION_MANUFACTURER_NAME, HYPERION_MANUFACTURER_NAME,
HYPERION_MODEL_NAME, HYPERION_MODEL_NAME,
@ -62,12 +61,11 @@ def _sensor_unique_id(server_id: str, instance_num: int, suffix: str) -> str:
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, entry: HyperionConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up a Hyperion platform from config entry.""" """Set up a Hyperion platform from config entry."""
entry_data = hass.data[DOMAIN][config_entry.entry_id] server_id = entry.unique_id
server_id = config_entry.unique_id
@callback @callback
def instance_add(instance_num: int, instance_name: str) -> None: def instance_add(instance_num: int, instance_name: str) -> None:
@ -78,7 +76,7 @@ async def async_setup_entry(
server_id, server_id,
instance_num, instance_num,
instance_name, instance_name,
entry_data[CONF_INSTANCE_CLIENTS][instance_num], entry.runtime_data.instance_clients[instance_num],
PRIORITY_SENSOR_DESCRIPTION, PRIORITY_SENSOR_DESCRIPTION,
) )
] ]
@ -98,7 +96,7 @@ async def async_setup_entry(
), ),
) )
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove) listen_for_instance_updates(hass, entry, instance_add, instance_remove)
class HyperionSensor(SensorEntity): class HyperionSensor(SensorEntity):

View File

@ -26,7 +26,6 @@ from hyperion.const import (
) )
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@ -38,12 +37,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import slugify from homeassistant.util import slugify
from . import ( from . import (
HyperionConfigEntry,
get_hyperion_device_id, get_hyperion_device_id,
get_hyperion_unique_id, get_hyperion_unique_id,
listen_for_instance_updates, listen_for_instance_updates,
) )
from .const import ( from .const import (
CONF_INSTANCE_CLIENTS,
DOMAIN, DOMAIN,
HYPERION_MANUFACTURER_NAME, HYPERION_MANUFACTURER_NAME,
HYPERION_MODEL_NAME, HYPERION_MODEL_NAME,
@ -89,12 +88,11 @@ def _component_to_translation_key(component: str) -> str:
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, entry: HyperionConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up a Hyperion platform from config entry.""" """Set up a Hyperion platform from config entry."""
entry_data = hass.data[DOMAIN][config_entry.entry_id] server_id = entry.unique_id
server_id = config_entry.unique_id
@callback @callback
def instance_add(instance_num: int, instance_name: str) -> None: def instance_add(instance_num: int, instance_name: str) -> None:
@ -106,7 +104,7 @@ async def async_setup_entry(
instance_num, instance_num,
instance_name, instance_name,
component, component,
entry_data[CONF_INSTANCE_CLIENTS][instance_num], entry.runtime_data.instance_clients[instance_num],
) )
for component in COMPONENT_SWITCHES for component in COMPONENT_SWITCHES
) )
@ -123,7 +121,7 @@ async def async_setup_entry(
), ),
) )
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove) listen_for_instance_updates(hass, entry, instance_add, instance_remove)
class HyperionComponentSwitch(SwitchEntity): class HyperionComponentSwitch(SwitchEntity):