mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Add config flow to Volumio (#38252)
This commit is contained in:
@ -926,6 +926,7 @@ omit =
|
||||
homeassistant/components/vlc/media_player.py
|
||||
homeassistant/components/vlc_telnet/media_player.py
|
||||
homeassistant/components/volkszaehler/sensor.py
|
||||
homeassistant/components/volumio/__init__.py
|
||||
homeassistant/components/volumio/media_player.py
|
||||
homeassistant/components/volvooncall/*
|
||||
homeassistant/components/w800rf32/*
|
||||
|
@ -454,6 +454,7 @@ homeassistant/components/vilfo/* @ManneW
|
||||
homeassistant/components/vivotek/* @HarlemSquirrel
|
||||
homeassistant/components/vizio/* @raman325
|
||||
homeassistant/components/vlc_telnet/* @rodripf
|
||||
homeassistant/components/volumio/* @OnFreund
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/weather/* @fabaff
|
||||
|
@ -71,7 +71,6 @@ SERVICE_HANDLERS = {
|
||||
"bose_soundtouch": ("media_player", "soundtouch"),
|
||||
"bluesound": ("media_player", "bluesound"),
|
||||
"kodi": ("media_player", "kodi"),
|
||||
"volumio": ("media_player", "volumio"),
|
||||
"lg_smart_device": ("media_player", "lg_soundbar"),
|
||||
"nanoleaf_aurora": ("light", "nanoleaf"),
|
||||
}
|
||||
@ -93,6 +92,7 @@ MIGRATED_SERVICE_HANDLERS = [
|
||||
"songpal",
|
||||
SERVICE_WEMO,
|
||||
SERVICE_XIAOMI_GW,
|
||||
"volumio",
|
||||
]
|
||||
|
||||
DEFAULT_ENABLED = (
|
||||
|
@ -1 +1,59 @@
|
||||
"""The volumio component."""
|
||||
"""The Volumio integration."""
|
||||
import asyncio
|
||||
|
||||
from pyvolumio import CannotConnectError, Volumio
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DATA_INFO, DATA_VOLUMIO, DOMAIN
|
||||
|
||||
PLATFORMS = ["media_player"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the Volumio component."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Volumio from a config entry."""
|
||||
|
||||
volumio = Volumio(
|
||||
entry.data[CONF_HOST], entry.data[CONF_PORT], async_get_clientsession(hass)
|
||||
)
|
||||
try:
|
||||
info = await volumio.get_system_version()
|
||||
except CannotConnectError as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
DATA_VOLUMIO: volumio,
|
||||
DATA_INFO: info,
|
||||
}
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
122
homeassistant/components/volumio/config_flow.py
Normal file
122
homeassistant/components/volumio/config_flow.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""Config flow for Volumio integration."""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from pyvolumio import CannotConnectError, Volumio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, exceptions
|
||||
from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_HOST): str, vol.Required(CONF_PORT, default=3000): int}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass, host, port):
|
||||
"""Validate the user input allows us to connect."""
|
||||
volumio = Volumio(host, port, async_get_clientsession(hass))
|
||||
|
||||
try:
|
||||
return await volumio.get_system_info()
|
||||
except CannotConnectError as error:
|
||||
raise CannotConnect from error
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Volumio."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize flow."""
|
||||
self._host: Optional[str] = None
|
||||
self._port: Optional[int] = None
|
||||
self._name: Optional[str] = None
|
||||
self._uuid: Optional[str] = None
|
||||
|
||||
@callback
|
||||
def _async_get_entry(self):
|
||||
return self.async_create_entry(
|
||||
title=self._name,
|
||||
data={
|
||||
CONF_NAME: self._name,
|
||||
CONF_HOST: self._host,
|
||||
CONF_PORT: self._port,
|
||||
CONF_ID: self._uuid,
|
||||
},
|
||||
)
|
||||
|
||||
async def _set_uid_and_abort(self):
|
||||
await self.async_set_unique_id(self._uuid)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: self._host,
|
||||
CONF_PORT: self._port,
|
||||
CONF_NAME: self._name,
|
||||
}
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
info = None
|
||||
try:
|
||||
self._host = user_input[CONF_HOST]
|
||||
self._port = user_input[CONF_PORT]
|
||||
info = await validate_input(self.hass, self._host, self._port)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if info is not None:
|
||||
self._name = info.get("name", self._host)
|
||||
self._uuid = info.get("id", None)
|
||||
if self._uuid is not None:
|
||||
await self._set_uid_and_abort()
|
||||
|
||||
return self._async_get_entry()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
|
||||
"""Handle zeroconf discovery."""
|
||||
self._host = discovery_info["host"]
|
||||
self._port = int(discovery_info["port"])
|
||||
self._name = discovery_info["properties"]["volumioName"]
|
||||
self._uuid = discovery_info["properties"]["UUID"]
|
||||
|
||||
await self._set_uid_and_abort()
|
||||
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(self, user_input=None):
|
||||
"""Handle user-confirmation of discovered node."""
|
||||
if user_input is not None:
|
||||
try:
|
||||
await validate_input(self.hass, self._host, self._port)
|
||||
return self._async_get_entry()
|
||||
except CannotConnect:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm", description_placeholders={"name": self._name}
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
6
homeassistant/components/volumio/const.py
Normal file
6
homeassistant/components/volumio/const.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Constants for the Volumio integration."""
|
||||
|
||||
DOMAIN = "volumio"
|
||||
|
||||
DATA_INFO = "info"
|
||||
DATA_VOLUMIO = "volumio"
|
@ -2,5 +2,8 @@
|
||||
"domain": "volumio",
|
||||
"name": "Volumio",
|
||||
"documentation": "https://www.home-assistant.io/integrations/volumio",
|
||||
"codeowners": []
|
||||
}
|
||||
"codeowners": ["@OnFreund"],
|
||||
"config_flow": true,
|
||||
"zeroconf": ["_Volumio._tcp.local."],
|
||||
"requirements": ["pyvolumio==0.1"]
|
||||
}
|
@ -3,15 +3,10 @@ Volumio Platform.
|
||||
|
||||
Volumio rest API: https://volumio.github.io/docs/API/REST_API.html
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
|
||||
from homeassistant.components.media_player import MediaPlayerEntity
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_TYPE_MUSIC,
|
||||
SUPPORT_CLEAR_PLAYLIST,
|
||||
@ -28,29 +23,19 @@ from homeassistant.components.media_player.const import (
|
||||
SUPPORT_VOLUME_STEP,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
HTTP_OK,
|
||||
STATE_IDLE,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import DATA_INFO, DATA_VOLUMIO, DOMAIN
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_NAME = "Volumio"
|
||||
DEFAULT_PORT = 3000
|
||||
|
||||
DATA_VOLUMIO = "volumio"
|
||||
|
||||
TIMEOUT = 10
|
||||
|
||||
SUPPORT_VOLUMIO = (
|
||||
SUPPORT_PAUSE
|
||||
| SUPPORT_VOLUME_SET
|
||||
@ -68,91 +53,59 @@ SUPPORT_VOLUMIO = (
|
||||
|
||||
PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
}
|
||||
)
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Volumio media player platform."""
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the Volumio platform."""
|
||||
if DATA_VOLUMIO not in hass.data:
|
||||
hass.data[DATA_VOLUMIO] = {}
|
||||
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
volumio = data[DATA_VOLUMIO]
|
||||
info = data[DATA_INFO]
|
||||
uid = config_entry.data[CONF_ID]
|
||||
name = config_entry.data[CONF_NAME]
|
||||
|
||||
# This is a manual configuration?
|
||||
if discovery_info is None:
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
else:
|
||||
name = "{} ({})".format(DEFAULT_NAME, discovery_info.get("hostname"))
|
||||
host = discovery_info.get("host")
|
||||
port = discovery_info.get("port")
|
||||
|
||||
# Only add a device once, so discovered devices do not override manual
|
||||
# config.
|
||||
ip_addr = socket.gethostbyname(host)
|
||||
if ip_addr in hass.data[DATA_VOLUMIO]:
|
||||
return
|
||||
|
||||
entity = Volumio(name, host, port, hass)
|
||||
|
||||
hass.data[DATA_VOLUMIO][ip_addr] = entity
|
||||
entity = Volumio(hass, volumio, uid, name, info)
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
class Volumio(MediaPlayerEntity):
|
||||
"""Volumio Player Object."""
|
||||
|
||||
def __init__(self, name, host, port, hass):
|
||||
def __init__(self, hass, volumio, uid, name, info):
|
||||
"""Initialize the media player."""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.hass = hass
|
||||
self._url = "{}:{}".format(host, str(port))
|
||||
self._hass = hass
|
||||
self._volumio = volumio
|
||||
self._uid = uid
|
||||
self._name = name
|
||||
self._info = info
|
||||
self._state = {}
|
||||
self._lastvol = self._state.get("volume", 0)
|
||||
self._playlists = []
|
||||
self._currentplaylist = None
|
||||
|
||||
async def send_volumio_msg(self, method, params=None):
|
||||
"""Send message."""
|
||||
url = f"http://{self.host}:{self.port}/api/v1/{method}/"
|
||||
|
||||
_LOGGER.debug("URL: %s params: %s", url, params)
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
response = await websession.get(url, params=params)
|
||||
if response.status == HTTP_OK:
|
||||
data = await response.json()
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Query failed, response code: %s Full message: %s",
|
||||
response.status,
|
||||
response,
|
||||
)
|
||||
return False
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError) as error:
|
||||
_LOGGER.error(
|
||||
"Failed communicating with Volumio '%s': %s", self._name, type(error)
|
||||
)
|
||||
return False
|
||||
|
||||
return data
|
||||
|
||||
async def async_update(self):
|
||||
"""Update state."""
|
||||
resp = await self.send_volumio_msg("getState")
|
||||
self._state = await self._volumio.get_state()
|
||||
await self._async_update_playlists()
|
||||
if resp is False:
|
||||
return
|
||||
self._state = resp.copy()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id for the entity."""
|
||||
return self._uid
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info for this device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.unique_id)},
|
||||
"name": self.name,
|
||||
"manufacturer": "Volumio",
|
||||
"sw_version": self._info["systemversion"],
|
||||
"model": self._info["hardware"],
|
||||
}
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
@ -189,13 +142,7 @@ class Volumio(MediaPlayerEntity):
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
url = self._state.get("albumart", None)
|
||||
if url is None:
|
||||
return
|
||||
if str(url[0:2]).lower() == "ht":
|
||||
mediaurl = url
|
||||
else:
|
||||
mediaurl = f"http://{self.host}:{self.port}{url}"
|
||||
return mediaurl
|
||||
return self._volumio.canonic_url(url)
|
||||
|
||||
@property
|
||||
def media_seek_position(self):
|
||||
@ -220,11 +167,6 @@ class Volumio(MediaPlayerEntity):
|
||||
"""Boolean if volume is currently muted."""
|
||||
return self._state.get("mute", None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def shuffle(self):
|
||||
"""Boolean if shuffle is enabled."""
|
||||
@ -247,79 +189,61 @@ class Volumio(MediaPlayerEntity):
|
||||
|
||||
async def async_media_next_track(self):
|
||||
"""Send media_next command to media player."""
|
||||
await self.send_volumio_msg("commands", params={"cmd": "next"})
|
||||
await self._volumio.next()
|
||||
|
||||
async def async_media_previous_track(self):
|
||||
"""Send media_previous command to media player."""
|
||||
await self.send_volumio_msg("commands", params={"cmd": "prev"})
|
||||
await self._volumio.previous()
|
||||
|
||||
async def async_media_play(self):
|
||||
"""Send media_play command to media player."""
|
||||
await self.send_volumio_msg("commands", params={"cmd": "play"})
|
||||
await self._volumio.play()
|
||||
|
||||
async def async_media_pause(self):
|
||||
"""Send media_pause command to media player."""
|
||||
if self._state["trackType"] == "webradio":
|
||||
await self.send_volumio_msg("commands", params={"cmd": "stop"})
|
||||
await self._volumio.stop()
|
||||
else:
|
||||
await self.send_volumio_msg("commands", params={"cmd": "pause"})
|
||||
await self._volumio.pause()
|
||||
|
||||
async def async_media_stop(self):
|
||||
"""Send media_stop command to media player."""
|
||||
await self.send_volumio_msg("commands", params={"cmd": "stop"})
|
||||
await self._volumio.stop()
|
||||
|
||||
async def async_set_volume_level(self, volume):
|
||||
"""Send volume_up command to media player."""
|
||||
await self.send_volumio_msg(
|
||||
"commands", params={"cmd": "volume", "volume": int(volume * 100)}
|
||||
)
|
||||
await self._volumio.set_volume_level(int(volume * 100))
|
||||
|
||||
async def async_volume_up(self):
|
||||
"""Service to send the Volumio the command for volume up."""
|
||||
await self.send_volumio_msg(
|
||||
"commands", params={"cmd": "volume", "volume": "plus"}
|
||||
)
|
||||
await self._volumio.volume_up()
|
||||
|
||||
async def async_volume_down(self):
|
||||
"""Service to send the Volumio the command for volume down."""
|
||||
await self.send_volumio_msg(
|
||||
"commands", params={"cmd": "volume", "volume": "minus"}
|
||||
)
|
||||
await self._volumio.volume_down()
|
||||
|
||||
async def async_mute_volume(self, mute):
|
||||
"""Send mute command to media player."""
|
||||
mutecmd = "mute" if mute else "unmute"
|
||||
if mute:
|
||||
# mute is implemented as 0 volume, do save last volume level
|
||||
self._lastvol = self._state["volume"]
|
||||
await self.send_volumio_msg(
|
||||
"commands", params={"cmd": "volume", "volume": mutecmd}
|
||||
)
|
||||
return
|
||||
|
||||
await self.send_volumio_msg(
|
||||
"commands", params={"cmd": "volume", "volume": self._lastvol}
|
||||
)
|
||||
await self._volumio.mute()
|
||||
else:
|
||||
await self._volumio.unmute()
|
||||
|
||||
async def async_set_shuffle(self, shuffle):
|
||||
"""Enable/disable shuffle mode."""
|
||||
await self.send_volumio_msg(
|
||||
"commands", params={"cmd": "random", "value": str(shuffle).lower()}
|
||||
)
|
||||
await self._volumio.set_shuffle(shuffle)
|
||||
|
||||
async def async_select_source(self, source):
|
||||
"""Choose a different available playlist and play it."""
|
||||
"""Choose an available playlist and play it."""
|
||||
await self._volumio.play_playlist(source)
|
||||
self._currentplaylist = source
|
||||
await self.send_volumio_msg(
|
||||
"commands", params={"cmd": "playplaylist", "name": source}
|
||||
)
|
||||
|
||||
async def async_clear_playlist(self):
|
||||
"""Clear players playlist."""
|
||||
await self._volumio.clear_playlist()
|
||||
self._currentplaylist = None
|
||||
await self.send_volumio_msg("commands", params={"cmd": "clearQueue"})
|
||||
|
||||
@Throttle(PLAYLIST_UPDATE_INTERVAL)
|
||||
async def _async_update_playlists(self, **kwargs):
|
||||
"""Update available Volumio playlists."""
|
||||
self._playlists = await self.send_volumio_msg("listplaylists")
|
||||
self._playlists = await self._volumio.get_playlists()
|
||||
|
24
homeassistant/components/volumio/strings.json
Normal file
24
homeassistant/components/volumio/strings.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
}
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to add Volumio (`{name}`) to Home Assistant?",
|
||||
"title": "Discovered Volumio"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "Cannot connect to discovered Volumio"
|
||||
}
|
||||
}
|
||||
}
|
@ -184,6 +184,7 @@ FLOWS = [
|
||||
"vesync",
|
||||
"vilfo",
|
||||
"vizio",
|
||||
"volumio",
|
||||
"wemo",
|
||||
"wiffi",
|
||||
"withings",
|
||||
|
@ -6,6 +6,9 @@ To update, run python3 -m script.hassfest
|
||||
# fmt: off
|
||||
|
||||
ZEROCONF = {
|
||||
"_Volumio._tcp.local.": [
|
||||
"volumio"
|
||||
],
|
||||
"_api._udp.local.": [
|
||||
"guardian"
|
||||
],
|
||||
|
@ -1827,6 +1827,9 @@ pyvizio==0.1.49
|
||||
# homeassistant.components.velux
|
||||
pyvlx==0.2.16
|
||||
|
||||
# homeassistant.components.volumio
|
||||
pyvolumio==0.1
|
||||
|
||||
# homeassistant.components.html5
|
||||
pywebpush==1.9.2
|
||||
|
||||
|
@ -826,6 +826,9 @@ pyvesync==1.1.0
|
||||
# homeassistant.components.vizio
|
||||
pyvizio==0.1.49
|
||||
|
||||
# homeassistant.components.volumio
|
||||
pyvolumio==0.1
|
||||
|
||||
# homeassistant.components.html5
|
||||
pywebpush==1.9.2
|
||||
|
||||
|
1
tests/components/volumio/__init__.py
Normal file
1
tests/components/volumio/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Volumio integration."""
|
252
tests/components/volumio/test_config_flow.py
Normal file
252
tests/components/volumio/test_config_flow.py
Normal file
@ -0,0 +1,252 @@
|
||||
"""Test the Volumio config flow."""
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.volumio.config_flow import CannotConnectError
|
||||
from homeassistant.components.volumio.const import DOMAIN
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_SYSTEM_INFO = {"id": "1111-1111-1111-1111", "name": "TestVolumio"}
|
||||
|
||||
|
||||
TEST_CONNECTION = {
|
||||
"host": "1.1.1.1",
|
||||
"port": 3000,
|
||||
}
|
||||
|
||||
|
||||
TEST_DISCOVERY = {
|
||||
"host": "1.1.1.1",
|
||||
"port": 3000,
|
||||
"properties": {"volumioName": "discovered", "UUID": "2222-2222-2222-2222"},
|
||||
}
|
||||
|
||||
TEST_DISCOVERY_RESULT = {
|
||||
"host": TEST_DISCOVERY["host"],
|
||||
"port": TEST_DISCOVERY["port"],
|
||||
"id": TEST_DISCOVERY["properties"]["UUID"],
|
||||
"name": TEST_DISCOVERY["properties"]["volumioName"],
|
||||
}
|
||||
|
||||
|
||||
async def test_form(hass):
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
|
||||
return_value=TEST_SYSTEM_INFO,
|
||||
), patch(
|
||||
"homeassistant.components.volumio.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.volumio.async_setup_entry", return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_CONNECTION,
|
||||
)
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "TestVolumio"
|
||||
assert result2["data"] == {**TEST_SYSTEM_INFO, **TEST_CONNECTION}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_updates_unique_id(hass):
|
||||
"""Test a duplicate id aborts and updates existing entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_SYSTEM_INFO["id"],
|
||||
data={
|
||||
"host": "dummy",
|
||||
"port": 11,
|
||||
"name": "dummy",
|
||||
"id": TEST_SYSTEM_INFO["id"],
|
||||
},
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
|
||||
return_value=TEST_SYSTEM_INFO,
|
||||
), patch("homeassistant.components.volumio.async_setup", return_value=True), patch(
|
||||
"homeassistant.components.volumio.async_setup_entry", return_value=True,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_CONNECTION,
|
||||
)
|
||||
|
||||
assert result2["type"] == "abort"
|
||||
assert result2["reason"] == "already_configured"
|
||||
|
||||
assert entry.data == {**TEST_SYSTEM_INFO, **TEST_CONNECTION}
|
||||
|
||||
|
||||
async def test_empty_system_info(hass):
|
||||
"""Test old volumio versions with empty system info."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
|
||||
return_value={},
|
||||
), patch(
|
||||
"homeassistant.components.volumio.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.volumio.async_setup_entry", return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_CONNECTION,
|
||||
)
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == TEST_CONNECTION["host"]
|
||||
assert result2["data"] == {
|
||||
"host": TEST_CONNECTION["host"],
|
||||
"port": TEST_CONNECTION["port"],
|
||||
"name": TEST_CONNECTION["host"],
|
||||
"id": None,
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
|
||||
side_effect=CannotConnectError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_CONNECTION,
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_exception(hass):
|
||||
"""Test we handle generic error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], TEST_CONNECTION,
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_discovery(hass):
|
||||
"""Test discovery flow works."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
|
||||
return_value=TEST_SYSTEM_INFO,
|
||||
), patch(
|
||||
"homeassistant.components.volumio.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.volumio.async_setup_entry", return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={},
|
||||
)
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == TEST_DISCOVERY_RESULT["name"]
|
||||
assert result2["data"] == TEST_DISCOVERY_RESULT
|
||||
|
||||
assert result2["result"]
|
||||
assert result2["result"].unique_id == TEST_DISCOVERY_RESULT["id"]
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_discovery_cannot_connect(hass):
|
||||
"""Test discovery aborts if cannot connect."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.volumio.config_flow.Volumio.get_system_info",
|
||||
side_effect=CannotConnectError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={},
|
||||
)
|
||||
|
||||
assert result2["type"] == "abort"
|
||||
assert result2["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_discovery_duplicate_data(hass):
|
||||
"""Test discovery aborts if same mDNS packet arrives."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
|
||||
)
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_in_progress"
|
||||
|
||||
|
||||
async def test_discovery_updates_unique_id(hass):
|
||||
"""Test a duplicate discovery id aborts and updates existing entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_DISCOVERY_RESULT["id"],
|
||||
data={
|
||||
"host": "dummy",
|
||||
"port": 11,
|
||||
"name": "dummy",
|
||||
"id": TEST_DISCOVERY_RESULT["id"],
|
||||
},
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
assert entry.data == TEST_DISCOVERY_RESULT
|
Reference in New Issue
Block a user