Fix Snapcast connection issues (#93010)

* Add (dis)connect and update listeners, terminate connection and reconnect. Set availability

* Pass entry_id to constructor
pull/93443/head
luar123 2023-05-24 08:16:09 +02:00 committed by GitHub
parent a43dcaf812
commit 869f970e59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 173 additions and 18 deletions

View File

@ -27,7 +27,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"Could not connect to Snapcast server at {host}:{port}" f"Could not connect to Snapcast server at {host}:{port}"
) from ex ) from ex
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantSnapcast(server) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantSnapcast(
hass, server, f"{host}:{port}", entry.entry_id
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -37,5 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id) snapcast_data = hass.data[DOMAIN].pop(entry.entry_id)
# disconnect from server
await snapcast_data.disconnect()
return unload_ok return unload_ok

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
from snapcast.control.server import CONTROL_PORT from snapcast.control.server import CONTROL_PORT, Snapserver
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@ -34,7 +34,6 @@ from .const import (
SERVICE_SNAPSHOT, SERVICE_SNAPSHOT,
SERVICE_UNJOIN, SERVICE_UNJOIN,
) )
from .server import HomeAssistantSnapcast
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -72,7 +71,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the snapcast config entry.""" """Set up the snapcast config entry."""
snapcast_data: HomeAssistantSnapcast = hass.data[DOMAIN][config_entry.entry_id] snapcast_server: Snapserver = hass.data[DOMAIN][config_entry.entry_id].server
register_services() register_services()
@ -80,14 +79,18 @@ async def async_setup_entry(
port = config_entry.data[CONF_PORT] port = config_entry.data[CONF_PORT]
hpid = f"{host}:{port}" hpid = f"{host}:{port}"
snapcast_data.groups = [ groups: list[MediaPlayerEntity] = [
SnapcastGroupDevice(group, hpid) for group in snapcast_data.server.groups SnapcastGroupDevice(group, hpid, config_entry.entry_id)
for group in snapcast_server.groups
] ]
snapcast_data.clients = [ clients: list[MediaPlayerEntity] = [
SnapcastClientDevice(client, hpid, config_entry.entry_id) SnapcastClientDevice(client, hpid, config_entry.entry_id)
for client in snapcast_data.server.clients for client in snapcast_server.clients
] ]
async_add_entities(snapcast_data.clients + snapcast_data.groups) async_add_entities(clients + groups)
hass.data[DOMAIN][
config_entry.entry_id
].hass_async_add_entities = async_add_entities
async def async_setup_platform( async def async_setup_platform(
@ -147,18 +150,27 @@ class SnapcastGroupDevice(MediaPlayerEntity):
| MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOURCE
) )
def __init__(self, group, uid_part): def __init__(self, group, uid_part, entry_id):
"""Initialize the Snapcast group device.""" """Initialize the Snapcast group device."""
self._attr_available = True
self._group = group self._group = group
self._entry_id = entry_id
self._uid = f"{GROUP_PREFIX}{uid_part}_{self._group.identifier}" self._uid = f"{GROUP_PREFIX}{uid_part}_{self._group.identifier}"
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to group events.""" """Subscribe to group events."""
self._group.set_callback(self.schedule_update_ha_state) self._group.set_callback(self.schedule_update_ha_state)
self.hass.data[DOMAIN][self._entry_id].groups.append(self)
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Disconnect group object when removed.""" """Disconnect group object when removed."""
self._group.set_callback(None) self._group.set_callback(None)
self.hass.data[DOMAIN][self._entry_id].groups.remove(self)
def set_availability(self, available: bool) -> None:
"""Set availability of group."""
self._attr_available = available
self.schedule_update_ha_state()
@property @property
def state(self) -> MediaPlayerState | None: def state(self) -> MediaPlayerState | None:
@ -172,6 +184,11 @@ class SnapcastGroupDevice(MediaPlayerEntity):
"""Return the ID of snapcast group.""" """Return the ID of snapcast group."""
return self._uid return self._uid
@property
def identifier(self):
"""Return the snapcast identifier."""
return self._group.identifier
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
@ -236,6 +253,7 @@ class SnapcastClientDevice(MediaPlayerEntity):
def __init__(self, client, uid_part, entry_id): def __init__(self, client, uid_part, entry_id):
"""Initialize the Snapcast client device.""" """Initialize the Snapcast client device."""
self._attr_available = True
self._client = client self._client = client
self._uid = f"{CLIENT_PREFIX}{uid_part}_{self._client.identifier}" self._uid = f"{CLIENT_PREFIX}{uid_part}_{self._client.identifier}"
self._entry_id = entry_id self._entry_id = entry_id
@ -243,10 +261,17 @@ class SnapcastClientDevice(MediaPlayerEntity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to client events.""" """Subscribe to client events."""
self._client.set_callback(self.schedule_update_ha_state) 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: async def async_will_remove_from_hass(self) -> None:
"""Disconnect client object when removed.""" """Disconnect client object when removed."""
self._client.set_callback(None) 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()
@property @property
def unique_id(self): def unique_id(self):

View File

@ -1,15 +1,141 @@
"""Snapcast Integration.""" """Snapcast Integration."""
from dataclasses import dataclass, field from __future__ import annotations
from snapcast.control import Snapserver import logging
import snapcast.control
from snapcast.control.client import Snapclient
from homeassistant.components.media_player import MediaPlayerEntity 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__)
@dataclass
class HomeAssistantSnapcast: class HomeAssistantSnapcast:
"""Snapcast data stored in the Home Assistant data object.""" """Snapcast server and data stored in the Home Assistant data object."""
server: Snapserver hass: HomeAssistant
clients: list[MediaPlayerEntity] = field(default_factory=list)
groups: list[MediaPlayerEntity] = field(default_factory=list) 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)
await 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(new_clients))
_LOGGER.debug("New groups: %s", str(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.info("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)