Add config flow for Bose SoundTouch (#72967)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>pull/74292/head
parent
7655b84494
commit
273e9b287f
homeassistant
components
discovery
generated
tests/components/soundtouch
|
@ -979,6 +979,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/songpal/ @rytilahti @shenxn
|
||||
/homeassistant/components/sonos/ @cgtobi @jjlawren
|
||||
/tests/components/sonos/ @cgtobi @jjlawren
|
||||
/homeassistant/components/soundtouch/ @kroimon
|
||||
/tests/components/soundtouch/ @kroimon
|
||||
/homeassistant/components/spaceapi/ @fabaff
|
||||
/tests/components/spaceapi/ @fabaff
|
||||
/homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87
|
||||
|
|
|
@ -61,7 +61,6 @@ SERVICE_HANDLERS = {
|
|||
"yamaha": ServiceDetails("media_player", "yamaha"),
|
||||
"frontier_silicon": ServiceDetails("media_player", "frontier_silicon"),
|
||||
"openhome": ServiceDetails("media_player", "openhome"),
|
||||
"bose_soundtouch": ServiceDetails("media_player", "soundtouch"),
|
||||
"bluesound": ServiceDetails("media_player", "bluesound"),
|
||||
}
|
||||
|
||||
|
@ -70,6 +69,7 @@ OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {}
|
|||
MIGRATED_SERVICE_HANDLERS = [
|
||||
SERVICE_APPLE_TV,
|
||||
"axis",
|
||||
"bose_soundtouch",
|
||||
"deconz",
|
||||
SERVICE_DAIKIN,
|
||||
"denonavr",
|
||||
|
|
|
@ -1 +1,142 @@
|
|||
"""The soundtouch component."""
|
||||
import logging
|
||||
|
||||
from libsoundtouch import soundtouch_device
|
||||
from libsoundtouch.device import SoundTouchDevice
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SERVICE_ADD_ZONE_SLAVE,
|
||||
SERVICE_CREATE_ZONE,
|
||||
SERVICE_PLAY_EVERYWHERE,
|
||||
SERVICE_REMOVE_ZONE_SLAVE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_PLAY_EVERYWHERE_SCHEMA = vol.Schema({vol.Required("master"): cv.entity_id})
|
||||
SERVICE_CREATE_ZONE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("master"): cv.entity_id,
|
||||
vol.Required("slaves"): cv.entity_ids,
|
||||
}
|
||||
)
|
||||
SERVICE_ADD_ZONE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("master"): cv.entity_id,
|
||||
vol.Required("slaves"): cv.entity_ids,
|
||||
}
|
||||
)
|
||||
SERVICE_REMOVE_ZONE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("master"): cv.entity_id,
|
||||
vol.Required("slaves"): cv.entity_ids,
|
||||
}
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
class SoundTouchData:
|
||||
"""SoundTouch data stored in the Home Assistant data object."""
|
||||
|
||||
def __init__(self, device: SoundTouchDevice) -> None:
|
||||
"""Initialize the SoundTouch data object for a device."""
|
||||
self.device = device
|
||||
self.media_player = None
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Bose SoundTouch component."""
|
||||
|
||||
async def service_handle(service: ServiceCall) -> None:
|
||||
"""Handle the applying of a service."""
|
||||
master_id = service.data.get("master")
|
||||
slaves_ids = service.data.get("slaves")
|
||||
slaves = []
|
||||
if slaves_ids:
|
||||
slaves = [
|
||||
data.media_player
|
||||
for data in hass.data[DOMAIN].values()
|
||||
if data.media_player.entity_id in slaves_ids
|
||||
]
|
||||
|
||||
master = next(
|
||||
iter(
|
||||
[
|
||||
data.media_player
|
||||
for data in hass.data[DOMAIN].values()
|
||||
if data.media_player.entity_id == master_id
|
||||
]
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if master is None:
|
||||
_LOGGER.warning("Unable to find master with entity_id: %s", str(master_id))
|
||||
return
|
||||
|
||||
if service.service == SERVICE_PLAY_EVERYWHERE:
|
||||
slaves = [
|
||||
data.media_player
|
||||
for data in hass.data[DOMAIN].values()
|
||||
if data.media_player.entity_id != master_id
|
||||
]
|
||||
await hass.async_add_executor_job(master.create_zone, slaves)
|
||||
elif service.service == SERVICE_CREATE_ZONE:
|
||||
await hass.async_add_executor_job(master.create_zone, slaves)
|
||||
elif service.service == SERVICE_REMOVE_ZONE_SLAVE:
|
||||
await hass.async_add_executor_job(master.remove_zone_slave, slaves)
|
||||
elif service.service == SERVICE_ADD_ZONE_SLAVE:
|
||||
await hass.async_add_executor_job(master.add_zone_slave, slaves)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_PLAY_EVERYWHERE,
|
||||
service_handle,
|
||||
schema=SERVICE_PLAY_EVERYWHERE_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CREATE_ZONE,
|
||||
service_handle,
|
||||
schema=SERVICE_CREATE_ZONE_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_REMOVE_ZONE_SLAVE,
|
||||
service_handle,
|
||||
schema=SERVICE_REMOVE_ZONE_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_ADD_ZONE_SLAVE,
|
||||
service_handle,
|
||||
schema=SERVICE_ADD_ZONE_SCHEMA,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Bose SoundTouch from a config entry."""
|
||||
device = await hass.async_add_executor_job(soundtouch_device, entry.data[CONF_HOST])
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device)
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
return unload_ok
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
"""Config flow for Bose SoundTouch integration."""
|
||||
import logging
|
||||
|
||||
from libsoundtouch import soundtouch_device
|
||||
from requests import RequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SoundtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Bose SoundTouch."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a new SoundTouch config flow."""
|
||||
self.host = None
|
||||
self.name = None
|
||||
|
||||
async def async_step_import(self, import_data):
|
||||
"""Handle a flow initiated by configuration file."""
|
||||
self.host = import_data[CONF_HOST]
|
||||
|
||||
try:
|
||||
await self._async_get_device_id()
|
||||
except RequestException:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
return await self._async_create_soundtouch_entry()
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initiated by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self.host = user_input[CONF_HOST]
|
||||
|
||||
try:
|
||||
await self._async_get_device_id(raise_on_progress=False)
|
||||
except RequestException:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return await self._async_create_soundtouch_entry()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
last_step=True,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Handle a flow initiated by a zeroconf discovery."""
|
||||
self.host = discovery_info.host
|
||||
|
||||
try:
|
||||
await self._async_get_device_id()
|
||||
except RequestException:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
self.context["title_placeholders"] = {"name": self.name}
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(self, user_input=None):
|
||||
"""Handle user-confirmation of discovered node."""
|
||||
if user_input is not None:
|
||||
return await self._async_create_soundtouch_entry()
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
last_step=True,
|
||||
description_placeholders={"name": self.name},
|
||||
)
|
||||
|
||||
async def _async_get_device_id(self, raise_on_progress: bool = True) -> None:
|
||||
"""Get device ID from SoundTouch device."""
|
||||
device = await self.hass.async_add_executor_job(soundtouch_device, self.host)
|
||||
|
||||
# Check if already configured
|
||||
await self.async_set_unique_id(
|
||||
device.config.device_id, raise_on_progress=raise_on_progress
|
||||
)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
|
||||
|
||||
self.name = device.config.name
|
||||
|
||||
async def _async_create_soundtouch_entry(self):
|
||||
"""Finish config flow and create a SoundTouch config entry."""
|
||||
return self.async_create_entry(
|
||||
title=self.name,
|
||||
data={
|
||||
CONF_HOST: self.host,
|
||||
},
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
"""Constants for the Bose Soundtouch component."""
|
||||
"""Constants for the Bose SoundTouch component."""
|
||||
DOMAIN = "soundtouch"
|
||||
SERVICE_PLAY_EVERYWHERE = "play_everywhere"
|
||||
SERVICE_CREATE_ZONE = "create_zone"
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"domain": "soundtouch",
|
||||
"name": "Bose Soundtouch",
|
||||
"name": "Bose SoundTouch",
|
||||
"documentation": "https://www.home-assistant.io/integrations/soundtouch",
|
||||
"requirements": ["libsoundtouch==0.8"],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": [],
|
||||
"zeroconf": ["_soundtouch._tcp.local."],
|
||||
"codeowners": ["@kroimon"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["libsoundtouch"]
|
||||
"loggers": ["libsoundtouch"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
"""Support for interface with a Bose Soundtouch."""
|
||||
"""Support for interface with a Bose SoundTouch."""
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
|
||||
from libsoundtouch import soundtouch_device
|
||||
from libsoundtouch.device import SoundTouchDevice
|
||||
from libsoundtouch.utils import Source
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
PLATFORM_SCHEMA,
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
)
|
||||
from homeassistant.components.media_player.browse_media import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
|
@ -27,19 +29,16 @@ from homeassistant.const import (
|
|||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SERVICE_ADD_ZONE_SLAVE,
|
||||
SERVICE_CREATE_ZONE,
|
||||
SERVICE_PLAY_EVERYWHERE,
|
||||
SERVICE_REMOVE_ZONE_SLAVE,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -50,137 +49,57 @@ MAP_STATUS = {
|
|||
"STOP_STATE": STATE_OFF,
|
||||
}
|
||||
|
||||
DATA_SOUNDTOUCH = "soundtouch"
|
||||
ATTR_SOUNDTOUCH_GROUP = "soundtouch_group"
|
||||
ATTR_SOUNDTOUCH_ZONE = "soundtouch_zone"
|
||||
|
||||
SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({vol.Required("master"): cv.entity_id})
|
||||
|
||||
SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema(
|
||||
{vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids}
|
||||
)
|
||||
|
||||
SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema(
|
||||
{vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids}
|
||||
)
|
||||
|
||||
SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema(
|
||||
{vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids}
|
||||
)
|
||||
|
||||
DEFAULT_NAME = "Bose Soundtouch"
|
||||
DEFAULT_PORT = 8090
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
}
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_NAME, default=""): cv.string,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Bose Soundtouch platform."""
|
||||
if DATA_SOUNDTOUCH not in hass.data:
|
||||
hass.data[DATA_SOUNDTOUCH] = []
|
||||
|
||||
if discovery_info:
|
||||
host = discovery_info["host"]
|
||||
port = int(discovery_info["port"])
|
||||
|
||||
# if device already exists by config
|
||||
if host in [device.config["host"] for device in hass.data[DATA_SOUNDTOUCH]]:
|
||||
return
|
||||
|
||||
remote_config = {"id": "ha.component.soundtouch", "host": host, "port": port}
|
||||
bose_soundtouch_entity = SoundTouchDevice(None, remote_config)
|
||||
hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity)
|
||||
add_entities([bose_soundtouch_entity], True)
|
||||
else:
|
||||
name = config.get(CONF_NAME)
|
||||
remote_config = {
|
||||
"id": "ha.component.soundtouch",
|
||||
"port": config.get(CONF_PORT),
|
||||
"host": config.get(CONF_HOST),
|
||||
}
|
||||
bose_soundtouch_entity = SoundTouchDevice(name, remote_config)
|
||||
hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity)
|
||||
add_entities([bose_soundtouch_entity], True)
|
||||
|
||||
def service_handle(service: ServiceCall) -> None:
|
||||
"""Handle the applying of a service."""
|
||||
master_device_id = service.data.get("master")
|
||||
slaves_ids = service.data.get("slaves")
|
||||
slaves = []
|
||||
if slaves_ids:
|
||||
slaves = [
|
||||
device
|
||||
for device in hass.data[DATA_SOUNDTOUCH]
|
||||
if device.entity_id in slaves_ids
|
||||
]
|
||||
|
||||
master = next(
|
||||
iter(
|
||||
[
|
||||
device
|
||||
for device in hass.data[DATA_SOUNDTOUCH]
|
||||
if device.entity_id == master_device_id
|
||||
]
|
||||
),
|
||||
None,
|
||||
"""Set up the Bose SoundTouch platform."""
|
||||
_LOGGER.warning(
|
||||
"Configuration of the Bose SoundTouch platform in YAML is deprecated and will be "
|
||||
"removed in a future release; Your existing configuration "
|
||||
"has been imported into the UI automatically and can be safely removed "
|
||||
"from your configuration.yaml file"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
|
||||
if master is None:
|
||||
_LOGGER.warning(
|
||||
"Unable to find master with entity_id: %s", str(master_device_id)
|
||||
)
|
||||
return
|
||||
|
||||
if service.service == SERVICE_PLAY_EVERYWHERE:
|
||||
slaves = [
|
||||
d for d in hass.data[DATA_SOUNDTOUCH] if d.entity_id != master_device_id
|
||||
]
|
||||
master.create_zone(slaves)
|
||||
elif service.service == SERVICE_CREATE_ZONE:
|
||||
master.create_zone(slaves)
|
||||
elif service.service == SERVICE_REMOVE_ZONE_SLAVE:
|
||||
master.remove_zone_slave(slaves)
|
||||
elif service.service == SERVICE_ADD_ZONE_SLAVE:
|
||||
master.add_zone_slave(slaves)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
SERVICE_PLAY_EVERYWHERE,
|
||||
service_handle,
|
||||
schema=SOUNDTOUCH_PLAY_EVERYWHERE,
|
||||
)
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
SERVICE_CREATE_ZONE,
|
||||
service_handle,
|
||||
schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA,
|
||||
)
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
SERVICE_REMOVE_ZONE_SLAVE,
|
||||
service_handle,
|
||||
schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA,
|
||||
)
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
SERVICE_ADD_ZONE_SLAVE,
|
||||
service_handle,
|
||||
schema=SOUNDTOUCH_ADD_ZONE_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class SoundTouchDevice(MediaPlayerEntity):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Bose SoundTouch media player based on a config entry."""
|
||||
device = hass.data[DOMAIN][entry.entry_id].device
|
||||
media_player = SoundTouchMediaPlayer(device)
|
||||
|
||||
async_add_entities([media_player], True)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id].media_player = media_player
|
||||
|
||||
|
||||
class SoundTouchMediaPlayer(MediaPlayerEntity):
|
||||
"""Representation of a SoundTouch Bose device."""
|
||||
|
||||
_attr_supported_features = (
|
||||
|
@ -197,28 +116,32 @@ class SoundTouchDevice(MediaPlayerEntity):
|
|||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
)
|
||||
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||
|
||||
def __init__(self, name, config):
|
||||
"""Create Soundtouch Entity."""
|
||||
def __init__(self, device: SoundTouchDevice) -> None:
|
||||
"""Create SoundTouch media player entity."""
|
||||
|
||||
self._device = device
|
||||
|
||||
self._attr_unique_id = self._device.config.device_id
|
||||
self._attr_name = self._device.config.name
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device.config.device_id)},
|
||||
connections={
|
||||
(CONNECTION_NETWORK_MAC, format_mac(self._device.config.mac_address))
|
||||
},
|
||||
manufacturer="Bose Corporation",
|
||||
model=self._device.config.type,
|
||||
name=self._device.config.name,
|
||||
)
|
||||
|
||||
self._device = soundtouch_device(config["host"], config["port"])
|
||||
if name is None:
|
||||
self._name = self._device.config.name
|
||||
else:
|
||||
self._name = name
|
||||
self._status = None
|
||||
self._volume = None
|
||||
self._config = config
|
||||
self._zone = None
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""Return specific soundtouch configuration."""
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""Return Soundtouch device."""
|
||||
"""Return SoundTouch device."""
|
||||
return self._device
|
||||
|
||||
def update(self):
|
||||
|
@ -232,17 +155,15 @@ class SoundTouchDevice(MediaPlayerEntity):
|
|||
"""Volume level of the media player (0..1)."""
|
||||
return self._volume.actual / 100
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._status.source == "STANDBY":
|
||||
if self._status is None or self._status.source == "STANDBY":
|
||||
return STATE_OFF
|
||||
|
||||
if self._status.source == "INVALID_SOURCE":
|
||||
return STATE_UNKNOWN
|
||||
|
||||
return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE)
|
||||
|
||||
@property
|
||||
|
@ -478,15 +399,12 @@ class SoundTouchDevice(MediaPlayerEntity):
|
|||
if not zone_status:
|
||||
return None
|
||||
|
||||
# Due to a bug in the SoundTouch API itself client devices do NOT return their
|
||||
# siblings as part of the "slaves" list. Only the master has the full list of
|
||||
# slaves for some reason. To compensate for this shortcoming we have to fetch
|
||||
# the zone info from the master when the current device is a slave until this is
|
||||
# fixed in the SoundTouch API or libsoundtouch, or of course until somebody has a
|
||||
# better idea on how to fix this.
|
||||
# Client devices do NOT return their siblings as part of the "slaves" list.
|
||||
# Only the master has the full list of slaves. To compensate for this shortcoming
|
||||
# we have to fetch the zone info from the master when the current device is a slave.
|
||||
# In addition to this shortcoming, libsoundtouch seems to report the "is_master"
|
||||
# property wrong on some slaves, so the only reliable way to detect if the current
|
||||
# devices is the master, is by comparing the master_id of the zone with the device_id
|
||||
# devices is the master, is by comparing the master_id of the zone with the device_id.
|
||||
if zone_status.master_id == self._device.config.device_id:
|
||||
return self._build_zone_info(self.entity_id, zone_status.slaves)
|
||||
|
||||
|
@ -505,16 +423,16 @@ class SoundTouchDevice(MediaPlayerEntity):
|
|||
|
||||
def _get_instance_by_ip(self, ip_address):
|
||||
"""Search and return a SoundTouchDevice instance by it's IP address."""
|
||||
for instance in self.hass.data[DATA_SOUNDTOUCH]:
|
||||
if instance and instance.config["host"] == ip_address:
|
||||
return instance
|
||||
for data in self.hass.data[DOMAIN].values():
|
||||
if data.device.config.device_ip == ip_address:
|
||||
return data.media_player
|
||||
return None
|
||||
|
||||
def _get_instance_by_id(self, instance_id):
|
||||
"""Search and return a SoundTouchDevice instance by it's ID (aka MAC address)."""
|
||||
for instance in self.hass.data[DATA_SOUNDTOUCH]:
|
||||
if instance and instance.device.config.device_id == instance_id:
|
||||
return instance
|
||||
for data in self.hass.data[DOMAIN].values():
|
||||
if data.device.config.device_id == instance_id:
|
||||
return data.media_player
|
||||
return None
|
||||
|
||||
def _build_zone_info(self, master, zone_slaves):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
play_everywhere:
|
||||
name: Play everywhere
|
||||
description: Play on all Bose Soundtouch devices.
|
||||
description: Play on all Bose SoundTouch devices.
|
||||
fields:
|
||||
master:
|
||||
name: Master
|
||||
|
@ -13,7 +13,7 @@ play_everywhere:
|
|||
|
||||
create_zone:
|
||||
name: Create zone
|
||||
description: Create a Soundtouch multi-room zone.
|
||||
description: Create a SoundTouch multi-room zone.
|
||||
fields:
|
||||
master:
|
||||
name: Master
|
||||
|
@ -35,7 +35,7 @@ create_zone:
|
|||
|
||||
add_zone_slave:
|
||||
name: Add zone slave
|
||||
description: Add a slave to a Soundtouch multi-room zone.
|
||||
description: Add a slave to a SoundTouch multi-room zone.
|
||||
fields:
|
||||
master:
|
||||
name: Master
|
||||
|
@ -57,7 +57,7 @@ add_zone_slave:
|
|||
|
||||
remove_zone_slave:
|
||||
name: Remove zone slave
|
||||
description: Remove a slave from the Soundtouch multi-room zone.
|
||||
description: Remove a slave from the SoundTouch multi-room zone.
|
||||
fields:
|
||||
master:
|
||||
name: Master
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Confirm adding Bose SoundTouch device",
|
||||
"description": "You are about to add the SoundTouch device named `{name}` to Home Assistant."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "You are about to add the SoundTouch device named `{name}` to Home Assistant.",
|
||||
"title": "Confirm adding Bose SoundTouch device"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -334,6 +334,7 @@ FLOWS = {
|
|||
"sonarr",
|
||||
"songpal",
|
||||
"sonos",
|
||||
"soundtouch",
|
||||
"speedtestdotnet",
|
||||
"spider",
|
||||
"spotify",
|
||||
|
|
|
@ -347,6 +347,11 @@ ZEROCONF = {
|
|||
"domain": "sonos"
|
||||
}
|
||||
],
|
||||
"_soundtouch._tcp.local.": [
|
||||
{
|
||||
"domain": "soundtouch"
|
||||
}
|
||||
],
|
||||
"_spotify-connect._tcp.local.": [
|
||||
{
|
||||
"domain": "spotify"
|
||||
|
|
|
@ -4,9 +4,9 @@ from requests_mock import Mocker
|
|||
|
||||
from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.components.soundtouch.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PLATFORM
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
|
||||
from tests.common import load_fixture
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
DEVICE_1_ID = "020000000001"
|
||||
DEVICE_2_ID = "020000000002"
|
||||
|
@ -14,8 +14,8 @@ DEVICE_1_IP = "192.168.42.1"
|
|||
DEVICE_2_IP = "192.168.42.2"
|
||||
DEVICE_1_URL = f"http://{DEVICE_1_IP}:8090"
|
||||
DEVICE_2_URL = f"http://{DEVICE_2_IP}:8090"
|
||||
DEVICE_1_NAME = "My Soundtouch 1"
|
||||
DEVICE_2_NAME = "My Soundtouch 2"
|
||||
DEVICE_1_NAME = "My SoundTouch 1"
|
||||
DEVICE_2_NAME = "My SoundTouch 2"
|
||||
DEVICE_1_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_1"
|
||||
DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2"
|
||||
|
||||
|
@ -24,15 +24,29 @@ DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2"
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def device1_config() -> dict[str, str]:
|
||||
"""Mock SoundTouch device 1 config."""
|
||||
yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_1_IP, CONF_NAME: DEVICE_1_NAME}
|
||||
def device1_config() -> MockConfigEntry:
|
||||
"""Mock SoundTouch device 1 config entry."""
|
||||
yield MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=DEVICE_1_ID,
|
||||
data={
|
||||
CONF_HOST: DEVICE_1_IP,
|
||||
CONF_NAME: "",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device2_config() -> dict[str, str]:
|
||||
"""Mock SoundTouch device 2 config."""
|
||||
yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_2_IP, CONF_NAME: DEVICE_2_NAME}
|
||||
def device2_config() -> MockConfigEntry:
|
||||
"""Mock SoundTouch device 2 config entry."""
|
||||
yield MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=DEVICE_2_ID,
|
||||
data={
|
||||
CONF_HOST: DEVICE_2_IP,
|
||||
CONF_NAME: "",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
"""Test config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from requests import RequestException
|
||||
from requests_mock import ANY, Mocker
|
||||
|
||||
from homeassistant.components.soundtouch.const import DOMAIN
|
||||
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .conftest import DEVICE_1_ID, DEVICE_1_IP, DEVICE_1_NAME
|
||||
|
||||
|
||||
async def test_user_flow_create_entry(
|
||||
hass: HomeAssistant, device1_requests_mock_standby: Mocker
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result.get("type") == FlowResultType.FORM
|
||||
assert result.get("step_id") == "user"
|
||||
assert "flow_id" in result
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.soundtouch.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_HOST: DEVICE_1_IP,
|
||||
},
|
||||
)
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
assert result.get("type") == FlowResultType.CREATE_ENTRY
|
||||
assert result.get("title") == DEVICE_1_NAME
|
||||
assert result.get("data") == {
|
||||
CONF_HOST: DEVICE_1_IP,
|
||||
}
|
||||
assert "result" in result
|
||||
assert result["result"].unique_id == DEVICE_1_ID
|
||||
assert result["result"].title == DEVICE_1_NAME
|
||||
|
||||
|
||||
async def test_user_flow_cannot_connect(
|
||||
hass: HomeAssistant, requests_mock: Mocker
|
||||
) -> None:
|
||||
"""Test a manual user flow with an invalid host."""
|
||||
requests_mock.get(ANY, exc=RequestException())
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_USER},
|
||||
data={
|
||||
CONF_HOST: "invalid-hostname",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_zeroconf_flow_create_entry(
|
||||
hass: HomeAssistant, device1_requests_mock_standby: Mocker
|
||||
) -> None:
|
||||
"""Test the zeroconf flow from start to finish."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_ZEROCONF},
|
||||
data=ZeroconfServiceInfo(
|
||||
host=DEVICE_1_IP,
|
||||
addresses=[DEVICE_1_IP],
|
||||
port=8090,
|
||||
hostname="Bose-SM2-060000000001.local.",
|
||||
type="_soundtouch._tcp.local.",
|
||||
name=f"{DEVICE_1_NAME}._soundtouch._tcp.local.",
|
||||
properties={
|
||||
"DESCRIPTION": "SoundTouch",
|
||||
"MAC": DEVICE_1_ID,
|
||||
"MANUFACTURER": "Bose Corporation",
|
||||
"MODEL": "SoundTouch",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert result.get("type") == FlowResultType.FORM
|
||||
assert result.get("step_id") == "zeroconf_confirm"
|
||||
assert result.get("description_placeholders") == {"name": DEVICE_1_NAME}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.soundtouch.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
assert result.get("type") == FlowResultType.CREATE_ENTRY
|
||||
assert result.get("title") == DEVICE_1_NAME
|
||||
assert result.get("data") == {
|
||||
CONF_HOST: DEVICE_1_IP,
|
||||
}
|
||||
assert "result" in result
|
||||
assert result["result"].unique_id == DEVICE_1_ID
|
||||
assert result["result"].title == DEVICE_1_NAME
|
|
@ -1,4 +1,5 @@
|
|||
"""Test the SoundTouch component."""
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from requests_mock import Mocker
|
||||
|
@ -25,22 +26,26 @@ from homeassistant.components.soundtouch.const import (
|
|||
from homeassistant.components.soundtouch.media_player import (
|
||||
ATTR_SOUNDTOUCH_GROUP,
|
||||
ATTR_SOUNDTOUCH_ZONE,
|
||||
DATA_SOUNDTOUCH,
|
||||
)
|
||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt
|
||||
|
||||
from .conftest import DEVICE_1_ENTITY_ID, DEVICE_2_ENTITY_ID
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
async def setup_soundtouch(hass: HomeAssistant, *configs: dict[str, str]):
|
||||
|
||||
async def setup_soundtouch(hass: HomeAssistant, *mock_entries: MockConfigEntry):
|
||||
"""Initialize media_player for tests."""
|
||||
assert await async_setup_component(
|
||||
hass, MEDIA_PLAYER_DOMAIN, {MEDIA_PLAYER_DOMAIN: list(configs)}
|
||||
)
|
||||
assert await async_setup_component(hass, MEDIA_PLAYER_DOMAIN, {})
|
||||
|
||||
for mock_entry in mock_entries:
|
||||
mock_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_start()
|
||||
|
||||
|
||||
async def _test_key_service(
|
||||
|
@ -59,7 +64,7 @@ async def _test_key_service(
|
|||
|
||||
async def test_playing_media(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
):
|
||||
"""Test playing media info."""
|
||||
|
@ -76,7 +81,7 @@ async def test_playing_media(
|
|||
|
||||
async def test_playing_radio(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_radio,
|
||||
):
|
||||
"""Test playing radio info."""
|
||||
|
@ -89,7 +94,7 @@ async def test_playing_radio(
|
|||
|
||||
async def test_playing_aux(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_aux,
|
||||
):
|
||||
"""Test playing AUX info."""
|
||||
|
@ -102,7 +107,7 @@ async def test_playing_aux(
|
|||
|
||||
async def test_playing_bluetooth(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_bluetooth,
|
||||
):
|
||||
"""Test playing Bluetooth info."""
|
||||
|
@ -118,7 +123,7 @@ async def test_playing_bluetooth(
|
|||
|
||||
async def test_get_volume_level(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
):
|
||||
"""Test volume level."""
|
||||
|
@ -130,7 +135,7 @@ async def test_get_volume_level(
|
|||
|
||||
async def test_get_state_off(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
):
|
||||
"""Test state device is off."""
|
||||
|
@ -142,7 +147,7 @@ async def test_get_state_off(
|
|||
|
||||
async def test_get_state_pause(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp_paused,
|
||||
):
|
||||
"""Test state device is paused."""
|
||||
|
@ -154,7 +159,7 @@ async def test_get_state_pause(
|
|||
|
||||
async def test_is_muted(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
device1_volume_muted: str,
|
||||
):
|
||||
|
@ -170,7 +175,7 @@ async def test_is_muted(
|
|||
|
||||
async def test_should_turn_off(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
device1_requests_mock_key,
|
||||
):
|
||||
|
@ -187,7 +192,7 @@ async def test_should_turn_off(
|
|||
|
||||
async def test_should_turn_on(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device1_requests_mock_key,
|
||||
):
|
||||
|
@ -204,7 +209,7 @@ async def test_should_turn_on(
|
|||
|
||||
async def test_volume_up(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
device1_requests_mock_key,
|
||||
):
|
||||
|
@ -221,7 +226,7 @@ async def test_volume_up(
|
|||
|
||||
async def test_volume_down(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
device1_requests_mock_key,
|
||||
):
|
||||
|
@ -238,7 +243,7 @@ async def test_volume_down(
|
|||
|
||||
async def test_set_volume_level(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
device1_requests_mock_volume,
|
||||
):
|
||||
|
@ -258,7 +263,7 @@ async def test_set_volume_level(
|
|||
|
||||
async def test_mute(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
device1_requests_mock_key,
|
||||
):
|
||||
|
@ -275,7 +280,7 @@ async def test_mute(
|
|||
|
||||
async def test_play(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp_paused,
|
||||
device1_requests_mock_key,
|
||||
):
|
||||
|
@ -292,7 +297,7 @@ async def test_play(
|
|||
|
||||
async def test_pause(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
device1_requests_mock_key,
|
||||
):
|
||||
|
@ -309,7 +314,7 @@ async def test_pause(
|
|||
|
||||
async def test_play_pause(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
device1_requests_mock_key,
|
||||
):
|
||||
|
@ -326,7 +331,7 @@ async def test_play_pause(
|
|||
|
||||
async def test_next_previous_track(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_upnp,
|
||||
device1_requests_mock_key,
|
||||
):
|
||||
|
@ -351,7 +356,7 @@ async def test_next_previous_track(
|
|||
|
||||
async def test_play_media(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device1_requests_mock_select,
|
||||
):
|
||||
|
@ -391,7 +396,7 @@ async def test_play_media(
|
|||
|
||||
async def test_play_media_url(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device1_requests_mock_dlna,
|
||||
):
|
||||
|
@ -415,7 +420,7 @@ async def test_play_media_url(
|
|||
|
||||
async def test_select_source_aux(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device1_requests_mock_select,
|
||||
):
|
||||
|
@ -435,7 +440,7 @@ async def test_select_source_aux(
|
|||
|
||||
async def test_select_source_bluetooth(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device1_requests_mock_select,
|
||||
):
|
||||
|
@ -455,7 +460,7 @@ async def test_select_source_bluetooth(
|
|||
|
||||
async def test_select_source_invalid_source(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device1_requests_mock_select,
|
||||
):
|
||||
|
@ -477,14 +482,25 @@ async def test_select_source_invalid_source(
|
|||
|
||||
async def test_play_everywhere(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device2_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device2_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device2_requests_mock_standby,
|
||||
device1_requests_mock_set_zone,
|
||||
):
|
||||
"""Test play everywhere."""
|
||||
await setup_soundtouch(hass, device1_config, device2_config)
|
||||
await setup_soundtouch(hass, device1_config)
|
||||
|
||||
# no slaves, set zone must not be called
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_PLAY_EVERYWHERE,
|
||||
{"master": DEVICE_1_ENTITY_ID},
|
||||
True,
|
||||
)
|
||||
assert device1_requests_mock_set_zone.call_count == 0
|
||||
|
||||
await setup_soundtouch(hass, device2_config)
|
||||
|
||||
# one master, one slave => set zone
|
||||
await hass.services.async_call(
|
||||
|
@ -504,27 +520,11 @@ async def test_play_everywhere(
|
|||
)
|
||||
assert device1_requests_mock_set_zone.call_count == 1
|
||||
|
||||
# remove second device
|
||||
for entity in list(hass.data[DATA_SOUNDTOUCH]):
|
||||
if entity.entity_id == DEVICE_1_ENTITY_ID:
|
||||
continue
|
||||
hass.data[DATA_SOUNDTOUCH].remove(entity)
|
||||
await entity.async_remove()
|
||||
|
||||
# no slaves, set zone must not be called
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_PLAY_EVERYWHERE,
|
||||
{"master": DEVICE_1_ENTITY_ID},
|
||||
True,
|
||||
)
|
||||
assert device1_requests_mock_set_zone.call_count == 1
|
||||
|
||||
|
||||
async def test_create_zone(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device2_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device2_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device2_requests_mock_standby,
|
||||
device1_requests_mock_set_zone,
|
||||
|
@ -567,8 +567,8 @@ async def test_create_zone(
|
|||
|
||||
async def test_remove_zone_slave(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device2_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device2_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device2_requests_mock_standby,
|
||||
device1_requests_mock_remove_zone_slave,
|
||||
|
@ -609,8 +609,8 @@ async def test_remove_zone_slave(
|
|||
|
||||
async def test_add_zone_slave(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device2_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device2_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device2_requests_mock_standby,
|
||||
device1_requests_mock_add_zone_slave,
|
||||
|
@ -651,14 +651,21 @@ async def test_add_zone_slave(
|
|||
|
||||
async def test_zone_attributes(
|
||||
hass: HomeAssistant,
|
||||
device1_config: dict[str, str],
|
||||
device2_config: dict[str, str],
|
||||
device1_config: MockConfigEntry,
|
||||
device2_config: MockConfigEntry,
|
||||
device1_requests_mock_standby,
|
||||
device2_requests_mock_standby,
|
||||
):
|
||||
"""Test zone attributes."""
|
||||
await setup_soundtouch(hass, device1_config, device2_config)
|
||||
|
||||
# Fast-forward time to allow all entities to be set up and updated again
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID)
|
||||
assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"]
|
||||
assert (
|
||||
|
|
Loading…
Reference in New Issue