mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
Squeezebox config flow (#35669)
* Squeezebox add config flow and player discovery * Fixes to config flow * Unavailable player detection and recovery * Improved error message for auth failure * Testing for squeezebox config flow * Import configuration.yaml * Support for discovery integration * Internal server discovery * Fix bug restoring previously detected squeezebox player * Tests for user and edit steps in config flow * Tests for import config flow * Additional config flow tests and fixes * Linter fixes * Check that players are found before iterating them * Remove noisy logger message * Update requirements_all after rebase * Use asyncio.Event in discovery task * Use common keys in strings.json * Bump pysqueezebox to v0.2.2 for fixed server discovery using python3.7 * Bump pysqueezebox version to v0.2.3 * Don't trap AbortFlow exception Co-authored-by: J. Nick Koston <nick@koston.org> * Refactor validate_input * Update squeezebox tests * Build data flow schema using function * Fix linter error * Updated en.json * Update homeassistant/components/squeezebox/media_player.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update homeassistant/components/squeezebox/media_player.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update homeassistant/components/squeezebox/media_player.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update homeassistant/components/squeezebox/media_player.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update .coveragerc for squeezebox config flow test * Mock TIMEOUT for faster testing * More schema de-duplication and testing improvements * Apply suggestions from code review Co-authored-by: J. Nick Koston <nick@koston.org> * Testing and config flow improvements * Remove unused exceptions * Remove deprecated logger message * Update homeassistant/components/squeezebox/media_player.py Co-authored-by: J. Nick Koston <nick@koston.org> * Implement suggestions from code review * Add async_unload_entry * Use MockConfigEntry in squeezebox tests * Remove unnecessary config schema * Stop server discovery task when last config entry unloaded * Improvements to async_unload_entry * Fix bug in _discovery arguments * Do not await server discovery in async_setup_entry * Do not await start server discovery in async_setup * Do not start server discovery from async_setup_entry until homeassistant running * Re-detect players when server removed and re-added without restart * Use entry.entry_id instead of unique_id * Update unittests to avoid patching homeassistant code Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
e25f216fd6
commit
3f427602ba
@ -757,7 +757,8 @@ omit =
|
|||||||
homeassistant/components/spotcrime/sensor.py
|
homeassistant/components/spotcrime/sensor.py
|
||||||
homeassistant/components/spotify/__init__.py
|
homeassistant/components/spotify/__init__.py
|
||||||
homeassistant/components/spotify/media_player.py
|
homeassistant/components/spotify/media_player.py
|
||||||
homeassistant/components/squeezebox/*
|
homeassistant/components/squeezebox/__init__.py
|
||||||
|
homeassistant/components/squeezebox/media_player.py
|
||||||
homeassistant/components/starline/*
|
homeassistant/components/starline/*
|
||||||
homeassistant/components/starlingbank/sensor.py
|
homeassistant/components/starlingbank/sensor.py
|
||||||
homeassistant/components/steam_online/sensor.py
|
homeassistant/components/steam_online/sensor.py
|
||||||
|
@ -47,6 +47,7 @@ SERVICE_XIAOMI_GW = "xiaomi_gw"
|
|||||||
CONFIG_ENTRY_HANDLERS = {
|
CONFIG_ENTRY_HANDLERS = {
|
||||||
SERVICE_DAIKIN: "daikin",
|
SERVICE_DAIKIN: "daikin",
|
||||||
SERVICE_TELLDUSLIVE: "tellduslive",
|
SERVICE_TELLDUSLIVE: "tellduslive",
|
||||||
|
"logitech_mediaserver": "squeezebox",
|
||||||
}
|
}
|
||||||
|
|
||||||
SERVICE_HANDLERS = {
|
SERVICE_HANDLERS = {
|
||||||
@ -64,7 +65,6 @@ SERVICE_HANDLERS = {
|
|||||||
SERVICE_FREEBOX: ("freebox", None),
|
SERVICE_FREEBOX: ("freebox", None),
|
||||||
SERVICE_YEELIGHT: ("yeelight", None),
|
SERVICE_YEELIGHT: ("yeelight", None),
|
||||||
"yamaha": ("media_player", "yamaha"),
|
"yamaha": ("media_player", "yamaha"),
|
||||||
"logitech_mediaserver": ("media_player", "squeezebox"),
|
|
||||||
"frontier_silicon": ("media_player", "frontier_silicon"),
|
"frontier_silicon": ("media_player", "frontier_silicon"),
|
||||||
"openhome": ("media_player", "openhome"),
|
"openhome": ("media_player", "openhome"),
|
||||||
"bose_soundtouch": ("media_player", "soundtouch"),
|
"bose_soundtouch": ("media_player", "soundtouch"),
|
||||||
|
@ -1 +1,86 @@
|
|||||||
"""The squeezebox component."""
|
"""The Logitech Squeezebox integration."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pysqueezebox import async_discover
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_DISCOVERY, ConfigEntry
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DOMAIN, ENTRY_PLAYERS, KNOWN_PLAYERS, PLAYER_DISCOVERY_UNSUB
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DISCOVERY_TASK = "discovery_task"
|
||||||
|
|
||||||
|
|
||||||
|
async def start_server_discovery(hass):
|
||||||
|
"""Start a server discovery task."""
|
||||||
|
|
||||||
|
def _discovered_server(server):
|
||||||
|
asyncio.create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_DISCOVERY},
|
||||||
|
data={
|
||||||
|
CONF_HOST: server.host,
|
||||||
|
CONF_PORT: int(server.port),
|
||||||
|
"uuid": server.uuid,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
if DISCOVERY_TASK not in hass.data[DOMAIN]:
|
||||||
|
_LOGGER.debug("Adding server discovery task for squeezebox")
|
||||||
|
hass.data[DOMAIN][DISCOVERY_TASK] = hass.async_create_task(
|
||||||
|
async_discover(_discovered_server)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
|
"""Set up the Logitech Squeezebox component."""
|
||||||
|
if hass.is_running:
|
||||||
|
asyncio.create_task(start_server_discovery(hass))
|
||||||
|
else:
|
||||||
|
hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_START, start_server_discovery(hass)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Set up Logitech Squeezebox from a config entry."""
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
# Stop player discovery task for this config entry.
|
||||||
|
hass.data[DOMAIN][entry.entry_id][PLAYER_DISCOVERY_UNSUB]()
|
||||||
|
|
||||||
|
# Remove config entry's players from list of known players
|
||||||
|
entry_players = hass.data[DOMAIN][entry.entry_id][ENTRY_PLAYERS]
|
||||||
|
if entry_players:
|
||||||
|
for player in entry_players:
|
||||||
|
_LOGGER.debug("Remove entry player %s from list of known players.", player)
|
||||||
|
hass.data[DOMAIN][KNOWN_PLAYERS].remove(player)
|
||||||
|
|
||||||
|
# Remove stored data for this config entry
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
# Stop server discovery task if this is the last config entry.
|
||||||
|
current_entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
if len(current_entries) == 1 and current_entries[0] == entry:
|
||||||
|
_LOGGER.debug("Stopping server discovery task")
|
||||||
|
hass.data[DOMAIN][DISCOVERY_TASK].cancel()
|
||||||
|
hass.data[DOMAIN].pop(DISCOVERY_TASK)
|
||||||
|
|
||||||
|
return await hass.config_entries.async_forward_entry_unload(entry, MP_DOMAIN)
|
||||||
|
189
homeassistant/components/squeezebox/config_flow.py
Normal file
189
homeassistant/components/squeezebox/config_flow.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
"""Config flow for Logitech Squeezebox integration."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pysqueezebox import Server, async_discover
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
HTTP_UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
# pylint: disable=unused-import
|
||||||
|
from .const import DEFAULT_PORT, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TIMEOUT = 5
|
||||||
|
|
||||||
|
|
||||||
|
def _base_schema(discovery_info=None):
|
||||||
|
"""Generate base schema."""
|
||||||
|
base_schema = {}
|
||||||
|
if discovery_info and CONF_HOST in discovery_info:
|
||||||
|
base_schema.update(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_HOST,
|
||||||
|
description={"suggested_value": discovery_info[CONF_HOST]},
|
||||||
|
): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
base_schema.update({vol.Required(CONF_HOST): str})
|
||||||
|
|
||||||
|
if discovery_info and CONF_PORT in discovery_info:
|
||||||
|
base_schema.update(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_PORT,
|
||||||
|
default=DEFAULT_PORT,
|
||||||
|
description={"suggested_value": discovery_info[CONF_PORT]},
|
||||||
|
): int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
base_schema.update({vol.Required(CONF_PORT, default=DEFAULT_PORT): int})
|
||||||
|
base_schema.update(
|
||||||
|
{vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str}
|
||||||
|
)
|
||||||
|
return vol.Schema(base_schema)
|
||||||
|
|
||||||
|
|
||||||
|
class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Logitech Squeezebox."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize an instance of the squeezebox config flow."""
|
||||||
|
self.data_schema = _base_schema()
|
||||||
|
self.discovery_info = None
|
||||||
|
|
||||||
|
async def _discover(self, uuid=None):
|
||||||
|
"""Discover an unconfigured LMS server."""
|
||||||
|
self.discovery_info = None
|
||||||
|
discovery_event = asyncio.Event()
|
||||||
|
|
||||||
|
def _discovery_callback(server):
|
||||||
|
if server.uuid:
|
||||||
|
# ignore already configured uuids
|
||||||
|
for entry in self._async_current_entries():
|
||||||
|
if entry.unique_id == server.uuid:
|
||||||
|
return
|
||||||
|
self.discovery_info = {
|
||||||
|
CONF_HOST: server.host,
|
||||||
|
CONF_PORT: server.port,
|
||||||
|
"uuid": server.uuid,
|
||||||
|
}
|
||||||
|
_LOGGER.debug("Discovered server: %s", self.discovery_info)
|
||||||
|
discovery_event.set()
|
||||||
|
|
||||||
|
discovery_task = self.hass.async_create_task(
|
||||||
|
async_discover(_discovery_callback)
|
||||||
|
)
|
||||||
|
|
||||||
|
await discovery_event.wait()
|
||||||
|
discovery_task.cancel() # stop searching as soon as we find server
|
||||||
|
|
||||||
|
# update with suggested values from discovery
|
||||||
|
self.data_schema = _base_schema(self.discovery_info)
|
||||||
|
|
||||||
|
async def _validate_input(self, data):
|
||||||
|
"""
|
||||||
|
Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Retrieve unique id and abort if already configured.
|
||||||
|
"""
|
||||||
|
server = Server(
|
||||||
|
async_get_clientsession(self.hass),
|
||||||
|
data[CONF_HOST],
|
||||||
|
data[CONF_PORT],
|
||||||
|
data.get(CONF_USERNAME),
|
||||||
|
data.get(CONF_PASSWORD),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
status = await server.async_query("serverstatus")
|
||||||
|
if not status:
|
||||||
|
if server.http_status == HTTP_UNAUTHORIZED:
|
||||||
|
return "invalid_auth"
|
||||||
|
return "cannot_connect"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
if "uuid" in status:
|
||||||
|
await self.async_set_unique_id(status["uuid"])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors = {}
|
||||||
|
if user_input and CONF_HOST in user_input:
|
||||||
|
# update with host provided by user
|
||||||
|
self.data_schema = _base_schema(user_input)
|
||||||
|
return await self.async_step_edit()
|
||||||
|
|
||||||
|
# no host specified, see if we can discover an unconfigured LMS server
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self._discover(), timeout=TIMEOUT)
|
||||||
|
return await self.async_step_edit()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
errors["base"] = "no_server_found"
|
||||||
|
|
||||||
|
# display the form
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({vol.Optional(CONF_HOST): str}),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_edit(self, user_input=None):
|
||||||
|
"""Edit a discovered or manually inputted server."""
|
||||||
|
errors = {}
|
||||||
|
if user_input:
|
||||||
|
error = await self._validate_input(user_input)
|
||||||
|
if error:
|
||||||
|
errors["base"] = error
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_HOST], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="edit", data_schema=self.data_schema, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, config):
|
||||||
|
"""Import a config flow from configuration."""
|
||||||
|
error = await self._validate_input(config)
|
||||||
|
if error:
|
||||||
|
return self.async_abort(reason=error)
|
||||||
|
return self.async_create_entry(title=config[CONF_HOST], data=config)
|
||||||
|
|
||||||
|
async def async_step_discovery(self, discovery_info):
|
||||||
|
"""Handle discovery."""
|
||||||
|
_LOGGER.debug("Reached discovery flow with info: %s", discovery_info)
|
||||||
|
if "uuid" in discovery_info:
|
||||||
|
await self.async_set_unique_id(discovery_info.pop("uuid"))
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
else:
|
||||||
|
# attempt to connect to server and determine uuid. will fail if password required
|
||||||
|
error = await self._validate_input(discovery_info)
|
||||||
|
if error:
|
||||||
|
await self._async_handle_discovery_without_unique_id()
|
||||||
|
|
||||||
|
# update schema with suggested values from discovery
|
||||||
|
self.data_schema = _base_schema(discovery_info)
|
||||||
|
|
||||||
|
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||||
|
self.context.update({"title_placeholders": {"host": discovery_info[CONF_HOST]}})
|
||||||
|
|
||||||
|
return await self.async_step_edit()
|
@ -1,10 +1,6 @@
|
|||||||
"""Constants for the Squeezebox component."""
|
"""Constants for the Squeezebox component."""
|
||||||
from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING
|
|
||||||
|
|
||||||
DOMAIN = "squeezebox"
|
DOMAIN = "squeezebox"
|
||||||
SERVICE_CALL_METHOD = "call_method"
|
ENTRY_PLAYERS = "entry_players"
|
||||||
SQUEEZEBOX_MODE = {
|
KNOWN_PLAYERS = "known_players"
|
||||||
"pause": STATE_PAUSED,
|
PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub"
|
||||||
"play": STATE_PLAYING,
|
DEFAULT_PORT = 9000
|
||||||
"stop": STATE_IDLE,
|
|
||||||
}
|
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
"domain": "squeezebox",
|
"domain": "squeezebox",
|
||||||
"name": "Logitech Squeezebox",
|
"name": "Logitech Squeezebox",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/squeezebox",
|
"documentation": "https://www.home-assistant.io/integrations/squeezebox",
|
||||||
"codeowners": ["@rajlaud"],
|
"codeowners": [
|
||||||
"requirements": ["pysqueezebox==0.2.1"]
|
"@rajlaud"
|
||||||
|
],
|
||||||
|
"requirements": [
|
||||||
|
"pysqueezebox==0.2.4"
|
||||||
|
],
|
||||||
|
"config_flow": true
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
"""Support for interfacing to the Logitech SqueezeBox API."""
|
"""Support for interfacing to the Logitech SqueezeBox API."""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import socket
|
|
||||||
|
|
||||||
from pysqueezebox import Server
|
from pysqueezebox import Server
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
|
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
ATTR_MEDIA_ENQUEUE,
|
ATTR_MEDIA_ENQUEUE,
|
||||||
@ -28,14 +29,25 @@ from homeassistant.const import (
|
|||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
|
EVENT_HOMEASSISTANT_START,
|
||||||
|
STATE_IDLE,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
|
STATE_PAUSED,
|
||||||
|
STATE_PLAYING,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from .const import SQUEEZEBOX_MODE
|
from .__init__ import start_server_discovery
|
||||||
|
from .const import (
|
||||||
|
DEFAULT_PORT,
|
||||||
|
DOMAIN,
|
||||||
|
ENTRY_PLAYERS,
|
||||||
|
KNOWN_PLAYERS,
|
||||||
|
PLAYER_DISCOVERY_UNSUB,
|
||||||
|
)
|
||||||
|
|
||||||
SERVICE_CALL_METHOD = "call_method"
|
SERVICE_CALL_METHOD = "call_method"
|
||||||
SERVICE_CALL_QUERY = "call_query"
|
SERVICE_CALL_QUERY = "call_query"
|
||||||
@ -47,8 +59,7 @@ ATTR_SYNC_GROUP = "sync_group"
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_PORT = 9000
|
DISCOVERY_INTERVAL = 60
|
||||||
TIMEOUT = 10
|
|
||||||
|
|
||||||
SUPPORT_SQUEEZEBOX = (
|
SUPPORT_SQUEEZEBOX = (
|
||||||
SUPPORT_PAUSE
|
SUPPORT_PAUSE
|
||||||
@ -65,21 +76,23 @@ SUPPORT_SQUEEZEBOX = (
|
|||||||
| SUPPORT_CLEAR_PLAYLIST
|
| SUPPORT_CLEAR_PLAYLIST
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = vol.All(
|
||||||
|
cv.deprecated(CONF_HOST),
|
||||||
|
cv.deprecated(CONF_PORT),
|
||||||
|
cv.deprecated(CONF_PASSWORD),
|
||||||
|
cv.deprecated(CONF_USERNAME),
|
||||||
|
PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD): cv.string,
|
vol.Optional(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.Optional(CONF_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
}
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
DATA_SQUEEZEBOX = "squeezebox"
|
KNOWN_SERVERS = "known_servers"
|
||||||
|
|
||||||
KNOWN_SERVERS = "squeezebox_known_servers"
|
|
||||||
|
|
||||||
ATTR_PARAMETERS = "parameters"
|
ATTR_PARAMETERS = "parameters"
|
||||||
|
|
||||||
ATTR_OTHER_PLAYER = "other_player"
|
ATTR_OTHER_PLAYER = "other_player"
|
||||||
|
|
||||||
ATTR_TO_PROPERTY = [
|
ATTR_TO_PROPERTY = [
|
||||||
@ -87,57 +100,84 @@ ATTR_TO_PROPERTY = [
|
|||||||
ATTR_SYNC_GROUP,
|
ATTR_SYNC_GROUP,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SQUEEZEBOX_MODE = {
|
||||||
|
"pause": STATE_PAUSED,
|
||||||
|
"play": STATE_PLAYING,
|
||||||
|
"stop": STATE_IDLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the squeezebox platform."""
|
"""Set up squeezebox platform from platform entry in configuration.yaml (deprecated)."""
|
||||||
|
|
||||||
known_servers = hass.data.get(KNOWN_SERVERS)
|
if config:
|
||||||
if known_servers is None:
|
await hass.config_entries.flow.async_init(
|
||||||
hass.data[KNOWN_SERVERS] = known_servers = set()
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
|
||||||
|
)
|
||||||
|
|
||||||
if DATA_SQUEEZEBOX not in hass.data:
|
|
||||||
hass.data[DATA_SQUEEZEBOX] = []
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up an LMS Server from a config entry."""
|
||||||
|
config = config_entry.data
|
||||||
|
_LOGGER.debug("Reached async_setup_entry for host=%s", config[CONF_HOST])
|
||||||
|
|
||||||
username = config.get(CONF_USERNAME)
|
username = config.get(CONF_USERNAME)
|
||||||
password = config.get(CONF_PASSWORD)
|
password = config.get(CONF_PASSWORD)
|
||||||
|
host = config[CONF_HOST]
|
||||||
|
port = config[CONF_PORT]
|
||||||
|
|
||||||
if discovery_info is not None:
|
hass.data.setdefault(DOMAIN, {})
|
||||||
host = discovery_info.get("host")
|
hass.data[DOMAIN].setdefault(config_entry.entry_id, {})
|
||||||
port = discovery_info.get("port")
|
|
||||||
else:
|
|
||||||
host = config.get(CONF_HOST)
|
|
||||||
port = config.get(CONF_PORT)
|
|
||||||
|
|
||||||
# In case the port is not discovered
|
known_players = hass.data[DOMAIN].get(KNOWN_PLAYERS)
|
||||||
if port is None:
|
if known_players is None:
|
||||||
port = DEFAULT_PORT
|
hass.data[DOMAIN][KNOWN_PLAYERS] = known_players = []
|
||||||
|
|
||||||
# Get IP of host, to prevent duplication of same host (different DNS names)
|
entry_players = hass.data[DOMAIN][config_entry.entry_id].setdefault(
|
||||||
try:
|
ENTRY_PLAYERS, []
|
||||||
ipaddr = await hass.async_add_executor_job(socket.gethostbyname, host)
|
)
|
||||||
except OSError as error:
|
|
||||||
_LOGGER.error("Could not communicate with %s:%d: %s", host, port, error)
|
|
||||||
raise PlatformNotReady from error
|
|
||||||
|
|
||||||
if ipaddr in known_servers:
|
_LOGGER.debug("Creating LMS object for %s", host)
|
||||||
return
|
|
||||||
|
|
||||||
_LOGGER.debug("Creating LMS object for %s", ipaddr)
|
|
||||||
lms = Server(async_get_clientsession(hass), host, port, username, password)
|
lms = Server(async_get_clientsession(hass), host, port, username, password)
|
||||||
known_servers.add(ipaddr)
|
|
||||||
|
async def _discovery(now=None):
|
||||||
|
"""Discover squeezebox players by polling server."""
|
||||||
|
|
||||||
|
async def _discovered_player(player):
|
||||||
|
"""Handle a (re)discovered player."""
|
||||||
|
entity = next(
|
||||||
|
(
|
||||||
|
known
|
||||||
|
for known in known_players
|
||||||
|
if known.unique_id == player.player_id
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if entity and not entity.available:
|
||||||
|
# check if previously unavailable player has connected
|
||||||
|
await player.async_update()
|
||||||
|
entity.available = player.connected
|
||||||
|
if not entity:
|
||||||
|
_LOGGER.debug("Adding new entity: %s", player)
|
||||||
|
entity = SqueezeBoxEntity(player)
|
||||||
|
known_players.append(entity)
|
||||||
|
entry_players.append(entity)
|
||||||
|
async_add_entities([entity])
|
||||||
|
|
||||||
players = await lms.async_get_players()
|
players = await lms.async_get_players()
|
||||||
if players is None:
|
if players:
|
||||||
raise PlatformNotReady
|
|
||||||
media_players = []
|
|
||||||
for player in players:
|
for player in players:
|
||||||
media_players.append(SqueezeBoxDevice(player))
|
hass.async_create_task(_discovered_player(player))
|
||||||
|
|
||||||
hass.data[DATA_SQUEEZEBOX].extend(media_players)
|
hass.data[DOMAIN][config_entry.entry_id][
|
||||||
async_add_entities(media_players)
|
PLAYER_DISCOVERY_UNSUB
|
||||||
|
] = hass.helpers.event.async_call_later(DISCOVERY_INTERVAL, _discovery)
|
||||||
|
|
||||||
|
_LOGGER.debug("Adding player discovery job for LMS server: %s", host)
|
||||||
|
asyncio.create_task(_discovery())
|
||||||
|
|
||||||
|
# Register entity services
|
||||||
platform = entity_platform.current_platform.get()
|
platform = entity_platform.current_platform.get()
|
||||||
|
|
||||||
platform.async_register_entity_service(
|
platform.async_register_entity_service(
|
||||||
SERVICE_CALL_METHOD,
|
SERVICE_CALL_METHOD,
|
||||||
{
|
{
|
||||||
@ -148,7 +188,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
},
|
},
|
||||||
"async_call_method",
|
"async_call_method",
|
||||||
)
|
)
|
||||||
|
|
||||||
platform.async_register_entity_service(
|
platform.async_register_entity_service(
|
||||||
SERVICE_CALL_QUERY,
|
SERVICE_CALL_QUERY,
|
||||||
{
|
{
|
||||||
@ -159,17 +198,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
},
|
},
|
||||||
"async_call_query",
|
"async_call_query",
|
||||||
)
|
)
|
||||||
|
|
||||||
platform.async_register_entity_service(
|
platform.async_register_entity_service(
|
||||||
SERVICE_SYNC, {vol.Required(ATTR_OTHER_PLAYER): cv.string}, "async_sync",
|
SERVICE_SYNC, {vol.Required(ATTR_OTHER_PLAYER): cv.string}, "async_sync",
|
||||||
)
|
)
|
||||||
|
|
||||||
platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync")
|
platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync")
|
||||||
|
|
||||||
|
# Start server discovery task if not already running
|
||||||
|
if hass.is_running:
|
||||||
|
asyncio.create_task(start_server_discovery(hass))
|
||||||
|
else:
|
||||||
|
hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_START, start_server_discovery(hass)
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class SqueezeBoxDevice(MediaPlayerEntity):
|
class SqueezeBoxEntity(MediaPlayerEntity):
|
||||||
"""
|
"""
|
||||||
Representation of a SqueezeBox device.
|
Representation of a SqueezeBox device.
|
||||||
|
|
||||||
@ -181,6 +226,7 @@ class SqueezeBoxDevice(MediaPlayerEntity):
|
|||||||
self._player = player
|
self._player = player
|
||||||
self._last_update = None
|
self._last_update = None
|
||||||
self._query_result = {}
|
self._query_result = {}
|
||||||
|
self._available = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
@ -203,10 +249,22 @@ class SqueezeBoxDevice(MediaPlayerEntity):
|
|||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
return self._player.player_id
|
return self._player.player_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if device connected to LMS server."""
|
||||||
|
return self._available
|
||||||
|
|
||||||
|
@available.setter
|
||||||
|
def available(self, val):
|
||||||
|
"""Set available to True or False."""
|
||||||
|
self._available = bool(val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if self._player.power is not None and not self._player.power:
|
if not self.available:
|
||||||
|
return STATE_UNAVAILABLE
|
||||||
|
if not self._player.power:
|
||||||
return STATE_OFF
|
return STATE_OFF
|
||||||
if self._player.mode:
|
if self._player.mode:
|
||||||
return SQUEEZEBOX_MODE.get(self._player.mode)
|
return SQUEEZEBOX_MODE.get(self._player.mode)
|
||||||
@ -214,13 +272,15 @@ class SqueezeBoxDevice(MediaPlayerEntity):
|
|||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Update the Player() object."""
|
"""Update the Player() object."""
|
||||||
|
# only update available players, newly available players will be rediscovered and marked available
|
||||||
|
if self._available:
|
||||||
last_media_position = self.media_position
|
last_media_position = self.media_position
|
||||||
await self._player.async_update()
|
await self._player.async_update()
|
||||||
if self.media_position != last_media_position:
|
if self.media_position != last_media_position:
|
||||||
_LOGGER.debug(
|
|
||||||
"Media position updated for %s: %s", self, self.media_position
|
|
||||||
)
|
|
||||||
self._last_update = utcnow()
|
self._last_update = utcnow()
|
||||||
|
if self._player.connected is False:
|
||||||
|
_LOGGER.info("Player %s is not available", self.name)
|
||||||
|
self._available = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
@ -291,7 +351,9 @@ class SqueezeBoxDevice(MediaPlayerEntity):
|
|||||||
@property
|
@property
|
||||||
def sync_group(self):
|
def sync_group(self):
|
||||||
"""List players we are synced with."""
|
"""List players we are synced with."""
|
||||||
player_ids = {p.unique_id: p.entity_id for p in self.hass.data[DATA_SQUEEZEBOX]}
|
player_ids = {
|
||||||
|
p.unique_id: p.entity_id for p in self.hass.data[DOMAIN][KNOWN_PLAYERS]
|
||||||
|
}
|
||||||
sync_group = []
|
sync_group = []
|
||||||
for player in self._player.sync_group:
|
for player in self._player.sync_group:
|
||||||
if player in player_ids:
|
if player in player_ids:
|
||||||
@ -407,7 +469,9 @@ class SqueezeBoxDevice(MediaPlayerEntity):
|
|||||||
If the other player is a member of a sync group, it will leave the current sync group
|
If the other player is a member of a sync group, it will leave the current sync group
|
||||||
without asking.
|
without asking.
|
||||||
"""
|
"""
|
||||||
player_ids = {p.entity_id: p.unique_id for p in self.hass.data[DATA_SQUEEZEBOX]}
|
player_ids = {
|
||||||
|
p.entity_id: p.unique_id for p in self.hass.data[DOMAIN][KNOWN_PLAYERS]
|
||||||
|
}
|
||||||
other_player_id = player_ids.get(other_player)
|
other_player_id = player_ids.get(other_player)
|
||||||
if other_player_id:
|
if other_player_id:
|
||||||
await self._player.async_sync(other_player_id)
|
await self._player.async_sync(other_player_id)
|
||||||
|
33
homeassistant/components/squeezebox/strings.json
Normal file
33
homeassistant/components/squeezebox/strings.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"title": "Logitech Squeezebox",
|
||||||
|
"config": {
|
||||||
|
"flow_title": "Logitech Squeezebox: {host}",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Configure Logitech Media Server",
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "Edit connection information",
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||||
|
"no_server_found": "Could not automatically discover server."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"no_server_found": "No LMS server found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
homeassistant/components/squeezebox/translations/en.json
Normal file
33
homeassistant/components/squeezebox/translations/en.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "Logitech Squeezebox: {host}",
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured",
|
||||||
|
"no_server_found": "No LMS server found."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"no_server_found": "Could not automatically discover server.",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"edit": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"password": "Password",
|
||||||
|
"port": "Port",
|
||||||
|
"username": "Username"
|
||||||
|
},
|
||||||
|
"title": "Edit connection information"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host"
|
||||||
|
},
|
||||||
|
"title": "Configure Logitech Media Server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Logitech Squeezebox"
|
||||||
|
}
|
@ -148,6 +148,7 @@ FLOWS = [
|
|||||||
"sonos",
|
"sonos",
|
||||||
"speedtestdotnet",
|
"speedtestdotnet",
|
||||||
"spotify",
|
"spotify",
|
||||||
|
"squeezebox",
|
||||||
"starline",
|
"starline",
|
||||||
"synology_dsm",
|
"synology_dsm",
|
||||||
"tado",
|
"tado",
|
||||||
|
@ -1643,7 +1643,7 @@ pysonos==0.0.31
|
|||||||
pyspcwebgw==0.4.0
|
pyspcwebgw==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.squeezebox
|
# homeassistant.components.squeezebox
|
||||||
pysqueezebox==0.2.1
|
pysqueezebox==0.2.4
|
||||||
|
|
||||||
# homeassistant.components.stiebel_eltron
|
# homeassistant.components.stiebel_eltron
|
||||||
pystiebeleltron==0.0.1.dev2
|
pystiebeleltron==0.0.1.dev2
|
||||||
|
@ -729,6 +729,9 @@ pysonos==0.0.31
|
|||||||
# homeassistant.components.spc
|
# homeassistant.components.spc
|
||||||
pyspcwebgw==0.4.0
|
pyspcwebgw==0.4.0
|
||||||
|
|
||||||
|
# homeassistant.components.squeezebox
|
||||||
|
pysqueezebox==0.2.4
|
||||||
|
|
||||||
# homeassistant.components.ecobee
|
# homeassistant.components.ecobee
|
||||||
python-ecobee-api==0.2.5
|
python-ecobee-api==0.2.5
|
||||||
|
|
||||||
|
1
tests/components/squeezebox/__init__.py
Normal file
1
tests/components/squeezebox/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Logitech Squeezebox integration."""
|
263
tests/components/squeezebox/test_config_flow.py
Normal file
263
tests/components/squeezebox/test_config_flow.py
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
"""Test the Logitech Squeezebox config flow."""
|
||||||
|
from asynctest import patch
|
||||||
|
from pysqueezebox import Server
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.squeezebox.const import DOMAIN
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
HTTP_UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
from homeassistant.data_entry_flow import (
|
||||||
|
RESULT_TYPE_ABORT,
|
||||||
|
RESULT_TYPE_CREATE_ENTRY,
|
||||||
|
RESULT_TYPE_FORM,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
HOST = "1.1.1.1"
|
||||||
|
HOST2 = "2.2.2.2"
|
||||||
|
PORT = 9000
|
||||||
|
UUID = "test-uuid"
|
||||||
|
UNKNOWN_ERROR = "1234"
|
||||||
|
|
||||||
|
|
||||||
|
async def mock_discover(_discovery_callback):
|
||||||
|
"""Mock discovering a Logitech Media Server."""
|
||||||
|
_discovery_callback(Server(None, HOST, PORT, uuid=UUID))
|
||||||
|
|
||||||
|
|
||||||
|
async def mock_failed_discover(_discovery_callback):
|
||||||
|
"""Mock unsuccessful discovery by doing nothing."""
|
||||||
|
|
||||||
|
|
||||||
|
async def patch_async_query_unauthorized(self, *args):
|
||||||
|
"""Mock an unauthorized query."""
|
||||||
|
self.http_status = HTTP_UNAUTHORIZED
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form(hass):
|
||||||
|
"""Test user-initiated flow, including discovery and the edit step."""
|
||||||
|
with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch(
|
||||||
|
"homeassistant.components.squeezebox.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.squeezebox.async_setup_entry", return_value=True,
|
||||||
|
) as mock_setup_entry, patch(
|
||||||
|
"homeassistant.components.squeezebox.config_flow.async_discover", mock_discover
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "edit"
|
||||||
|
assert CONF_HOST in result["data_schema"].schema
|
||||||
|
for key in result["data_schema"].schema:
|
||||||
|
if key == CONF_HOST:
|
||||||
|
assert key.description == {"suggested_value": HOST}
|
||||||
|
|
||||||
|
# test the edit step
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_HOST: HOST, CONF_PORT: PORT, CONF_USERNAME: "", CONF_PASSWORD: ""},
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == HOST
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_USERNAME: "",
|
||||||
|
CONF_PASSWORD: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form_timeout(hass):
|
||||||
|
"""Test we handle server search timeout."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.squeezebox.config_flow.async_discover",
|
||||||
|
mock_failed_discover,
|
||||||
|
), patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "no_server_found"}
|
||||||
|
|
||||||
|
# simulate manual input of host
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], {CONF_HOST: HOST2}
|
||||||
|
)
|
||||||
|
assert result2["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result2["step_id"] == "edit"
|
||||||
|
assert CONF_HOST in result2["data_schema"].schema
|
||||||
|
for key in result2["data_schema"].schema:
|
||||||
|
if key == CONF_HOST:
|
||||||
|
assert key.description == {"suggested_value": HOST2}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form_duplicate(hass):
|
||||||
|
"""Test duplicate discovered servers are skipped."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.squeezebox.config_flow.async_discover", mock_discover,
|
||||||
|
), patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1), patch(
|
||||||
|
"homeassistant.components.squeezebox.async_setup", return_value=True
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.squeezebox.async_setup_entry", return_value=True,
|
||||||
|
):
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, unique_id=UUID)
|
||||||
|
await hass.config_entries.async_add(entry)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "no_server_found"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_invalid_auth(hass):
|
||||||
|
"""Test we handle invalid auth."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "edit"}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def patch_async_query(self, *args):
|
||||||
|
self.http_status = HTTP_UNAUTHORIZED
|
||||||
|
return False
|
||||||
|
|
||||||
|
with patch("pysqueezebox.Server.async_query", new=patch_async_query):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_USERNAME: "test-username",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "edit"}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"pysqueezebox.Server.async_query", return_value=False,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_USERNAME: "test-username",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery(hass):
|
||||||
|
"""Test handling of discovered server."""
|
||||||
|
with patch(
|
||||||
|
"pysqueezebox.Server.async_query", return_value={"uuid": UUID},
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_DISCOVERY},
|
||||||
|
data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID},
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "edit"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery_no_uuid(hass):
|
||||||
|
"""Test handling of discovered server with unavailable uuid."""
|
||||||
|
with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_DISCOVERY},
|
||||||
|
data={CONF_HOST: HOST, CONF_PORT: PORT},
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "edit"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import(hass):
|
||||||
|
"""Test handling of configuration imported."""
|
||||||
|
with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch(
|
||||||
|
"homeassistant.components.squeezebox.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.squeezebox.async_setup_entry", return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={CONF_HOST: HOST, CONF_PORT: PORT},
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_bad_host(hass):
|
||||||
|
"""Test handling of configuration imported with bad host."""
|
||||||
|
with patch("pysqueezebox.Server.async_query", return_value=False):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={CONF_HOST: HOST, CONF_PORT: PORT},
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "cannot_connect"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_bad_auth(hass):
|
||||||
|
"""Test handling of configuration import with bad authentication."""
|
||||||
|
with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
CONF_HOST: HOST,
|
||||||
|
CONF_PORT: PORT,
|
||||||
|
CONF_USERNAME: "test",
|
||||||
|
CONF_PASSWORD: "bad",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "invalid_auth"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_existing(hass):
|
||||||
|
"""Test handling of configuration import of existing server."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.squeezebox.async_setup", return_value=True
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.squeezebox.async_setup_entry", return_value=True,
|
||||||
|
), patch(
|
||||||
|
"pysqueezebox.Server.async_query", return_value={"ip": HOST, "uuid": UUID},
|
||||||
|
):
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, unique_id=UUID)
|
||||||
|
await hass.config_entries.async_add(entry)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={CONF_HOST: HOST, CONF_PORT: PORT},
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
Loading…
x
Reference in New Issue
Block a user