mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 06:47:09 +00:00
Add config flow to Volumio (#38252)
This commit is contained in:
parent
8b06d1d4bd
commit
b226a7183f
@ -926,6 +926,7 @@ omit =
|
|||||||
homeassistant/components/vlc/media_player.py
|
homeassistant/components/vlc/media_player.py
|
||||||
homeassistant/components/vlc_telnet/media_player.py
|
homeassistant/components/vlc_telnet/media_player.py
|
||||||
homeassistant/components/volkszaehler/sensor.py
|
homeassistant/components/volkszaehler/sensor.py
|
||||||
|
homeassistant/components/volumio/__init__.py
|
||||||
homeassistant/components/volumio/media_player.py
|
homeassistant/components/volumio/media_player.py
|
||||||
homeassistant/components/volvooncall/*
|
homeassistant/components/volvooncall/*
|
||||||
homeassistant/components/w800rf32/*
|
homeassistant/components/w800rf32/*
|
||||||
|
@ -454,6 +454,7 @@ homeassistant/components/vilfo/* @ManneW
|
|||||||
homeassistant/components/vivotek/* @HarlemSquirrel
|
homeassistant/components/vivotek/* @HarlemSquirrel
|
||||||
homeassistant/components/vizio/* @raman325
|
homeassistant/components/vizio/* @raman325
|
||||||
homeassistant/components/vlc_telnet/* @rodripf
|
homeassistant/components/vlc_telnet/* @rodripf
|
||||||
|
homeassistant/components/volumio/* @OnFreund
|
||||||
homeassistant/components/waqi/* @andrey-git
|
homeassistant/components/waqi/* @andrey-git
|
||||||
homeassistant/components/watson_tts/* @rutkai
|
homeassistant/components/watson_tts/* @rutkai
|
||||||
homeassistant/components/weather/* @fabaff
|
homeassistant/components/weather/* @fabaff
|
||||||
|
@ -71,7 +71,6 @@ SERVICE_HANDLERS = {
|
|||||||
"bose_soundtouch": ("media_player", "soundtouch"),
|
"bose_soundtouch": ("media_player", "soundtouch"),
|
||||||
"bluesound": ("media_player", "bluesound"),
|
"bluesound": ("media_player", "bluesound"),
|
||||||
"kodi": ("media_player", "kodi"),
|
"kodi": ("media_player", "kodi"),
|
||||||
"volumio": ("media_player", "volumio"),
|
|
||||||
"lg_smart_device": ("media_player", "lg_soundbar"),
|
"lg_smart_device": ("media_player", "lg_soundbar"),
|
||||||
"nanoleaf_aurora": ("light", "nanoleaf"),
|
"nanoleaf_aurora": ("light", "nanoleaf"),
|
||||||
}
|
}
|
||||||
@ -93,6 +92,7 @@ MIGRATED_SERVICE_HANDLERS = [
|
|||||||
"songpal",
|
"songpal",
|
||||||
SERVICE_WEMO,
|
SERVICE_WEMO,
|
||||||
SERVICE_XIAOMI_GW,
|
SERVICE_XIAOMI_GW,
|
||||||
|
"volumio",
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_ENABLED = (
|
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",
|
"domain": "volumio",
|
||||||
"name": "Volumio",
|
"name": "Volumio",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/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
|
Volumio rest API: https://volumio.github.io/docs/API/REST_API.html
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import socket
|
|
||||||
|
|
||||||
import aiohttp
|
from homeassistant.components.media_player import MediaPlayerEntity
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
|
|
||||||
from homeassistant.components.media_player.const import (
|
from homeassistant.components.media_player.const import (
|
||||||
MEDIA_TYPE_MUSIC,
|
MEDIA_TYPE_MUSIC,
|
||||||
SUPPORT_CLEAR_PLAYLIST,
|
SUPPORT_CLEAR_PLAYLIST,
|
||||||
@ -28,29 +23,19 @@ from homeassistant.components.media_player.const import (
|
|||||||
SUPPORT_VOLUME_STEP,
|
SUPPORT_VOLUME_STEP,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_ID,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_PORT,
|
|
||||||
HTTP_OK,
|
|
||||||
STATE_IDLE,
|
STATE_IDLE,
|
||||||
STATE_PAUSED,
|
STATE_PAUSED,
|
||||||
STATE_PLAYING,
|
STATE_PLAYING,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
from .const import DATA_INFO, DATA_VOLUMIO, DOMAIN
|
||||||
|
|
||||||
_CONFIGURING = {}
|
_CONFIGURING = {}
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_HOST = "localhost"
|
|
||||||
DEFAULT_NAME = "Volumio"
|
|
||||||
DEFAULT_PORT = 3000
|
|
||||||
|
|
||||||
DATA_VOLUMIO = "volumio"
|
|
||||||
|
|
||||||
TIMEOUT = 10
|
|
||||||
|
|
||||||
SUPPORT_VOLUMIO = (
|
SUPPORT_VOLUMIO = (
|
||||||
SUPPORT_PAUSE
|
SUPPORT_PAUSE
|
||||||
| SUPPORT_VOLUME_SET
|
| SUPPORT_VOLUME_SET
|
||||||
@ -68,91 +53,59 @@ SUPPORT_VOLUMIO = (
|
|||||||
|
|
||||||
PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=15)
|
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):
|
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
"""Set up the Volumio platform."""
|
volumio = data[DATA_VOLUMIO]
|
||||||
if DATA_VOLUMIO not in hass.data:
|
info = data[DATA_INFO]
|
||||||
hass.data[DATA_VOLUMIO] = {}
|
uid = config_entry.data[CONF_ID]
|
||||||
|
name = config_entry.data[CONF_NAME]
|
||||||
|
|
||||||
# This is a manual configuration?
|
entity = Volumio(hass, volumio, uid, name, info)
|
||||||
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
|
|
||||||
async_add_entities([entity])
|
async_add_entities([entity])
|
||||||
|
|
||||||
|
|
||||||
class Volumio(MediaPlayerEntity):
|
class Volumio(MediaPlayerEntity):
|
||||||
"""Volumio Player Object."""
|
"""Volumio Player Object."""
|
||||||
|
|
||||||
def __init__(self, name, host, port, hass):
|
def __init__(self, hass, volumio, uid, name, info):
|
||||||
"""Initialize the media player."""
|
"""Initialize the media player."""
|
||||||
self.host = host
|
self._hass = hass
|
||||||
self.port = port
|
self._volumio = volumio
|
||||||
self.hass = hass
|
self._uid = uid
|
||||||
self._url = "{}:{}".format(host, str(port))
|
|
||||||
self._name = name
|
self._name = name
|
||||||
|
self._info = info
|
||||||
self._state = {}
|
self._state = {}
|
||||||
self._lastvol = self._state.get("volume", 0)
|
|
||||||
self._playlists = []
|
self._playlists = []
|
||||||
self._currentplaylist = None
|
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):
|
async def async_update(self):
|
||||||
"""Update state."""
|
"""Update state."""
|
||||||
resp = await self.send_volumio_msg("getState")
|
self._state = await self._volumio.get_state()
|
||||||
await self._async_update_playlists()
|
await self._async_update_playlists()
|
||||||
if resp is False:
|
|
||||||
return
|
@property
|
||||||
self._state = resp.copy()
|
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
|
@property
|
||||||
def media_content_type(self):
|
def media_content_type(self):
|
||||||
@ -189,13 +142,7 @@ class Volumio(MediaPlayerEntity):
|
|||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
"""Image url of current playing media."""
|
"""Image url of current playing media."""
|
||||||
url = self._state.get("albumart", None)
|
url = self._state.get("albumart", None)
|
||||||
if url is None:
|
return self._volumio.canonic_url(url)
|
||||||
return
|
|
||||||
if str(url[0:2]).lower() == "ht":
|
|
||||||
mediaurl = url
|
|
||||||
else:
|
|
||||||
mediaurl = f"http://{self.host}:{self.port}{url}"
|
|
||||||
return mediaurl
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_seek_position(self):
|
def media_seek_position(self):
|
||||||
@ -220,11 +167,6 @@ class Volumio(MediaPlayerEntity):
|
|||||||
"""Boolean if volume is currently muted."""
|
"""Boolean if volume is currently muted."""
|
||||||
return self._state.get("mute", None)
|
return self._state.get("mute", None)
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
"""Return the name of the device."""
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shuffle(self):
|
def shuffle(self):
|
||||||
"""Boolean if shuffle is enabled."""
|
"""Boolean if shuffle is enabled."""
|
||||||
@ -247,79 +189,61 @@ class Volumio(MediaPlayerEntity):
|
|||||||
|
|
||||||
async def async_media_next_track(self):
|
async def async_media_next_track(self):
|
||||||
"""Send media_next command to media player."""
|
"""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):
|
async def async_media_previous_track(self):
|
||||||
"""Send media_previous command to media player."""
|
"""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):
|
async def async_media_play(self):
|
||||||
"""Send media_play command to media player."""
|
"""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):
|
async def async_media_pause(self):
|
||||||
"""Send media_pause command to media player."""
|
"""Send media_pause command to media player."""
|
||||||
if self._state["trackType"] == "webradio":
|
if self._state["trackType"] == "webradio":
|
||||||
await self.send_volumio_msg("commands", params={"cmd": "stop"})
|
await self._volumio.stop()
|
||||||
else:
|
else:
|
||||||
await self.send_volumio_msg("commands", params={"cmd": "pause"})
|
await self._volumio.pause()
|
||||||
|
|
||||||
async def async_media_stop(self):
|
async def async_media_stop(self):
|
||||||
"""Send media_stop command to media player."""
|
"""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):
|
async def async_set_volume_level(self, volume):
|
||||||
"""Send volume_up command to media player."""
|
"""Send volume_up command to media player."""
|
||||||
await self.send_volumio_msg(
|
await self._volumio.set_volume_level(int(volume * 100))
|
||||||
"commands", params={"cmd": "volume", "volume": int(volume * 100)}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_volume_up(self):
|
async def async_volume_up(self):
|
||||||
"""Service to send the Volumio the command for volume up."""
|
"""Service to send the Volumio the command for volume up."""
|
||||||
await self.send_volumio_msg(
|
await self._volumio.volume_up()
|
||||||
"commands", params={"cmd": "volume", "volume": "plus"}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_volume_down(self):
|
async def async_volume_down(self):
|
||||||
"""Service to send the Volumio the command for volume down."""
|
"""Service to send the Volumio the command for volume down."""
|
||||||
await self.send_volumio_msg(
|
await self._volumio.volume_down()
|
||||||
"commands", params={"cmd": "volume", "volume": "minus"}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_mute_volume(self, mute):
|
async def async_mute_volume(self, mute):
|
||||||
"""Send mute command to media player."""
|
"""Send mute command to media player."""
|
||||||
mutecmd = "mute" if mute else "unmute"
|
|
||||||
if mute:
|
if mute:
|
||||||
# mute is implemented as 0 volume, do save last volume level
|
await self._volumio.mute()
|
||||||
self._lastvol = self._state["volume"]
|
else:
|
||||||
await self.send_volumio_msg(
|
await self._volumio.unmute()
|
||||||
"commands", params={"cmd": "volume", "volume": mutecmd}
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.send_volumio_msg(
|
|
||||||
"commands", params={"cmd": "volume", "volume": self._lastvol}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_set_shuffle(self, shuffle):
|
async def async_set_shuffle(self, shuffle):
|
||||||
"""Enable/disable shuffle mode."""
|
"""Enable/disable shuffle mode."""
|
||||||
await self.send_volumio_msg(
|
await self._volumio.set_shuffle(shuffle)
|
||||||
"commands", params={"cmd": "random", "value": str(shuffle).lower()}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_select_source(self, source):
|
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
|
self._currentplaylist = source
|
||||||
await self.send_volumio_msg(
|
|
||||||
"commands", params={"cmd": "playplaylist", "name": source}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_clear_playlist(self):
|
async def async_clear_playlist(self):
|
||||||
"""Clear players playlist."""
|
"""Clear players playlist."""
|
||||||
|
await self._volumio.clear_playlist()
|
||||||
self._currentplaylist = None
|
self._currentplaylist = None
|
||||||
await self.send_volumio_msg("commands", params={"cmd": "clearQueue"})
|
|
||||||
|
|
||||||
@Throttle(PLAYLIST_UPDATE_INTERVAL)
|
@Throttle(PLAYLIST_UPDATE_INTERVAL)
|
||||||
async def _async_update_playlists(self, **kwargs):
|
async def _async_update_playlists(self, **kwargs):
|
||||||
"""Update available Volumio playlists."""
|
"""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",
|
"vesync",
|
||||||
"vilfo",
|
"vilfo",
|
||||||
"vizio",
|
"vizio",
|
||||||
|
"volumio",
|
||||||
"wemo",
|
"wemo",
|
||||||
"wiffi",
|
"wiffi",
|
||||||
"withings",
|
"withings",
|
||||||
|
@ -6,6 +6,9 @@ To update, run python3 -m script.hassfest
|
|||||||
# fmt: off
|
# fmt: off
|
||||||
|
|
||||||
ZEROCONF = {
|
ZEROCONF = {
|
||||||
|
"_Volumio._tcp.local.": [
|
||||||
|
"volumio"
|
||||||
|
],
|
||||||
"_api._udp.local.": [
|
"_api._udp.local.": [
|
||||||
"guardian"
|
"guardian"
|
||||||
],
|
],
|
||||||
|
@ -1827,6 +1827,9 @@ pyvizio==0.1.49
|
|||||||
# homeassistant.components.velux
|
# homeassistant.components.velux
|
||||||
pyvlx==0.2.16
|
pyvlx==0.2.16
|
||||||
|
|
||||||
|
# homeassistant.components.volumio
|
||||||
|
pyvolumio==0.1
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.9.2
|
||||||
|
|
||||||
|
@ -826,6 +826,9 @@ pyvesync==1.1.0
|
|||||||
# homeassistant.components.vizio
|
# homeassistant.components.vizio
|
||||||
pyvizio==0.1.49
|
pyvizio==0.1.49
|
||||||
|
|
||||||
|
# homeassistant.components.volumio
|
||||||
|
pyvolumio==0.1
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
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
|
Loading…
x
Reference in New Issue
Block a user