mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Update Sonos to use SonosConfigEntry and runtime data (#145512)
* fix: initial * fix: cleanup * fix: cleanup * fix: cleanup * fix: SonosConfigEntry * add config_entry.py * fix: sonos_data to runtime_data * fix: move to helpers.py
This commit is contained in:
parent
78ed1097c4
commit
afc0a2789d
@ -3,8 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import OrderedDict
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
import datetime
|
import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from ipaddress import AddressValueError, IPv4Address
|
from ipaddress import AddressValueError, IPv4Address
|
||||||
@ -25,9 +23,8 @@ import voluptuous as vol
|
|||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import ssdp
|
from homeassistant.components import ssdp
|
||||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
config_validation as cv,
|
config_validation as cv,
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
@ -46,7 +43,6 @@ from homeassistant.util.async_ import create_eager_task
|
|||||||
from .alarms import SonosAlarms
|
from .alarms import SonosAlarms
|
||||||
from .const import (
|
from .const import (
|
||||||
AVAILABILITY_CHECK_INTERVAL,
|
AVAILABILITY_CHECK_INTERVAL,
|
||||||
DATA_SONOS,
|
|
||||||
DATA_SONOS_DISCOVERY_MANAGER,
|
DATA_SONOS_DISCOVERY_MANAGER,
|
||||||
DISCOVERY_INTERVAL,
|
DISCOVERY_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -62,7 +58,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .exception import SonosUpdateError
|
from .exception import SonosUpdateError
|
||||||
from .favorites import SonosFavorites
|
from .favorites import SonosFavorites
|
||||||
from .helpers import sync_get_visible_zones
|
from .helpers import SonosConfigEntry, SonosData, sync_get_visible_zones
|
||||||
from .speaker import SonosSpeaker
|
from .speaker import SonosSpeaker
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -95,32 +91,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UnjoinData:
|
|
||||||
"""Class to track data necessary for unjoin coalescing."""
|
|
||||||
|
|
||||||
speakers: list[SonosSpeaker]
|
|
||||||
event: asyncio.Event = field(default_factory=asyncio.Event)
|
|
||||||
|
|
||||||
|
|
||||||
class SonosData:
|
|
||||||
"""Storage class for platform global data."""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Initialize the data."""
|
|
||||||
# OrderedDict behavior used by SonosAlarms and SonosFavorites
|
|
||||||
self.discovered: OrderedDict[str, SonosSpeaker] = OrderedDict()
|
|
||||||
self.favorites: dict[str, SonosFavorites] = {}
|
|
||||||
self.alarms: dict[str, SonosAlarms] = {}
|
|
||||||
self.topology_condition = asyncio.Condition()
|
|
||||||
self.hosts_heartbeat: CALLBACK_TYPE | None = None
|
|
||||||
self.discovery_known: set[str] = set()
|
|
||||||
self.boot_counts: dict[str, int] = {}
|
|
||||||
self.mdns_names: dict[str, str] = {}
|
|
||||||
self.entity_id_mappings: dict[str, SonosSpeaker] = {}
|
|
||||||
self.unjoin_data: dict[str, UnjoinData] = {}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the Sonos component."""
|
"""Set up the Sonos component."""
|
||||||
conf = config.get(DOMAIN)
|
conf = config.get(DOMAIN)
|
||||||
@ -137,17 +107,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: SonosConfigEntry) -> bool:
|
||||||
"""Set up Sonos from a config entry."""
|
"""Set up Sonos from a config entry."""
|
||||||
soco_config.EVENTS_MODULE = events_asyncio
|
soco_config.EVENTS_MODULE = events_asyncio
|
||||||
soco_config.REQUEST_TIMEOUT = 9.5
|
soco_config.REQUEST_TIMEOUT = 9.5
|
||||||
soco_config.ZGT_EVENT_FALLBACK = False
|
soco_config.ZGT_EVENT_FALLBACK = False
|
||||||
zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT
|
zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT
|
||||||
|
|
||||||
if DATA_SONOS not in hass.data:
|
data = entry.runtime_data = SonosData()
|
||||||
hass.data[DATA_SONOS] = SonosData()
|
|
||||||
|
|
||||||
data = hass.data[DATA_SONOS]
|
|
||||||
config = hass.data[DOMAIN].get("media_player", {})
|
config = hass.data[DOMAIN].get("media_player", {})
|
||||||
hosts = config.get(CONF_HOSTS, [])
|
hosts = config.get(CONF_HOSTS, [])
|
||||||
_LOGGER.debug("Reached async_setup_entry, config=%s", config)
|
_LOGGER.debug("Reached async_setup_entry, config=%s", config)
|
||||||
@ -172,12 +140,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, config_entry: SonosConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a Sonos config entry."""
|
"""Unload a Sonos config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||||
|
config_entry, PLATFORMS
|
||||||
|
)
|
||||||
await hass.data[DATA_SONOS_DISCOVERY_MANAGER].async_shutdown()
|
await hass.data[DATA_SONOS_DISCOVERY_MANAGER].async_shutdown()
|
||||||
hass.data.pop(DATA_SONOS)
|
|
||||||
hass.data.pop(DATA_SONOS_DISCOVERY_MANAGER)
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
@ -185,7 +155,11 @@ class SonosDiscoveryManager:
|
|||||||
"""Manage sonos discovery."""
|
"""Manage sonos discovery."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, entry: ConfigEntry, data: SonosData, hosts: list[str]
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SonosConfigEntry,
|
||||||
|
data: SonosData,
|
||||||
|
hosts: list[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init discovery manager."""
|
"""Init discovery manager."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -380,7 +354,9 @@ class SonosDiscoveryManager:
|
|||||||
if soco.uid not in self.data.boot_counts:
|
if soco.uid not in self.data.boot_counts:
|
||||||
self.data.boot_counts[soco.uid] = soco.boot_seqnum
|
self.data.boot_counts[soco.uid] = soco.boot_seqnum
|
||||||
_LOGGER.debug("Adding new speaker: %s", speaker_info)
|
_LOGGER.debug("Adding new speaker: %s", speaker_info)
|
||||||
speaker = SonosSpeaker(self.hass, soco, speaker_info, zone_group_state_sub)
|
speaker = SonosSpeaker(
|
||||||
|
self.hass, self.entry, soco, speaker_info, zone_group_state_sub
|
||||||
|
)
|
||||||
self.data.discovered[soco.uid] = speaker
|
self.data.discovered[soco.uid] = speaker
|
||||||
for coordinator, coord_dict in (
|
for coordinator, coord_dict in (
|
||||||
(SonosAlarms, self.data.alarms),
|
(SonosAlarms, self.data.alarms),
|
||||||
@ -388,7 +364,9 @@ class SonosDiscoveryManager:
|
|||||||
):
|
):
|
||||||
c_dict: dict[str, Any] = coord_dict
|
c_dict: dict[str, Any] = coord_dict
|
||||||
if soco.household_id not in c_dict:
|
if soco.household_id not in c_dict:
|
||||||
new_coordinator = coordinator(self.hass, soco.household_id)
|
new_coordinator = coordinator(
|
||||||
|
self.hass, soco.household_id, self.entry
|
||||||
|
)
|
||||||
new_coordinator.setup(soco)
|
new_coordinator.setup(soco)
|
||||||
c_dict[soco.household_id] = new_coordinator
|
c_dict[soco.household_id] = new_coordinator
|
||||||
speaker.setup(self.entry)
|
speaker.setup(self.entry)
|
||||||
@ -622,10 +600,10 @@ class SonosDiscoveryManager:
|
|||||||
|
|
||||||
|
|
||||||
async def async_remove_config_entry_device(
|
async def async_remove_config_entry_device(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
|
hass: HomeAssistant, config_entry: SonosConfigEntry, device_entry: dr.DeviceEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Remove Sonos config entry from a device."""
|
"""Remove Sonos config entry from a device."""
|
||||||
known_devices = hass.data[DATA_SONOS].discovered.keys()
|
known_devices = config_entry.runtime_data.discovered.keys()
|
||||||
for identifier in device_entry.identifiers:
|
for identifier in device_entry.identifiers:
|
||||||
if identifier[0] != DOMAIN:
|
if identifier[0] != DOMAIN:
|
||||||
continue
|
continue
|
||||||
|
@ -12,7 +12,7 @@ from soco.events_base import Event as SonosEvent
|
|||||||
|
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
from .const import DATA_SONOS, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM
|
from .const import SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM
|
||||||
from .helpers import soco_error
|
from .helpers import soco_error
|
||||||
from .household_coordinator import SonosHouseholdCoordinator
|
from .household_coordinator import SonosHouseholdCoordinator
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ class SonosAlarms(SonosHouseholdCoordinator):
|
|||||||
for alarm_id, alarm in self.alarms.alarms.items():
|
for alarm_id, alarm in self.alarms.alarms.items():
|
||||||
if alarm_id in self.created_alarm_ids:
|
if alarm_id in self.created_alarm_ids:
|
||||||
continue
|
continue
|
||||||
speaker = self.hass.data[DATA_SONOS].discovered.get(alarm.zone.uid)
|
speaker = self.config_entry.runtime_data.discovered.get(alarm.zone.uid)
|
||||||
if speaker:
|
if speaker:
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
self.hass, SONOS_CREATE_ALARM, speaker, [alarm_id]
|
self.hass, SONOS_CREATE_ALARM, speaker, [alarm_id]
|
||||||
|
@ -9,7 +9,6 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
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.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
@ -17,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
|
|
||||||
from .const import SONOS_CREATE_BATTERY, SONOS_CREATE_MIC_SENSOR
|
from .const import SONOS_CREATE_BATTERY, SONOS_CREATE_MIC_SENSOR
|
||||||
from .entity import SonosEntity
|
from .entity import SonosEntity
|
||||||
from .helpers import soco_error
|
from .helpers import SonosConfigEntry, soco_error
|
||||||
from .speaker import SonosSpeaker
|
from .speaker import SonosSpeaker
|
||||||
|
|
||||||
ATTR_BATTERY_POWER_SOURCE = "power_source"
|
ATTR_BATTERY_POWER_SOURCE = "power_source"
|
||||||
@ -27,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: SonosConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Sonos from a config entry."""
|
"""Set up Sonos from a config entry."""
|
||||||
@ -35,13 +34,13 @@ async def async_setup_entry(
|
|||||||
@callback
|
@callback
|
||||||
def _async_create_battery_entity(speaker: SonosSpeaker) -> None:
|
def _async_create_battery_entity(speaker: SonosSpeaker) -> None:
|
||||||
_LOGGER.debug("Creating battery binary_sensor on %s", speaker.zone_name)
|
_LOGGER.debug("Creating battery binary_sensor on %s", speaker.zone_name)
|
||||||
entity = SonosPowerEntity(speaker)
|
entity = SonosPowerEntity(speaker, config_entry)
|
||||||
async_add_entities([entity])
|
async_add_entities([entity])
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_create_mic_entity(speaker: SonosSpeaker) -> None:
|
def _async_create_mic_entity(speaker: SonosSpeaker) -> None:
|
||||||
_LOGGER.debug("Creating microphone binary_sensor on %s", speaker.zone_name)
|
_LOGGER.debug("Creating microphone binary_sensor on %s", speaker.zone_name)
|
||||||
async_add_entities([SonosMicrophoneSensorEntity(speaker)])
|
async_add_entities([SonosMicrophoneSensorEntity(speaker, config_entry)])
|
||||||
|
|
||||||
config_entry.async_on_unload(
|
config_entry.async_on_unload(
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
@ -62,9 +61,9 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity):
|
|||||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
|
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
|
||||||
|
|
||||||
def __init__(self, speaker: SonosSpeaker) -> None:
|
def __init__(self, speaker: SonosSpeaker, config_entry: SonosConfigEntry) -> None:
|
||||||
"""Initialize the power entity binary sensor."""
|
"""Initialize the power entity binary sensor."""
|
||||||
super().__init__(speaker)
|
super().__init__(speaker, config_entry)
|
||||||
self._attr_unique_id = f"{self.soco.uid}-power"
|
self._attr_unique_id = f"{self.soco.uid}-power"
|
||||||
|
|
||||||
async def _async_fallback_poll(self) -> None:
|
async def _async_fallback_poll(self) -> None:
|
||||||
@ -95,9 +94,9 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity):
|
|||||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
_attr_translation_key = "microphone"
|
_attr_translation_key = "microphone"
|
||||||
|
|
||||||
def __init__(self, speaker: SonosSpeaker) -> None:
|
def __init__(self, speaker: SonosSpeaker, config_entry: SonosConfigEntry) -> None:
|
||||||
"""Initialize the microphone binary sensor entity."""
|
"""Initialize the microphone binary sensor entity."""
|
||||||
super().__init__(speaker)
|
super().__init__(speaker, config_entry)
|
||||||
self._attr_unique_id = f"{self.soco.uid}-microphone"
|
self._attr_unique_id = f"{self.soco.uid}-microphone"
|
||||||
|
|
||||||
async def _async_fallback_poll(self) -> None:
|
async def _async_fallback_poll(self) -> None:
|
||||||
|
@ -10,7 +10,6 @@ from homeassistant.const import Platform
|
|||||||
UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1"
|
UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||||
|
|
||||||
DOMAIN = "sonos"
|
DOMAIN = "sonos"
|
||||||
DATA_SONOS = "sonos_media_player"
|
|
||||||
DATA_SONOS_DISCOVERY_MANAGER = "sonos_discovery_manager"
|
DATA_SONOS_DISCOVERY_MANAGER = "sonos_discovery_manager"
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
|
@ -5,11 +5,11 @@ from __future__ import annotations
|
|||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceEntry
|
from homeassistant.helpers.device_registry import DeviceEntry
|
||||||
|
|
||||||
from .const import DATA_SONOS, DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .helpers import SonosConfigEntry
|
||||||
from .speaker import SonosSpeaker
|
from .speaker import SonosSpeaker
|
||||||
|
|
||||||
MEDIA_DIAGNOSTIC_ATTRIBUTES = (
|
MEDIA_DIAGNOSTIC_ATTRIBUTES = (
|
||||||
@ -45,27 +45,29 @@ SPEAKER_DIAGNOSTIC_ATTRIBUTES = (
|
|||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry
|
hass: HomeAssistant, config_entry: SonosConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
payload: dict[str, Any] = {"current_timestamp": time.monotonic()}
|
payload: dict[str, Any] = {"current_timestamp": time.monotonic()}
|
||||||
|
|
||||||
for section in ("discovered", "discovery_known"):
|
for section in ("discovered", "discovery_known"):
|
||||||
payload[section] = {}
|
payload[section] = {}
|
||||||
data: set[Any] | dict[str, Any] = getattr(hass.data[DATA_SONOS], section)
|
data: set[Any] | dict[str, Any] = getattr(config_entry.runtime_data, section)
|
||||||
if isinstance(data, set):
|
if isinstance(data, set):
|
||||||
payload[section] = data
|
payload[section] = data
|
||||||
continue
|
continue
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if isinstance(value, SonosSpeaker):
|
if isinstance(value, SonosSpeaker):
|
||||||
payload[section][key] = await async_generate_speaker_info(hass, value)
|
payload[section][key] = await async_generate_speaker_info(
|
||||||
|
hass, config_entry, value
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
payload[section][key] = value
|
payload[section][key] = value
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
async def async_get_device_diagnostics(
|
async def async_get_device_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
|
hass: HomeAssistant, config_entry: SonosConfigEntry, device: DeviceEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a device."""
|
"""Return diagnostics for a device."""
|
||||||
uid = next(
|
uid = next(
|
||||||
@ -75,10 +77,10 @@ async def async_get_device_diagnostics(
|
|||||||
if uid is None:
|
if uid is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
if (speaker := hass.data[DATA_SONOS].discovered.get(uid)) is None:
|
if (speaker := config_entry.runtime_data.discovered.get(uid)) is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
return await async_generate_speaker_info(hass, speaker)
|
return await async_generate_speaker_info(hass, config_entry, speaker)
|
||||||
|
|
||||||
|
|
||||||
async def async_generate_media_info(
|
async def async_generate_media_info(
|
||||||
@ -107,7 +109,7 @@ async def async_generate_media_info(
|
|||||||
|
|
||||||
|
|
||||||
async def async_generate_speaker_info(
|
async def async_generate_speaker_info(
|
||||||
hass: HomeAssistant, speaker: SonosSpeaker
|
hass: HomeAssistant, config_entry: SonosConfigEntry, speaker: SonosSpeaker
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Generate the diagnostic payload for a specific speaker."""
|
"""Generate the diagnostic payload for a specific speaker."""
|
||||||
payload: dict[str, Any] = {}
|
payload: dict[str, Any] = {}
|
||||||
@ -132,7 +134,7 @@ async def async_generate_speaker_info(
|
|||||||
|
|
||||||
payload["enabled_entities"] = sorted(
|
payload["enabled_entities"] = sorted(
|
||||||
entity_id
|
entity_id
|
||||||
for entity_id, s in hass.data[DATA_SONOS].entity_id_mappings.items()
|
for entity_id, s in config_entry.runtime_data.entity_id_mappings.items()
|
||||||
if s is speaker
|
if s is speaker
|
||||||
)
|
)
|
||||||
payload["media"] = await async_generate_media_info(hass, speaker)
|
payload["media"] = await async_generate_media_info(hass, speaker)
|
||||||
|
@ -13,8 +13,9 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import DATA_SONOS, DOMAIN, SONOS_FALLBACK_POLL, SONOS_STATE_UPDATED
|
from .const import DOMAIN, SONOS_FALLBACK_POLL, SONOS_STATE_UPDATED
|
||||||
from .exception import SonosUpdateError
|
from .exception import SonosUpdateError
|
||||||
|
from .helpers import SonosConfigEntry
|
||||||
from .speaker import SonosSpeaker
|
from .speaker import SonosSpeaker
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -26,13 +27,14 @@ class SonosEntity(Entity):
|
|||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(self, speaker: SonosSpeaker) -> None:
|
def __init__(self, speaker: SonosSpeaker, config_entry: SonosConfigEntry) -> None:
|
||||||
"""Initialize a SonosEntity."""
|
"""Initialize a SonosEntity."""
|
||||||
self.speaker = speaker
|
self.speaker = speaker
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Handle common setup when added to hass."""
|
"""Handle common setup when added to hass."""
|
||||||
self.hass.data[DATA_SONOS].entity_id_mappings[self.entity_id] = self.speaker
|
self.config_entry.runtime_data.entity_id_mappings[self.entity_id] = self.speaker
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass,
|
self.hass,
|
||||||
@ -50,7 +52,7 @@ class SonosEntity(Entity):
|
|||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Clean up when entity is removed."""
|
"""Clean up when entity is removed."""
|
||||||
del self.hass.data[DATA_SONOS].entity_id_mappings[self.entity_id]
|
del self.config_entry.runtime_data.entity_id_mappings[self.entity_id]
|
||||||
|
|
||||||
async def async_fallback_poll(self, now: datetime.datetime) -> None:
|
async def async_fallback_poll(self, now: datetime.datetime) -> None:
|
||||||
"""Poll the entity if subscriptions fail."""
|
"""Poll the entity if subscriptions fail."""
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections import OrderedDict
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass, field
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Concatenate, overload
|
from typing import TYPE_CHECKING, Any, Concatenate, overload
|
||||||
|
|
||||||
@ -10,13 +13,17 @@ from requests.exceptions import Timeout
|
|||||||
from soco import SoCo
|
from soco import SoCo
|
||||||
from soco.exceptions import SoCoException, SoCoUPnPException
|
from soco.exceptions import SoCoException, SoCoUPnPException
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import CALLBACK_TYPE
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
|
|
||||||
from .const import SONOS_SPEAKER_ACTIVITY
|
from .const import SONOS_SPEAKER_ACTIVITY
|
||||||
from .exception import SonosUpdateError
|
from .exception import SonosUpdateError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .alarms import SonosAlarms
|
||||||
from .entity import SonosEntity
|
from .entity import SonosEntity
|
||||||
|
from .favorites import SonosFavorites
|
||||||
from .household_coordinator import SonosHouseholdCoordinator
|
from .household_coordinator import SonosHouseholdCoordinator
|
||||||
from .media import SonosMedia
|
from .media import SonosMedia
|
||||||
from .speaker import SonosSpeaker
|
from .speaker import SonosSpeaker
|
||||||
@ -120,3 +127,30 @@ def sync_get_visible_zones(soco: SoCo) -> set[SoCo]:
|
|||||||
_ = soco.household_id
|
_ = soco.household_id
|
||||||
_ = soco.uid
|
_ = soco.uid
|
||||||
return soco.visible_zones
|
return soco.visible_zones
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UnjoinData:
|
||||||
|
"""Class to track data necessary for unjoin coalescing."""
|
||||||
|
|
||||||
|
speakers: list[SonosSpeaker] = field(default_factory=list)
|
||||||
|
event: asyncio.Event = field(default_factory=asyncio.Event)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SonosData:
|
||||||
|
"""Storage class for platform global data."""
|
||||||
|
|
||||||
|
discovered: OrderedDict[str, SonosSpeaker] = field(default_factory=OrderedDict)
|
||||||
|
favorites: dict[str, SonosFavorites] = field(default_factory=dict)
|
||||||
|
alarms: dict[str, SonosAlarms] = field(default_factory=dict)
|
||||||
|
topology_condition: asyncio.Condition = field(default_factory=asyncio.Condition)
|
||||||
|
hosts_heartbeat: CALLBACK_TYPE | None = None
|
||||||
|
discovery_known: set[str] = field(default_factory=set)
|
||||||
|
boot_counts: dict[str, int] = field(default_factory=dict)
|
||||||
|
mdns_names: dict[str, str] = field(default_factory=dict)
|
||||||
|
entity_id_mappings: dict[str, SonosSpeaker] = field(default_factory=dict)
|
||||||
|
unjoin_data: dict[str, UnjoinData] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
type SonosConfigEntry = ConfigEntry[SonosData]
|
||||||
|
@ -5,16 +5,18 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable, Coroutine
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from soco import SoCo
|
from soco import SoCo
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
|
|
||||||
from .const import DATA_SONOS
|
|
||||||
from .exception import SonosUpdateError
|
from .exception import SonosUpdateError
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .helpers import SonosConfigEntry
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -23,12 +25,15 @@ class SonosHouseholdCoordinator:
|
|||||||
|
|
||||||
cache_update_lock: asyncio.Lock
|
cache_update_lock: asyncio.Lock
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, household_id: str) -> None:
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, household_id: str, config_entry: SonosConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Initialize the data."""
|
"""Initialize the data."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.household_id = household_id
|
self.household_id = household_id
|
||||||
self.async_poll: Callable[[], Coroutine[None, None, None]] | None = None
|
self.async_poll: Callable[[], Coroutine[None, None, None]] | None = None
|
||||||
self.last_processed_event_id: int | None = None
|
self.last_processed_event_id: int | None = None
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
def setup(self, soco: SoCo) -> None:
|
def setup(self, soco: SoCo) -> None:
|
||||||
"""Set up the SonosAlarm instance."""
|
"""Set up the SonosAlarm instance."""
|
||||||
@ -54,7 +59,7 @@ class SonosHouseholdCoordinator:
|
|||||||
|
|
||||||
async def _async_poll(self) -> None:
|
async def _async_poll(self) -> None:
|
||||||
"""Poll any known speaker."""
|
"""Poll any known speaker."""
|
||||||
discovered = self.hass.data[DATA_SONOS].discovered
|
discovered = self.config_entry.runtime_data.discovered
|
||||||
|
|
||||||
for uid, speaker in discovered.items():
|
for uid, speaker in discovered.items():
|
||||||
_LOGGER.debug("Polling %s using %s", self.class_type, speaker.soco)
|
_LOGGER.debug("Polling %s using %s", self.class_type, speaker.soco)
|
||||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from soco import SoCo, alarms
|
from soco import SoCo, alarms
|
||||||
from soco.core import (
|
from soco.core import (
|
||||||
@ -40,7 +40,6 @@ from homeassistant.components.media_player import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.plex import PLEX_URI_SCHEME
|
from homeassistant.components.plex import PLEX_URI_SCHEME
|
||||||
from homeassistant.components.plex.services import process_plex_payload
|
from homeassistant.components.plex.services import process_plex_payload
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import ATTR_TIME
|
from homeassistant.const import ATTR_TIME
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
|
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
@ -49,9 +48,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
|
|
||||||
from . import UnjoinData, media_browser
|
from . import media_browser
|
||||||
from .const import (
|
from .const import (
|
||||||
DATA_SONOS,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MEDIA_TYPE_DIRECTORY,
|
MEDIA_TYPE_DIRECTORY,
|
||||||
MEDIA_TYPES_TO_SONOS,
|
MEDIA_TYPES_TO_SONOS,
|
||||||
@ -67,9 +65,12 @@ from .const import (
|
|||||||
SOURCE_TV,
|
SOURCE_TV,
|
||||||
)
|
)
|
||||||
from .entity import SonosEntity
|
from .entity import SonosEntity
|
||||||
from .helpers import soco_error
|
from .helpers import UnjoinData, soco_error
|
||||||
from .speaker import SonosMedia, SonosSpeaker
|
from .speaker import SonosMedia, SonosSpeaker
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .helpers import SonosConfigEntry
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
LONG_SERVICE_TIMEOUT = 30.0
|
LONG_SERVICE_TIMEOUT = 30.0
|
||||||
@ -108,7 +109,7 @@ ATTR_QUEUE_POSITION = "queue_position"
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: SonosConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Sonos from a config entry."""
|
"""Set up Sonos from a config entry."""
|
||||||
@ -118,7 +119,7 @@ async def async_setup_entry(
|
|||||||
def async_create_entities(speaker: SonosSpeaker) -> None:
|
def async_create_entities(speaker: SonosSpeaker) -> None:
|
||||||
"""Handle device discovery and create entities."""
|
"""Handle device discovery and create entities."""
|
||||||
_LOGGER.debug("Creating media_player on %s", speaker.zone_name)
|
_LOGGER.debug("Creating media_player on %s", speaker.zone_name)
|
||||||
async_add_entities([SonosMediaPlayerEntity(speaker)])
|
async_add_entities([SonosMediaPlayerEntity(speaker, config_entry)])
|
||||||
|
|
||||||
@service.verify_domain_control(hass, DOMAIN)
|
@service.verify_domain_control(hass, DOMAIN)
|
||||||
async def async_service_handle(service_call: ServiceCall) -> None:
|
async def async_service_handle(service_call: ServiceCall) -> None:
|
||||||
@ -136,11 +137,11 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
if service_call.service == SERVICE_SNAPSHOT:
|
if service_call.service == SERVICE_SNAPSHOT:
|
||||||
await SonosSpeaker.snapshot_multi(
|
await SonosSpeaker.snapshot_multi(
|
||||||
hass, speakers, service_call.data[ATTR_WITH_GROUP]
|
hass, config_entry, speakers, service_call.data[ATTR_WITH_GROUP]
|
||||||
)
|
)
|
||||||
elif service_call.service == SERVICE_RESTORE:
|
elif service_call.service == SERVICE_RESTORE:
|
||||||
await SonosSpeaker.restore_multi(
|
await SonosSpeaker.restore_multi(
|
||||||
hass, speakers, service_call.data[ATTR_WITH_GROUP]
|
hass, config_entry, speakers, service_call.data[ATTR_WITH_GROUP]
|
||||||
)
|
)
|
||||||
|
|
||||||
config_entry.async_on_unload(
|
config_entry.async_on_unload(
|
||||||
@ -231,9 +232,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
|||||||
_attr_media_content_type = MediaType.MUSIC
|
_attr_media_content_type = MediaType.MUSIC
|
||||||
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||||
|
|
||||||
def __init__(self, speaker: SonosSpeaker) -> None:
|
def __init__(self, speaker: SonosSpeaker, config_entry: SonosConfigEntry) -> None:
|
||||||
"""Initialize the media player entity."""
|
"""Initialize the media player entity."""
|
||||||
super().__init__(speaker)
|
super().__init__(speaker, config_entry)
|
||||||
self._attr_unique_id = self.soco.uid
|
self._attr_unique_id = self.soco.uid
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
@ -298,9 +299,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
|||||||
|
|
||||||
async def _async_fallback_poll(self) -> None:
|
async def _async_fallback_poll(self) -> None:
|
||||||
"""Retrieve latest state by polling."""
|
"""Retrieve latest state by polling."""
|
||||||
await (
|
favorites = self.config_entry.runtime_data.favorites[self.speaker.household_id]
|
||||||
self.hass.data[DATA_SONOS].favorites[self.speaker.household_id].async_poll()
|
assert favorites.async_poll
|
||||||
)
|
await favorites.async_poll()
|
||||||
await self.hass.async_add_executor_job(self._update)
|
await self.hass.async_add_executor_job(self._update)
|
||||||
|
|
||||||
def _update(self) -> None:
|
def _update(self) -> None:
|
||||||
@ -880,12 +881,16 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
|||||||
"""Join `group_members` as a player group with the current player."""
|
"""Join `group_members` as a player group with the current player."""
|
||||||
speakers = []
|
speakers = []
|
||||||
for entity_id in group_members:
|
for entity_id in group_members:
|
||||||
if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get(entity_id):
|
if speaker := self.config_entry.runtime_data.entity_id_mappings.get(
|
||||||
|
entity_id
|
||||||
|
):
|
||||||
speakers.append(speaker)
|
speakers.append(speaker)
|
||||||
else:
|
else:
|
||||||
raise HomeAssistantError(f"Not a known Sonos entity_id: {entity_id}")
|
raise HomeAssistantError(f"Not a known Sonos entity_id: {entity_id}")
|
||||||
|
|
||||||
await SonosSpeaker.join_multi(self.hass, self.speaker, speakers)
|
await SonosSpeaker.join_multi(
|
||||||
|
self.hass, self.config_entry, self.speaker, speakers
|
||||||
|
)
|
||||||
|
|
||||||
async def async_unjoin_player(self) -> None:
|
async def async_unjoin_player(self) -> None:
|
||||||
"""Remove this player from any group.
|
"""Remove this player from any group.
|
||||||
@ -894,7 +899,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
|||||||
which optimizes the order in which speakers are removed from their groups.
|
which optimizes the order in which speakers are removed from their groups.
|
||||||
Removing coordinators last better preserves playqueues on the speakers.
|
Removing coordinators last better preserves playqueues on the speakers.
|
||||||
"""
|
"""
|
||||||
sonos_data = self.hass.data[DATA_SONOS]
|
sonos_data = self.config_entry.runtime_data
|
||||||
household_id = self.speaker.household_id
|
household_id = self.speaker.household_id
|
||||||
|
|
||||||
async def async_process_unjoin(now: datetime.datetime) -> None:
|
async def async_process_unjoin(now: datetime.datetime) -> None:
|
||||||
@ -903,7 +908,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Processing unjoins for %s", [x.zone_name for x in unjoin_data.speakers]
|
"Processing unjoins for %s", [x.zone_name for x in unjoin_data.speakers]
|
||||||
)
|
)
|
||||||
await SonosSpeaker.unjoin_multi(self.hass, unjoin_data.speakers)
|
await SonosSpeaker.unjoin_multi(
|
||||||
|
self.hass, self.config_entry, unjoin_data.speakers
|
||||||
|
)
|
||||||
unjoin_data.event.set()
|
unjoin_data.event.set()
|
||||||
|
|
||||||
if unjoin_data := sonos_data.unjoin_data.get(household_id):
|
if unjoin_data := sonos_data.unjoin_data.get(household_id):
|
||||||
|
@ -6,7 +6,6 @@ import logging
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from homeassistant.components.number import NumberEntity
|
from homeassistant.components.number import NumberEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
@ -14,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
|
|
||||||
from .const import SONOS_CREATE_LEVELS
|
from .const import SONOS_CREATE_LEVELS
|
||||||
from .entity import SonosEntity
|
from .entity import SonosEntity
|
||||||
from .helpers import soco_error
|
from .helpers import SonosConfigEntry, soco_error
|
||||||
from .speaker import SonosSpeaker
|
from .speaker import SonosSpeaker
|
||||||
|
|
||||||
LEVEL_TYPES = {
|
LEVEL_TYPES = {
|
||||||
@ -69,7 +68,7 @@ LEVEL_FROM_NUMBER = {"balance": _balance_from_number}
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: SonosConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Sonos number platform from a config entry."""
|
"""Set up the Sonos number platform from a config entry."""
|
||||||
@ -93,7 +92,9 @@ async def async_setup_entry(
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Creating %s number control on %s", level_type, speaker.zone_name
|
"Creating %s number control on %s", level_type, speaker.zone_name
|
||||||
)
|
)
|
||||||
entities.append(SonosLevelEntity(speaker, level_type, valid_range))
|
entities.append(
|
||||||
|
SonosLevelEntity(speaker, config_entry, level_type, valid_range)
|
||||||
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
config_entry.async_on_unload(
|
config_entry.async_on_unload(
|
||||||
@ -107,10 +108,14 @@ class SonosLevelEntity(SonosEntity, NumberEntity):
|
|||||||
_attr_entity_category = EntityCategory.CONFIG
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, speaker: SonosSpeaker, level_type: str, valid_range: tuple[int, int]
|
self,
|
||||||
|
speaker: SonosSpeaker,
|
||||||
|
config_entry: SonosConfigEntry,
|
||||||
|
level_type: str,
|
||||||
|
valid_range: tuple[int, int],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the level entity."""
|
"""Initialize the level entity."""
|
||||||
super().__init__(speaker)
|
super().__init__(speaker, config_entry)
|
||||||
self._attr_unique_id = f"{self.soco.uid}-{level_type}"
|
self._attr_unique_id = f"{self.soco.uid}-{level_type}"
|
||||||
self._attr_translation_key = level_type
|
self._attr_translation_key = level_type
|
||||||
self.level_type = level_type
|
self.level_type = level_type
|
||||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
@ -20,7 +19,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .entity import SonosEntity, SonosPollingEntity
|
from .entity import SonosEntity, SonosPollingEntity
|
||||||
from .favorites import SonosFavorites
|
from .favorites import SonosFavorites
|
||||||
from .helpers import soco_error
|
from .helpers import SonosConfigEntry, soco_error
|
||||||
from .speaker import SonosSpeaker
|
from .speaker import SonosSpeaker
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -28,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: SonosConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Sonos from a config entry."""
|
"""Set up Sonos from a config entry."""
|
||||||
@ -38,13 +37,13 @@ async def async_setup_entry(
|
|||||||
speaker: SonosSpeaker, audio_format: str
|
speaker: SonosSpeaker, audio_format: str
|
||||||
) -> None:
|
) -> None:
|
||||||
_LOGGER.debug("Creating audio input format sensor on %s", speaker.zone_name)
|
_LOGGER.debug("Creating audio input format sensor on %s", speaker.zone_name)
|
||||||
entity = SonosAudioInputFormatSensorEntity(speaker, audio_format)
|
entity = SonosAudioInputFormatSensorEntity(speaker, config_entry, audio_format)
|
||||||
async_add_entities([entity])
|
async_add_entities([entity])
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_create_battery_sensor(speaker: SonosSpeaker) -> None:
|
def _async_create_battery_sensor(speaker: SonosSpeaker) -> None:
|
||||||
_LOGGER.debug("Creating battery level sensor on %s", speaker.zone_name)
|
_LOGGER.debug("Creating battery level sensor on %s", speaker.zone_name)
|
||||||
entity = SonosBatteryEntity(speaker)
|
entity = SonosBatteryEntity(speaker, config_entry)
|
||||||
async_add_entities([entity])
|
async_add_entities([entity])
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -82,9 +81,9 @@ class SonosBatteryEntity(SonosEntity, SensorEntity):
|
|||||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
_attr_native_unit_of_measurement = PERCENTAGE
|
_attr_native_unit_of_measurement = PERCENTAGE
|
||||||
|
|
||||||
def __init__(self, speaker: SonosSpeaker) -> None:
|
def __init__(self, speaker: SonosSpeaker, config_entry: SonosConfigEntry) -> None:
|
||||||
"""Initialize the battery sensor."""
|
"""Initialize the battery sensor."""
|
||||||
super().__init__(speaker)
|
super().__init__(speaker, config_entry)
|
||||||
self._attr_unique_id = f"{self.soco.uid}-battery"
|
self._attr_unique_id = f"{self.soco.uid}-battery"
|
||||||
|
|
||||||
async def _async_fallback_poll(self) -> None:
|
async def _async_fallback_poll(self) -> None:
|
||||||
@ -109,9 +108,11 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity):
|
|||||||
_attr_translation_key = "audio_input_format"
|
_attr_translation_key = "audio_input_format"
|
||||||
_attr_should_poll = True
|
_attr_should_poll = True
|
||||||
|
|
||||||
def __init__(self, speaker: SonosSpeaker, audio_format: str) -> None:
|
def __init__(
|
||||||
|
self, speaker: SonosSpeaker, config_entry: SonosConfigEntry, audio_format: str
|
||||||
|
) -> None:
|
||||||
"""Initialize the audio input format sensor."""
|
"""Initialize the audio input format sensor."""
|
||||||
super().__init__(speaker)
|
super().__init__(speaker, config_entry)
|
||||||
self._attr_unique_id = f"{self.soco.uid}-audio-format"
|
self._attr_unique_id = f"{self.soco.uid}-audio-format"
|
||||||
self._attr_native_value = audio_format
|
self._attr_native_value = audio_format
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ from soco.snapshot import Snapshot
|
|||||||
from sonos_websocket import SonosWebsocket
|
from sonos_websocket import SonosWebsocket
|
||||||
|
|
||||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
@ -38,7 +37,6 @@ from .alarms import SonosAlarms
|
|||||||
from .const import (
|
from .const import (
|
||||||
AVAILABILITY_TIMEOUT,
|
AVAILABILITY_TIMEOUT,
|
||||||
BATTERY_SCAN_INTERVAL,
|
BATTERY_SCAN_INTERVAL,
|
||||||
DATA_SONOS,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SCAN_INTERVAL,
|
SCAN_INTERVAL,
|
||||||
SONOS_CHECK_ACTIVITY,
|
SONOS_CHECK_ACTIVITY,
|
||||||
@ -66,7 +64,8 @@ from .media import SonosMedia
|
|||||||
from .statistics import ActivityStatistics, EventStatistics
|
from .statistics import ActivityStatistics, EventStatistics
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import SonosData
|
from .helpers import SonosConfigEntry
|
||||||
|
|
||||||
|
|
||||||
NEVER_TIME = -1200.0
|
NEVER_TIME = -1200.0
|
||||||
RESUB_COOLDOWN_SECONDS = 10.0
|
RESUB_COOLDOWN_SECONDS = 10.0
|
||||||
@ -95,13 +94,15 @@ class SonosSpeaker:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
config_entry: SonosConfigEntry,
|
||||||
soco: SoCo,
|
soco: SoCo,
|
||||||
speaker_info: dict[str, Any],
|
speaker_info: dict[str, Any],
|
||||||
zone_group_state_sub: SubscriptionBase | None,
|
zone_group_state_sub: SubscriptionBase | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a SonosSpeaker."""
|
"""Initialize a SonosSpeaker."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.data: SonosData = hass.data[DATA_SONOS]
|
self.config_entry = config_entry
|
||||||
|
self.data = config_entry.runtime_data
|
||||||
self.soco = soco
|
self.soco = soco
|
||||||
self.websocket: SonosWebsocket | None = None
|
self.websocket: SonosWebsocket | None = None
|
||||||
self.household_id: str = soco.household_id
|
self.household_id: str = soco.household_id
|
||||||
@ -179,7 +180,10 @@ class SonosSpeaker:
|
|||||||
self._group_members_missing: set[str] = set()
|
self._group_members_missing: set[str] = set()
|
||||||
|
|
||||||
async def async_setup(
|
async def async_setup(
|
||||||
self, entry: ConfigEntry, has_battery: bool, dispatches: list[tuple[Any, ...]]
|
self,
|
||||||
|
entry: SonosConfigEntry,
|
||||||
|
has_battery: bool,
|
||||||
|
dispatches: list[tuple[Any, ...]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Complete setup in async context."""
|
"""Complete setup in async context."""
|
||||||
# Battery events can be infrequent, polling is still necessary
|
# Battery events can be infrequent, polling is still necessary
|
||||||
@ -216,7 +220,7 @@ class SonosSpeaker:
|
|||||||
|
|
||||||
await self.async_subscribe()
|
await self.async_subscribe()
|
||||||
|
|
||||||
def setup(self, entry: ConfigEntry) -> None:
|
def setup(self, entry: SonosConfigEntry) -> None:
|
||||||
"""Run initial setup of the speaker."""
|
"""Run initial setup of the speaker."""
|
||||||
self.media.play_mode = self.soco.play_mode
|
self.media.play_mode = self.soco.play_mode
|
||||||
self.update_volume()
|
self.update_volume()
|
||||||
@ -961,15 +965,16 @@ class SonosSpeaker:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def join_multi(
|
async def join_multi(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
config_entry: SonosConfigEntry,
|
||||||
master: SonosSpeaker,
|
master: SonosSpeaker,
|
||||||
speakers: list[SonosSpeaker],
|
speakers: list[SonosSpeaker],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Form a group with other players."""
|
"""Form a group with other players."""
|
||||||
async with hass.data[DATA_SONOS].topology_condition:
|
async with config_entry.runtime_data.topology_condition:
|
||||||
group: list[SonosSpeaker] = await hass.async_add_executor_job(
|
group: list[SonosSpeaker] = await hass.async_add_executor_job(
|
||||||
master.join, speakers
|
master.join, speakers
|
||||||
)
|
)
|
||||||
await SonosSpeaker.wait_for_groups(hass, [group])
|
await SonosSpeaker.wait_for_groups(hass, config_entry, [group])
|
||||||
|
|
||||||
@soco_error()
|
@soco_error()
|
||||||
def unjoin(self) -> None:
|
def unjoin(self) -> None:
|
||||||
@ -980,7 +985,11 @@ class SonosSpeaker:
|
|||||||
self.coordinator = None
|
self.coordinator = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def unjoin_multi(hass: HomeAssistant, speakers: list[SonosSpeaker]) -> None:
|
async def unjoin_multi(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: SonosConfigEntry,
|
||||||
|
speakers: list[SonosSpeaker],
|
||||||
|
) -> None:
|
||||||
"""Unjoin several players from their group."""
|
"""Unjoin several players from their group."""
|
||||||
|
|
||||||
def _unjoin_all(speakers: list[SonosSpeaker]) -> None:
|
def _unjoin_all(speakers: list[SonosSpeaker]) -> None:
|
||||||
@ -992,9 +1001,11 @@ class SonosSpeaker:
|
|||||||
for speaker in joined_speakers + coordinators:
|
for speaker in joined_speakers + coordinators:
|
||||||
speaker.unjoin()
|
speaker.unjoin()
|
||||||
|
|
||||||
async with hass.data[DATA_SONOS].topology_condition:
|
async with config_entry.runtime_data.topology_condition:
|
||||||
await hass.async_add_executor_job(_unjoin_all, speakers)
|
await hass.async_add_executor_job(_unjoin_all, speakers)
|
||||||
await SonosSpeaker.wait_for_groups(hass, [[s] for s in speakers])
|
await SonosSpeaker.wait_for_groups(
|
||||||
|
hass, config_entry, [[s] for s in speakers]
|
||||||
|
)
|
||||||
|
|
||||||
@soco_error()
|
@soco_error()
|
||||||
def snapshot(self, with_group: bool) -> None:
|
def snapshot(self, with_group: bool) -> None:
|
||||||
@ -1008,7 +1019,10 @@ class SonosSpeaker:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def snapshot_multi(
|
async def snapshot_multi(
|
||||||
hass: HomeAssistant, speakers: list[SonosSpeaker], with_group: bool
|
hass: HomeAssistant,
|
||||||
|
config_entry: SonosConfigEntry,
|
||||||
|
speakers: list[SonosSpeaker],
|
||||||
|
with_group: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Snapshot all the speakers and optionally their groups."""
|
"""Snapshot all the speakers and optionally their groups."""
|
||||||
|
|
||||||
@ -1023,7 +1037,7 @@ class SonosSpeaker:
|
|||||||
for speaker in list(speakers_set):
|
for speaker in list(speakers_set):
|
||||||
speakers_set.update(speaker.sonos_group)
|
speakers_set.update(speaker.sonos_group)
|
||||||
|
|
||||||
async with hass.data[DATA_SONOS].topology_condition:
|
async with config_entry.runtime_data.topology_condition:
|
||||||
await hass.async_add_executor_job(_snapshot_all, speakers_set)
|
await hass.async_add_executor_job(_snapshot_all, speakers_set)
|
||||||
|
|
||||||
@soco_error()
|
@soco_error()
|
||||||
@ -1041,7 +1055,10 @@ class SonosSpeaker:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def restore_multi(
|
async def restore_multi(
|
||||||
hass: HomeAssistant, speakers: list[SonosSpeaker], with_group: bool
|
hass: HomeAssistant,
|
||||||
|
config_entry: SonosConfigEntry,
|
||||||
|
speakers: list[SonosSpeaker],
|
||||||
|
with_group: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Restore snapshots for all the speakers."""
|
"""Restore snapshots for all the speakers."""
|
||||||
|
|
||||||
@ -1119,16 +1136,18 @@ class SonosSpeaker:
|
|||||||
assert len(speaker.snapshot_group)
|
assert len(speaker.snapshot_group)
|
||||||
speakers_set.update(speaker.snapshot_group)
|
speakers_set.update(speaker.snapshot_group)
|
||||||
|
|
||||||
async with hass.data[DATA_SONOS].topology_condition:
|
async with config_entry.runtime_data.topology_condition:
|
||||||
groups = await hass.async_add_executor_job(
|
groups = await hass.async_add_executor_job(
|
||||||
_restore_groups, speakers_set, with_group
|
_restore_groups, speakers_set, with_group
|
||||||
)
|
)
|
||||||
await SonosSpeaker.wait_for_groups(hass, groups)
|
await SonosSpeaker.wait_for_groups(hass, config_entry, groups)
|
||||||
await hass.async_add_executor_job(_restore_players, speakers_set)
|
await hass.async_add_executor_job(_restore_players, speakers_set)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def wait_for_groups(
|
async def wait_for_groups(
|
||||||
hass: HomeAssistant, groups: list[list[SonosSpeaker]]
|
hass: HomeAssistant,
|
||||||
|
config_entry: SonosConfigEntry,
|
||||||
|
groups: list[list[SonosSpeaker]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Wait until all groups are present, or timeout."""
|
"""Wait until all groups are present, or timeout."""
|
||||||
|
|
||||||
@ -1151,11 +1170,11 @@ class SonosSpeaker:
|
|||||||
try:
|
try:
|
||||||
async with asyncio.timeout(5):
|
async with asyncio.timeout(5):
|
||||||
while not _test_groups(groups):
|
while not _test_groups(groups):
|
||||||
await hass.data[DATA_SONOS].topology_condition.wait()
|
await config_entry.runtime_data.topology_condition.wait()
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
_LOGGER.warning("Timeout waiting for target groups %s", groups)
|
_LOGGER.warning("Timeout waiting for target groups %s", groups)
|
||||||
|
|
||||||
any_speaker = next(iter(hass.data[DATA_SONOS].discovered.values()))
|
any_speaker = next(iter(config_entry.runtime_data.discovered.values()))
|
||||||
any_speaker.soco.zone_group_state.clear_cache()
|
any_speaker.soco.zone_group_state.clear_cache()
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -10,7 +10,6 @@ from soco.alarms import Alarm
|
|||||||
from soco.exceptions import SoCoSlaveException, SoCoUPnPException
|
from soco.exceptions import SoCoSlaveException, SoCoUPnPException
|
||||||
|
|
||||||
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
|
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import ATTR_TIME, EntityCategory
|
from homeassistant.const import ATTR_TIME, EntityCategory
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
@ -18,15 +17,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.event import async_track_time_change
|
from homeassistant.helpers.event import async_track_time_change
|
||||||
|
|
||||||
|
from .alarms import SonosAlarms
|
||||||
from .const import (
|
from .const import (
|
||||||
DATA_SONOS,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SONOS_ALARMS_UPDATED,
|
SONOS_ALARMS_UPDATED,
|
||||||
SONOS_CREATE_ALARM,
|
SONOS_CREATE_ALARM,
|
||||||
SONOS_CREATE_SWITCHES,
|
SONOS_CREATE_SWITCHES,
|
||||||
)
|
)
|
||||||
from .entity import SonosEntity, SonosPollingEntity
|
from .entity import SonosEntity, SonosPollingEntity
|
||||||
from .helpers import soco_error
|
from .helpers import SonosConfigEntry, soco_error
|
||||||
from .speaker import SonosSpeaker
|
from .speaker import SonosSpeaker
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -73,22 +72,22 @@ WEEKEND_DAYS = (0, 6)
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: SonosConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Sonos from a config entry."""
|
"""Set up Sonos from a config entry."""
|
||||||
|
|
||||||
async def _async_create_alarms(speaker: SonosSpeaker, alarm_ids: list[str]) -> None:
|
async def _async_create_alarms(speaker: SonosSpeaker, alarm_ids: list[str]) -> None:
|
||||||
entities = []
|
entities = []
|
||||||
created_alarms = (
|
created_alarms = config_entry.runtime_data.alarms[
|
||||||
hass.data[DATA_SONOS].alarms[speaker.household_id].created_alarm_ids
|
speaker.household_id
|
||||||
)
|
].created_alarm_ids
|
||||||
for alarm_id in alarm_ids:
|
for alarm_id in alarm_ids:
|
||||||
if alarm_id in created_alarms:
|
if alarm_id in created_alarms:
|
||||||
continue
|
continue
|
||||||
_LOGGER.debug("Creating alarm %s on %s", alarm_id, speaker.zone_name)
|
_LOGGER.debug("Creating alarm %s on %s", alarm_id, speaker.zone_name)
|
||||||
created_alarms.add(alarm_id)
|
created_alarms.add(alarm_id)
|
||||||
entities.append(SonosAlarmEntity(alarm_id, speaker))
|
entities.append(SonosAlarmEntity(alarm_id, speaker, config_entry))
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
def available_soco_attributes(speaker: SonosSpeaker) -> list[str]:
|
def available_soco_attributes(speaker: SonosSpeaker) -> list[str]:
|
||||||
@ -113,7 +112,7 @@ async def async_setup_entry(
|
|||||||
feature_type,
|
feature_type,
|
||||||
speaker.zone_name,
|
speaker.zone_name,
|
||||||
)
|
)
|
||||||
entities.append(SonosSwitchEntity(feature_type, speaker))
|
entities.append(SonosSwitchEntity(feature_type, speaker, config_entry))
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
config_entry.async_on_unload(
|
config_entry.async_on_unload(
|
||||||
@ -127,9 +126,11 @@ async def async_setup_entry(
|
|||||||
class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
|
class SonosSwitchEntity(SonosPollingEntity, SwitchEntity):
|
||||||
"""Representation of a Sonos feature switch."""
|
"""Representation of a Sonos feature switch."""
|
||||||
|
|
||||||
def __init__(self, feature_type: str, speaker: SonosSpeaker) -> None:
|
def __init__(
|
||||||
|
self, feature_type: str, speaker: SonosSpeaker, config_entry: SonosConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Initialize the switch."""
|
"""Initialize the switch."""
|
||||||
super().__init__(speaker)
|
super().__init__(speaker, config_entry)
|
||||||
self.feature_type = feature_type
|
self.feature_type = feature_type
|
||||||
self.needs_coordinator = feature_type in COORDINATOR_FEATURES
|
self.needs_coordinator = feature_type in COORDINATOR_FEATURES
|
||||||
self._attr_entity_category = EntityCategory.CONFIG
|
self._attr_entity_category = EntityCategory.CONFIG
|
||||||
@ -185,9 +186,11 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
|
|||||||
_attr_entity_category = EntityCategory.CONFIG
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
_attr_icon = "mdi:alarm"
|
_attr_icon = "mdi:alarm"
|
||||||
|
|
||||||
def __init__(self, alarm_id: str, speaker: SonosSpeaker) -> None:
|
def __init__(
|
||||||
|
self, alarm_id: str, speaker: SonosSpeaker, config_entry: SonosConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Initialize the switch."""
|
"""Initialize the switch."""
|
||||||
super().__init__(speaker)
|
super().__init__(speaker, config_entry)
|
||||||
self._attr_unique_id = f"alarm-{speaker.household_id}:{alarm_id}"
|
self._attr_unique_id = f"alarm-{speaker.household_id}:{alarm_id}"
|
||||||
self.alarm_id = alarm_id
|
self.alarm_id = alarm_id
|
||||||
self.household_id = speaker.household_id
|
self.household_id = speaker.household_id
|
||||||
@ -218,7 +221,9 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
|
|||||||
@property
|
@property
|
||||||
def alarm(self) -> Alarm:
|
def alarm(self) -> Alarm:
|
||||||
"""Return the alarm instance."""
|
"""Return the alarm instance."""
|
||||||
return self.hass.data[DATA_SONOS].alarms[self.household_id].get(self.alarm_id)
|
return self.config_entry.runtime_data.alarms[self.household_id].get(
|
||||||
|
self.alarm_id
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -230,7 +235,9 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
|
|||||||
|
|
||||||
async def _async_fallback_poll(self) -> None:
|
async def _async_fallback_poll(self) -> None:
|
||||||
"""Call the central alarm polling method."""
|
"""Call the central alarm polling method."""
|
||||||
await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll()
|
alarms: SonosAlarms = self.config_entry.runtime_data.alarms[self.household_id]
|
||||||
|
assert alarms.async_poll
|
||||||
|
await alarms.async_poll()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_check_if_available(self) -> bool:
|
def async_check_if_available(self) -> bool:
|
||||||
@ -252,9 +259,9 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.speaker.soco.uid != self.alarm.zone.uid:
|
if self.speaker.soco.uid != self.alarm.zone.uid:
|
||||||
self.speaker = self.hass.data[DATA_SONOS].discovered.get(
|
speaker = self.config_entry.runtime_data.discovered.get(self.alarm.zone.uid)
|
||||||
self.alarm.zone.uid
|
assert speaker
|
||||||
)
|
self.speaker = speaker
|
||||||
if self.speaker is None:
|
if self.speaker is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"No configured Sonos speaker has been found to match the alarm."
|
"No configured Sonos speaker has been found to match the alarm."
|
||||||
|
@ -5,12 +5,15 @@ from unittest.mock import Mock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, SERVICE_JOIN
|
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, SERVICE_JOIN
|
||||||
from homeassistant.components.sonos.const import DATA_SONOS
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
async def test_media_player_join(hass: HomeAssistant, async_autosetup_sonos) -> None:
|
|
||||||
|
async def test_media_player_join(
|
||||||
|
hass: HomeAssistant, async_autosetup_sonos, config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Test join service."""
|
"""Test join service."""
|
||||||
valid_entity_id = "media_player.zone_a"
|
valid_entity_id = "media_player.zone_a"
|
||||||
mocked_entity_id = "media_player.mocked"
|
mocked_entity_id = "media_player.mocked"
|
||||||
@ -29,7 +32,10 @@ async def test_media_player_join(hass: HomeAssistant, async_autosetup_sonos) ->
|
|||||||
mock_entity_id_mappings = {mocked_entity_id: mocked_speaker}
|
mock_entity_id_mappings = {mocked_entity_id: mocked_speaker}
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch.dict(hass.data[DATA_SONOS].entity_id_mappings, mock_entity_id_mappings),
|
patch.dict(
|
||||||
|
config_entry.runtime_data.entity_id_mappings,
|
||||||
|
mock_entity_id_mappings,
|
||||||
|
),
|
||||||
patch(
|
patch(
|
||||||
"homeassistant.components.sonos.speaker.SonosSpeaker.join_multi"
|
"homeassistant.components.sonos.speaker.SonosSpeaker.join_multi"
|
||||||
) as mock_join_multi,
|
) as mock_join_multi,
|
||||||
@ -41,5 +47,7 @@ async def test_media_player_join(hass: HomeAssistant, async_autosetup_sonos) ->
|
|||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
found_speaker = hass.data[DATA_SONOS].entity_id_mappings[valid_entity_id]
|
found_speaker = config_entry.runtime_data.entity_id_mappings[valid_entity_id]
|
||||||
mock_join_multi.assert_called_with(hass, found_speaker, [mocked_speaker])
|
mock_join_multi.assert_called_with(
|
||||||
|
hass, config_entry, found_speaker, [mocked_speaker]
|
||||||
|
)
|
||||||
|
@ -9,13 +9,18 @@ from homeassistant.components.media_player import (
|
|||||||
SERVICE_MEDIA_PLAY,
|
SERVICE_MEDIA_PLAY,
|
||||||
)
|
)
|
||||||
from homeassistant.components.sonos import DOMAIN
|
from homeassistant.components.sonos import DOMAIN
|
||||||
from homeassistant.components.sonos.const import DATA_SONOS, SCAN_INTERVAL
|
from homeassistant.components.sonos.const import SCAN_INTERVAL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .conftest import MockSoCo, SonosMockEvent
|
from .conftest import MockSoCo, SonosMockEvent
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed, load_fixture, load_json_value_fixture
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
async_fire_time_changed,
|
||||||
|
load_fixture,
|
||||||
|
load_json_value_fixture,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_fallback_to_polling(
|
async def test_fallback_to_polling(
|
||||||
@ -33,7 +38,7 @@ async def test_fallback_to_polling(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await fire_zgs_event()
|
await fire_zgs_event()
|
||||||
|
|
||||||
speaker = list(hass.data[DATA_SONOS].discovered.values())[0]
|
speaker = list(config_entry.runtime_data.discovered.values())[0]
|
||||||
assert speaker.soco is soco
|
assert speaker.soco is soco
|
||||||
assert speaker._subscriptions
|
assert speaker._subscriptions
|
||||||
assert not speaker.subscriptions_failed
|
assert not speaker.subscriptions_failed
|
||||||
@ -56,7 +61,7 @@ async def test_fallback_to_polling(
|
|||||||
|
|
||||||
|
|
||||||
async def test_subscription_creation_fails(
|
async def test_subscription_creation_fails(
|
||||||
hass: HomeAssistant, async_setup_sonos
|
hass: HomeAssistant, async_setup_sonos, config_entry: MockConfigEntry
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that subscription creation failures are handled."""
|
"""Test that subscription creation failures are handled."""
|
||||||
with patch(
|
with patch(
|
||||||
@ -66,7 +71,7 @@ async def test_subscription_creation_fails(
|
|||||||
await async_setup_sonos()
|
await async_setup_sonos()
|
||||||
await hass.async_block_till_done(wait_background_tasks=True)
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
speaker = list(hass.data[DATA_SONOS].discovered.values())[0]
|
speaker = list(config_entry.runtime_data.discovered.values())[0]
|
||||||
assert not speaker._subscriptions
|
assert not speaker._subscriptions
|
||||||
|
|
||||||
with patch.object(speaker, "_resub_cooldown_expires_at", None):
|
with patch.object(speaker, "_resub_cooldown_expires_at", None):
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
"""Tests for the Sonos statistics."""
|
"""Tests for the Sonos statistics."""
|
||||||
|
|
||||||
from homeassistant.components.sonos.const import DATA_SONOS
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def test_statistics_duplicate(
|
async def test_statistics_duplicate(
|
||||||
hass: HomeAssistant, async_autosetup_sonos, soco, device_properties_event
|
hass: HomeAssistant,
|
||||||
|
async_autosetup_sonos,
|
||||||
|
soco,
|
||||||
|
device_properties_event,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Sonos statistics."""
|
"""Test Sonos statistics."""
|
||||||
speaker = list(hass.data[DATA_SONOS].discovered.values())[0]
|
speaker = list(config_entry.runtime_data.discovered.values())[0]
|
||||||
|
|
||||||
subscription = soco.deviceProperties.subscribe.return_value
|
subscription = soco.deviceProperties.subscribe.return_value
|
||||||
sub_callback = subscription.callback
|
sub_callback = subscription.callback
|
||||||
|
Loading…
x
Reference in New Issue
Block a user