Refactor Snapcast client and group classes to use a common base clase (#124499)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Tucker Kern 2024-12-04 09:22:31 -07:00 committed by GitHub
parent b6b340ae63
commit b3ff8f56b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 359 additions and 375 deletions

View File

@ -1,37 +1,28 @@
"""Snapcast Integration."""
import logging
import snapcast.control
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, PLATFORMS
from .server import HomeAssistantSnapcast
_LOGGER = logging.getLogger(__name__)
from .coordinator import SnapcastUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Snapcast from a config entry."""
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
coordinator = SnapcastUpdateCoordinator(hass, host, port)
try:
server = await snapcast.control.create_server(
hass.loop, host, port, reconnect=True
)
await coordinator.async_config_entry_first_refresh()
except OSError as ex:
raise ConfigEntryNotReady(
f"Could not connect to Snapcast server at {host}:{port}"
) from ex
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantSnapcast(
hass, server, f"{host}:{port}", entry.entry_id
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@ -0,0 +1,72 @@
"""Data update coordinator for Snapcast server."""
from __future__ import annotations
import logging
from snapcast.control.server import Snapserver
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
_LOGGER = logging.getLogger(__name__)
class SnapcastUpdateCoordinator(DataUpdateCoordinator[None]):
"""Data update coordinator for pushed data from Snapcast server."""
def __init__(self, hass: HomeAssistant, host: str, port: int) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
logger=_LOGGER,
name=f"{host}:{port}",
update_interval=None, # Disable update interval as server pushes
)
self._server = Snapserver(hass.loop, host, port, True)
self.last_update_success = False
self._server.set_on_update_callback(self._on_update)
self._server.set_new_client_callback(self._on_update)
self._server.set_on_connect_callback(self._on_connect)
self._server.set_on_disconnect_callback(self._on_disconnect)
def _on_update(self) -> None:
"""Snapserver on_update callback."""
# Assume availability if an update is received.
self.last_update_success = True
self.async_update_listeners()
def _on_connect(self) -> None:
"""Snapserver on_connect callback."""
self.last_update_success = True
self.async_update_listeners()
def _on_disconnect(self, ex):
"""Snapsever on_disconnect callback."""
self.async_set_update_error(ex)
async def _async_setup(self) -> None:
"""Perform async setup for the coordinator."""
# Start the server
try:
await self._server.start()
except OSError as ex:
raise UpdateFailed from ex
async def _async_update_data(self) -> None:
"""Empty update method since data is pushed."""
async def disconnect(self) -> None:
"""Disconnect from the server."""
self._server.set_on_update_callback(None)
self._server.set_on_connect_callback(None)
self._server.set_on_disconnect_callback(None)
self._server.set_new_client_callback(None)
self._server.stop()
@property
def server(self) -> Snapserver:
"""Get the Snapserver object."""
return self._server

View File

@ -0,0 +1,11 @@
"""Coordinator entity for Snapcast server."""
from __future__ import annotations
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import SnapcastUpdateCoordinator
class SnapcastCoordinatorEntity(CoordinatorEntity[SnapcastUpdateCoordinator]):
"""Coordinator entity for Snapcast."""

View File

@ -2,18 +2,29 @@
from __future__ import annotations
from snapcast.control.server import Snapserver
from collections.abc import Mapping
import logging
from typing import Any
from snapcast.control.client import Snapclient
from snapcast.control.group import Snapgroup
import voluptuous as vol
from homeassistant.components.media_player import (
DOMAIN as MEDIA_PLAYER_DOMAIN,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
entity_registry as er,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
@ -30,6 +41,8 @@ from .const import (
SERVICE_SNAPSHOT,
SERVICE_UNJOIN,
)
from .coordinator import SnapcastUpdateCoordinator
from .entity import SnapcastCoordinatorEntity
STREAM_STATUS = {
"idle": MediaPlayerState.IDLE,
@ -37,21 +50,23 @@ STREAM_STATUS = {
"unknown": None,
}
_LOGGER = logging.getLogger(__name__)
def register_services():
def register_services() -> None:
"""Register snapcast services."""
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(SERVICE_SNAPSHOT, None, "snapshot")
platform.async_register_entity_service(SERVICE_RESTORE, None, "async_restore")
platform.async_register_entity_service(
SERVICE_JOIN, {vol.Required(ATTR_MASTER): cv.entity_id}, handle_async_join
SERVICE_JOIN, {vol.Required(ATTR_MASTER): cv.entity_id}, "async_join"
)
platform.async_register_entity_service(SERVICE_UNJOIN, None, handle_async_unjoin)
platform.async_register_entity_service(SERVICE_UNJOIN, None, "async_unjoin")
platform.async_register_entity_service(
SERVICE_SET_LATENCY,
{vol.Required(ATTR_LATENCY): cv.positive_int},
handle_set_latency,
"async_set_latency",
)
@ -61,51 +76,103 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the snapcast config entry."""
snapcast_server: Snapserver = hass.data[DOMAIN][config_entry.entry_id].server
# Fetch coordinator from global data
coordinator: SnapcastUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
# Create an ID for the Snapserver
host = config_entry.data[CONF_HOST]
port = config_entry.data[CONF_PORT]
host_id = f"{host}:{port}"
register_services()
host = config_entry.data[CONF_HOST]
port = config_entry.data[CONF_PORT]
hpid = f"{host}:{port}"
_known_group_ids: set[str] = set()
_known_client_ids: set[str] = set()
groups: list[MediaPlayerEntity] = [
SnapcastGroupDevice(group, hpid, config_entry.entry_id)
for group in snapcast_server.groups
]
clients: list[MediaPlayerEntity] = [
SnapcastClientDevice(client, hpid, config_entry.entry_id)
for client in snapcast_server.clients
]
async_add_entities(clients + groups)
hass.data[DOMAIN][
config_entry.entry_id
].hass_async_add_entities = async_add_entities
@callback
def _check_entities() -> None:
nonlocal _known_group_ids, _known_client_ids
def _update_known_ids(known_ids, ids) -> tuple[set[str], set[str]]:
ids_to_add = ids - known_ids
ids_to_remove = known_ids - ids
# Update known IDs
known_ids.difference_update(ids_to_remove)
known_ids.update(ids_to_add)
return ids_to_add, ids_to_remove
group_ids = {g.identifier for g in coordinator.server.groups}
groups_to_add, groups_to_remove = _update_known_ids(_known_group_ids, group_ids)
client_ids = {c.identifier for c in coordinator.server.clients}
clients_to_add, clients_to_remove = _update_known_ids(
_known_client_ids, client_ids
)
# Exit early if no changes
if not (groups_to_add | groups_to_remove | clients_to_add | clients_to_remove):
return
_LOGGER.debug(
"New clients: %s",
str([coordinator.server.client(c).friendly_name for c in clients_to_add]),
)
_LOGGER.debug(
"New groups: %s",
str([coordinator.server.group(g).friendly_name for g in groups_to_add]),
)
_LOGGER.debug(
"Remove client IDs: %s",
str([list(clients_to_remove)]),
)
_LOGGER.debug(
"Remove group IDs: %s",
str(list(groups_to_remove)),
)
# Add new entities
async_add_entities(
[
SnapcastGroupDevice(
coordinator, coordinator.server.group(group_id), host_id
)
for group_id in groups_to_add
]
+ [
SnapcastClientDevice(
coordinator, coordinator.server.client(client_id), host_id
)
for client_id in clients_to_add
]
)
# Remove stale entities
entity_registry = er.async_get(hass)
for group_id in groups_to_remove:
if entity_id := entity_registry.async_get_entity_id(
MEDIA_PLAYER_DOMAIN,
DOMAIN,
SnapcastGroupDevice.get_unique_id(host_id, group_id),
):
entity_registry.async_remove(entity_id)
for client_id in clients_to_remove:
if entity_id := entity_registry.async_get_entity_id(
MEDIA_PLAYER_DOMAIN,
DOMAIN,
SnapcastClientDevice.get_unique_id(host_id, client_id),
):
entity_registry.async_remove(entity_id)
coordinator.async_add_listener(_check_entities)
_check_entities()
async def handle_async_join(entity, service_call):
"""Handle the entity service join."""
if not isinstance(entity, SnapcastClientDevice):
raise TypeError("Entity is not a client. Can only join clients.")
await entity.async_join(service_call.data[ATTR_MASTER])
async def handle_async_unjoin(entity, service_call):
"""Handle the entity service unjoin."""
if not isinstance(entity, SnapcastClientDevice):
raise TypeError("Entity is not a client. Can only unjoin clients.")
await entity.async_unjoin()
async def handle_set_latency(entity, service_call):
"""Handle the entity service set_latency."""
if not isinstance(entity, SnapcastClientDevice):
raise TypeError("Latency can only be set for a Snapcast client.")
await entity.async_set_latency(service_call.data[ATTR_LATENCY])
class SnapcastGroupDevice(MediaPlayerEntity):
"""Representation of a Snapcast group device."""
class SnapcastBaseDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
"""Base class representing a Snapcast device."""
_attr_should_poll = False
_attr_supported_features = (
@ -114,166 +181,172 @@ class SnapcastGroupDevice(MediaPlayerEntity):
| MediaPlayerEntityFeature.SELECT_SOURCE
)
def __init__(self, group, uid_part, entry_id):
"""Initialize the Snapcast group device."""
self._attr_available = True
self._group = group
self._entry_id = entry_id
self._attr_unique_id = f"{GROUP_PREFIX}{uid_part}_{self._group.identifier}"
def __init__(
self,
coordinator: SnapcastUpdateCoordinator,
device: Snapgroup | Snapclient,
host_id: str,
) -> None:
"""Initialize the base device."""
super().__init__(coordinator)
self._device = device
self._attr_unique_id = self.get_unique_id(host_id, device.identifier)
@classmethod
def get_unique_id(cls, host, id) -> str:
"""Build a unique ID."""
raise NotImplementedError
@property
def _current_group(self) -> Snapgroup:
"""Return the group."""
raise NotImplementedError
async def async_added_to_hass(self) -> None:
"""Subscribe to group events."""
self._group.set_callback(self.schedule_update_ha_state)
self.hass.data[DOMAIN][self._entry_id].groups.append(self)
"""Subscribe to events."""
await super().async_added_to_hass()
self._device.set_callback(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect group object when removed."""
self._group.set_callback(None)
self.hass.data[DOMAIN][self._entry_id].groups.remove(self)
"""Disconnect object when removed."""
self._device.set_callback(None)
def set_availability(self, available: bool) -> None:
"""Set availability of group."""
self._attr_available = available
self.schedule_update_ha_state()
@property
def identifier(self) -> str:
"""Return the snapcast identifier."""
return self._device.identifier
@property
def source(self) -> str | None:
"""Return the current input source."""
return self._current_group.stream
@property
def source_list(self) -> list[str]:
"""List of available input sources."""
return list(self._current_group.streams_by_name().keys())
async def async_select_source(self, source: str) -> None:
"""Set input source."""
streams = self._current_group.streams_by_name()
if source in streams:
await self._current_group.set_stream(streams[source].identifier)
self.async_write_ha_state()
@property
def is_volume_muted(self) -> bool:
"""Volume muted."""
return self._device.muted
async def async_mute_volume(self, mute: bool) -> None:
"""Send the mute command."""
await self._device.set_muted(mute)
self.async_write_ha_state()
@property
def volume_level(self) -> float:
"""Return the volume level."""
return self._device.volume / 100
async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level."""
await self._device.set_volume(round(volume * 100))
self.async_write_ha_state()
def snapshot(self) -> None:
"""Snapshot the group state."""
self._device.snapshot()
async def async_restore(self) -> None:
"""Restore the group state."""
await self._device.restore()
self.async_write_ha_state()
async def async_set_latency(self, latency) -> None:
"""Handle the set_latency service."""
raise NotImplementedError
async def async_join(self, master) -> None:
"""Handle the join service."""
raise NotImplementedError
async def async_unjoin(self) -> None:
"""Handle the unjoin service."""
raise NotImplementedError
class SnapcastGroupDevice(SnapcastBaseDevice):
"""Representation of a Snapcast group device."""
_device: Snapgroup
@classmethod
def get_unique_id(cls, host, id) -> str:
"""Get a unique ID for a group."""
return f"{GROUP_PREFIX}{host}_{id}"
@property
def _current_group(self) -> Snapgroup:
"""Return the group."""
return self._device
@property
def name(self) -> str:
"""Return the name of the device."""
return f"{self._device.friendly_name} {GROUP_SUFFIX}"
@property
def state(self) -> MediaPlayerState | None:
"""Return the state of the player."""
if self.is_volume_muted:
return MediaPlayerState.IDLE
return STREAM_STATUS.get(self._group.stream_status)
return STREAM_STATUS.get(self._device.stream_status)
@property
def identifier(self):
"""Return the snapcast identifier."""
return self._group.identifier
async def async_set_latency(self, latency) -> None:
"""Handle the set_latency service."""
raise ServiceValidationError("Latency can only be set for a Snapcast client.")
@property
def name(self):
"""Return the name of the device."""
return f"{self._group.friendly_name} {GROUP_SUFFIX}"
async def async_join(self, master) -> None:
"""Handle the join service."""
raise ServiceValidationError("Entity is not a client. Can only join clients.")
@property
def source(self):
"""Return the current input source."""
return self._group.stream
@property
def volume_level(self):
"""Return the volume level."""
return self._group.volume / 100
@property
def is_volume_muted(self):
"""Volume muted."""
return self._group.muted
@property
def source_list(self):
"""List of available input sources."""
return list(self._group.streams_by_name().keys())
async def async_select_source(self, source: str) -> None:
"""Set input source."""
streams = self._group.streams_by_name()
if source in streams:
await self._group.set_stream(streams[source].identifier)
self.async_write_ha_state()
async def async_mute_volume(self, mute: bool) -> None:
"""Send the mute command."""
await self._group.set_muted(mute)
self.async_write_ha_state()
async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level."""
await self._group.set_volume(round(volume * 100))
self.async_write_ha_state()
def snapshot(self):
"""Snapshot the group state."""
self._group.snapshot()
async def async_restore(self):
"""Restore the group state."""
await self._group.restore()
self.async_write_ha_state()
async def async_unjoin(self) -> None:
"""Handle the unjoin service."""
raise ServiceValidationError("Entity is not a client. Can only unjoin clients.")
class SnapcastClientDevice(MediaPlayerEntity):
class SnapcastClientDevice(SnapcastBaseDevice):
"""Representation of a Snapcast client device."""
_attr_should_poll = False
_attr_supported_features = (
MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.SELECT_SOURCE
)
_device: Snapclient
def __init__(self, client, uid_part, entry_id):
"""Initialize the Snapcast client device."""
self._attr_available = True
self._client = client
# Note: Host part is needed, when using multiple snapservers
self._attr_unique_id = f"{CLIENT_PREFIX}{uid_part}_{self._client.identifier}"
self._entry_id = entry_id
async def async_added_to_hass(self) -> None:
"""Subscribe to client events."""
self._client.set_callback(self.schedule_update_ha_state)
self.hass.data[DOMAIN][self._entry_id].clients.append(self)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect client object when removed."""
self._client.set_callback(None)
self.hass.data[DOMAIN][self._entry_id].clients.remove(self)
def set_availability(self, available: bool) -> None:
"""Set availability of group."""
self._attr_available = available
self.schedule_update_ha_state()
@classmethod
def get_unique_id(cls, host, id) -> str:
"""Get a unique ID for a client."""
return f"{CLIENT_PREFIX}{host}_{id}"
@property
def identifier(self):
"""Return the snapcast identifier."""
return self._client.identifier
def _current_group(self) -> Snapgroup:
"""Return the group the client is associated with."""
return self._device.group
@property
def name(self):
def name(self) -> str:
"""Return the name of the device."""
return f"{self._client.friendly_name} {CLIENT_SUFFIX}"
@property
def source(self):
"""Return the current input source."""
return self._client.group.stream
@property
def volume_level(self):
"""Return the volume level."""
return self._client.volume / 100
@property
def is_volume_muted(self):
"""Volume muted."""
return self._client.muted
@property
def source_list(self):
"""List of available input sources."""
return list(self._client.group.streams_by_name().keys())
return f"{self._device.friendly_name} {CLIENT_SUFFIX}"
@property
def state(self) -> MediaPlayerState | None:
"""Return the state of the player."""
if self._client.connected:
if self.is_volume_muted or self._client.group.muted:
if self._device.connected:
if self.is_volume_muted or self._current_group.muted:
return MediaPlayerState.IDLE
return STREAM_STATUS.get(self._client.group.stream_status)
return STREAM_STATUS.get(self._current_group.stream_status)
return MediaPlayerState.STANDBY
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> Mapping[str, Any]:
"""Return the state attributes."""
state_attrs = {}
if self.latency is not None:
@ -281,60 +354,40 @@ class SnapcastClientDevice(MediaPlayerEntity):
return state_attrs
@property
def latency(self):
def latency(self) -> float | None:
"""Latency for Client."""
return self._client.latency
return self._device.latency
async def async_select_source(self, source: str) -> None:
"""Set input source."""
streams = self._client.group.streams_by_name()
if source in streams:
await self._client.group.set_stream(streams[source].identifier)
self.async_write_ha_state()
async def async_mute_volume(self, mute: bool) -> None:
"""Send the mute command."""
await self._client.set_muted(mute)
async def async_set_latency(self, latency) -> None:
"""Set the latency of the client."""
await self._device.set_latency(latency)
self.async_write_ha_state()
async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level."""
await self._client.set_volume(round(volume * 100))
self.async_write_ha_state()
async def async_join(self, master):
async def async_join(self, master) -> None:
"""Join the group of the master player."""
master_entity = next(
entity
for entity in self.hass.data[DOMAIN][self._entry_id].clients
if entity.entity_id == master
)
if not isinstance(master_entity, SnapcastClientDevice):
raise TypeError("Master is not a client device. Can only join clients.")
entity_registry = er.async_get(self.hass)
master_entity = entity_registry.async_get(master)
if master_entity is None:
raise ServiceValidationError(f"Master entity '{master}' not found.")
# Validate master entity is a client
unique_id = master_entity.unique_id
if not unique_id.startswith(CLIENT_PREFIX):
raise ServiceValidationError(
"Master is not a client device. Can only join clients."
)
# Extract the client ID and locate it's group
identifier = unique_id.split("_")[-1]
master_group = next(
group
for group in self._client.groups_available()
if master_entity.identifier in group.clients
for group in self._device.groups_available()
if identifier in group.clients
)
await master_group.add_client(self._client.identifier)
await master_group.add_client(self._device.identifier)
self.async_write_ha_state()
async def async_unjoin(self):
async def async_unjoin(self) -> None:
"""Unjoin the group the player is currently in."""
await self._client.group.remove_client(self._client.identifier)
self.async_write_ha_state()
def snapshot(self):
"""Snapshot the client state."""
self._client.snapshot()
async def async_restore(self):
"""Restore the client state."""
await self._client.restore()
self.async_write_ha_state()
async def async_set_latency(self, latency):
"""Set the latency of the client."""
await self._client.set_latency(latency)
await self._current_group.remove_client(self._device.identifier)
self.async_write_ha_state()

View File

@ -1,143 +0,0 @@
"""Snapcast Integration."""
from __future__ import annotations
import logging
import snapcast.control
from snapcast.control.client import Snapclient
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .media_player import SnapcastClientDevice, SnapcastGroupDevice
_LOGGER = logging.getLogger(__name__)
class HomeAssistantSnapcast:
"""Snapcast server and data stored in the Home Assistant data object."""
hass: HomeAssistant
def __init__(
self,
hass: HomeAssistant,
server: snapcast.control.Snapserver,
hpid: str,
entry_id: str,
) -> None:
"""Initialize the HomeAssistantSnapcast object.
Parameters
----------
hass: HomeAssistant
hass object
server : snapcast.control.Snapserver
Snapcast server
hpid : str
host and port
entry_id: str
ConfigEntry entry_id
Returns
-------
None
"""
self.hass: HomeAssistant = hass
self.server: snapcast.control.Snapserver = server
self.hpid: str = hpid
self._entry_id = entry_id
self.clients: list[SnapcastClientDevice] = []
self.groups: list[SnapcastGroupDevice] = []
self.hass_async_add_entities: AddEntitiesCallback
# connect callbacks
self.server.set_on_update_callback(self.on_update)
self.server.set_on_connect_callback(self.on_connect)
self.server.set_on_disconnect_callback(self.on_disconnect)
self.server.set_new_client_callback(self.on_add_client)
async def disconnect(self) -> None:
"""Disconnect from server."""
self.server.set_on_update_callback(None)
self.server.set_on_connect_callback(None)
self.server.set_on_disconnect_callback(None)
self.server.set_new_client_callback(None)
self.server.stop()
def on_update(self) -> None:
"""Update all entities.
Retrieve all groups/clients from server and add/update/delete entities.
"""
if not self.hass_async_add_entities:
return
new_groups: list[MediaPlayerEntity] = []
groups: list[MediaPlayerEntity] = []
hass_groups = {g.identifier: g for g in self.groups}
for group in self.server.groups:
if group.identifier in hass_groups:
groups.append(hass_groups[group.identifier])
hass_groups[group.identifier].async_schedule_update_ha_state()
else:
new_groups.append(SnapcastGroupDevice(group, self.hpid, self._entry_id))
new_clients: list[MediaPlayerEntity] = []
clients: list[MediaPlayerEntity] = []
hass_clients = {c.identifier: c for c in self.clients}
for client in self.server.clients:
if client.identifier in hass_clients:
clients.append(hass_clients[client.identifier])
hass_clients[client.identifier].async_schedule_update_ha_state()
else:
new_clients.append(
SnapcastClientDevice(client, self.hpid, self._entry_id)
)
del_entities: list[MediaPlayerEntity] = [
x for x in self.groups if x not in groups
]
del_entities.extend([x for x in self.clients if x not in clients])
_LOGGER.debug("New clients: %s", str([c.name for c in new_clients]))
_LOGGER.debug("New groups: %s", str([g.name for g in new_groups]))
_LOGGER.debug("Delete: %s", str(del_entities))
ent_reg = er.async_get(self.hass)
for entity in del_entities:
ent_reg.async_remove(entity.entity_id)
self.hass_async_add_entities(new_clients + new_groups)
def on_connect(self) -> None:
"""Activate all entities and update."""
for client in self.clients:
client.set_availability(True)
for group in self.groups:
group.set_availability(True)
_LOGGER.debug("Server connected: %s", self.hpid)
self.on_update()
def on_disconnect(self, ex: Exception | None) -> None:
"""Deactivate all entities."""
for client in self.clients:
client.set_availability(False)
for group in self.groups:
group.set_availability(False)
_LOGGER.warning(
"Server disconnected: %s. Trying to reconnect. %s", self.hpid, str(ex or "")
)
def on_add_client(self, client: Snapclient) -> None:
"""Add a Snapcast client.
Parameters
----------
client : Snapclient
Snapcast client to be added to HA.
"""
if not self.hass_async_add_entities:
return
clients = [SnapcastClientDevice(client, self.hpid, self._entry_id)]
self.hass_async_add_entities(clients)