DenonAVR Config Flow (#35255)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
25607c7129
commit
6db5ff98ed
15 changed files with 1272 additions and 138 deletions
|
@ -160,6 +160,7 @@ omit =
|
|||
homeassistant/components/deluge/switch.py
|
||||
homeassistant/components/denon/media_player.py
|
||||
homeassistant/components/denonavr/media_player.py
|
||||
homeassistant/components/denonavr/receiver.py
|
||||
homeassistant/components/deutsche_bahn/sensor.py
|
||||
homeassistant/components/devolo_home_control/__init__.py
|
||||
homeassistant/components/devolo_home_control/binary_sensor.py
|
||||
|
|
|
@ -1,15 +1,33 @@
|
|||
"""The denonavr component."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant import config_entries, core
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
DOMAIN = "denonavr"
|
||||
from .config_flow import (
|
||||
CONF_SHOW_ALL_SOURCES,
|
||||
CONF_ZONE2,
|
||||
CONF_ZONE3,
|
||||
DEFAULT_SHOW_SOURCES,
|
||||
DEFAULT_TIMEOUT,
|
||||
DEFAULT_ZONE2,
|
||||
DEFAULT_ZONE3,
|
||||
DOMAIN,
|
||||
)
|
||||
from .receiver import ConnectDenonAVR
|
||||
|
||||
CONF_RECEIVER = "receiver"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
SERVICE_GET_COMMAND = "get_command"
|
||||
ATTR_COMMAND = "command"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids})
|
||||
|
||||
GET_COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string})
|
||||
|
@ -19,7 +37,7 @@ SERVICE_TO_METHOD = {
|
|||
}
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
def setup(hass: core.HomeAssistant, config: dict):
|
||||
"""Set up the denonavr platform."""
|
||||
|
||||
def service_handler(service):
|
||||
|
@ -33,3 +51,72 @@ def setup(hass, config):
|
|||
hass.services.register(DOMAIN, service, service_handler, schema=schema)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
|
||||
):
|
||||
"""Set up the denonavr components from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# Connect to receiver
|
||||
connect_denonavr = ConnectDenonAVR(
|
||||
hass,
|
||||
entry.data[CONF_HOST],
|
||||
DEFAULT_TIMEOUT,
|
||||
entry.options.get(CONF_SHOW_ALL_SOURCES, DEFAULT_SHOW_SOURCES),
|
||||
entry.options.get(CONF_ZONE2, DEFAULT_ZONE2),
|
||||
entry.options.get(CONF_ZONE3, DEFAULT_ZONE3),
|
||||
)
|
||||
if not await connect_denonavr.async_connect_receiver():
|
||||
raise ConfigEntryNotReady
|
||||
receiver = connect_denonavr.receiver
|
||||
|
||||
undo_listener = entry.add_update_listener(update_listener)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
CONF_RECEIVER: receiver,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
}
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, "media_player")
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
|
||||
):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_forward_entry_unload(
|
||||
config_entry, "media_player"
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
|
||||
# Remove zone2 and zone3 entities if needed
|
||||
entity_registry = await er.async_get_registry(hass)
|
||||
entries = er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)
|
||||
zone2_id = f"{config_entry.unique_id}-Zone2"
|
||||
zone3_id = f"{config_entry.unique_id}-Zone3"
|
||||
for entry in entries:
|
||||
if entry.unique_id == zone2_id and not config_entry.options.get(CONF_ZONE2):
|
||||
entity_registry.async_remove(entry.entity_id)
|
||||
_LOGGER.debug("Removing zone2 from DenonAvr")
|
||||
if entry.unique_id == zone3_id and not config_entry.options.get(CONF_ZONE3):
|
||||
entity_registry.async_remove(entry.entity_id)
|
||||
_LOGGER.debug("Removing zone3 from DenonAvr")
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def update_listener(
|
||||
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
|
||||
):
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
|
256
homeassistant/components/denonavr/config_flow.py
Normal file
256
homeassistant/components/denonavr/config_flow.py
Normal file
|
@ -0,0 +1,256 @@
|
|||
"""Config flow to configure Denon AVR receivers using their HTTP interface."""
|
||||
from functools import partial
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import denonavr
|
||||
from getmac import get_mac_address
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import ssdp
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .receiver import ConnectDenonAVR
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "denonavr"
|
||||
|
||||
SUPPORTED_MANUFACTURERS = ["Denon", "DENON", "Marantz"]
|
||||
|
||||
CONF_SHOW_ALL_SOURCES = "show_all_sources"
|
||||
CONF_ZONE2 = "zone2"
|
||||
CONF_ZONE3 = "zone3"
|
||||
CONF_TYPE = "type"
|
||||
CONF_MODEL = "model"
|
||||
CONF_MANUFACTURER = "manufacturer"
|
||||
CONF_SERIAL_NUMBER = "serial_number"
|
||||
|
||||
DEFAULT_SHOW_SOURCES = False
|
||||
DEFAULT_TIMEOUT = 5
|
||||
DEFAULT_ZONE2 = False
|
||||
DEFAULT_ZONE3 = False
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str})
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Options for the component."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||
"""Init object."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
settings_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_SHOW_ALL_SOURCES,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_SHOW_ALL_SOURCES, DEFAULT_SHOW_SOURCES
|
||||
),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_ZONE2,
|
||||
default=self.config_entry.options.get(CONF_ZONE2, DEFAULT_ZONE2),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_ZONE3,
|
||||
default=self.config_entry.options.get(CONF_ZONE3, DEFAULT_ZONE3),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=settings_schema)
|
||||
|
||||
|
||||
class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Denon AVR config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Denon AVR flow."""
|
||||
self.host = None
|
||||
self.serial_number = None
|
||||
self.model_name = None
|
||||
self.timeout = DEFAULT_TIMEOUT
|
||||
self.show_all_sources = DEFAULT_SHOW_SOURCES
|
||||
self.zone2 = DEFAULT_ZONE2
|
||||
self.zone3 = DEFAULT_ZONE3
|
||||
self.d_receivers = []
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry) -> OptionsFlowHandler:
|
||||
"""Get the options flow."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
# check if IP address is set manually
|
||||
host = user_input.get(CONF_HOST)
|
||||
if host:
|
||||
self.host = host
|
||||
return await self.async_step_connect()
|
||||
|
||||
# discovery using denonavr library
|
||||
self.d_receivers = await self.hass.async_add_executor_job(denonavr.discover)
|
||||
# More than one receiver could be discovered by that method
|
||||
if len(self.d_receivers) == 1:
|
||||
self.host = self.d_receivers[0]["host"]
|
||||
return await self.async_step_connect()
|
||||
if len(self.d_receivers) > 1:
|
||||
# show selection form
|
||||
return await self.async_step_select()
|
||||
|
||||
errors["base"] = "discovery_error"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_select(self, user_input=None):
|
||||
"""Handle multiple receivers found."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
self.host = user_input["select_host"]
|
||||
return await self.async_step_connect()
|
||||
|
||||
select_scheme = vol.Schema(
|
||||
{
|
||||
vol.Required("select_host"): vol.In(
|
||||
[d_receiver["host"] for d_receiver in self.d_receivers]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="select", data_schema=select_scheme, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_confirm(self, user_input=None):
|
||||
"""Allow the user to confirm adding the device."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_connect()
|
||||
|
||||
return self.async_show_form(step_id="confirm")
|
||||
|
||||
async def async_step_connect(self, user_input=None):
|
||||
"""Connect to the receiver."""
|
||||
connect_denonavr = ConnectDenonAVR(
|
||||
self.hass,
|
||||
self.host,
|
||||
self.timeout,
|
||||
self.show_all_sources,
|
||||
self.zone2,
|
||||
self.zone3,
|
||||
)
|
||||
if not await connect_denonavr.async_connect_receiver():
|
||||
return self.async_abort(reason="connection_error")
|
||||
receiver = connect_denonavr.receiver
|
||||
|
||||
mac_address = await self.async_get_mac(self.host)
|
||||
|
||||
if not self.serial_number:
|
||||
self.serial_number = receiver.serial_number
|
||||
if not self.model_name:
|
||||
self.model_name = (receiver.model_name).replace("*", "")
|
||||
|
||||
if self.serial_number is not None:
|
||||
unique_id = self.construct_unique_id(self.model_name, self.serial_number)
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Could not get serial number of host %s, "
|
||||
"unique_id's will not be available",
|
||||
self.host,
|
||||
)
|
||||
for entry in self._async_current_entries():
|
||||
if entry.data[CONF_HOST] == self.host:
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=receiver.name,
|
||||
data={
|
||||
CONF_HOST: self.host,
|
||||
CONF_MAC: mac_address,
|
||||
CONF_TYPE: receiver.receiver_type,
|
||||
CONF_MODEL: self.model_name,
|
||||
CONF_MANUFACTURER: receiver.manufacturer,
|
||||
CONF_SERIAL_NUMBER: self.serial_number,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_ssdp(self, discovery_info):
|
||||
"""Handle a discovered Denon AVR.
|
||||
|
||||
This flow is triggered by the SSDP component. It will check if the
|
||||
host is already configured and delegate to the import step if not.
|
||||
"""
|
||||
# Filter out non-Denon AVRs#1
|
||||
if (
|
||||
discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER)
|
||||
not in SUPPORTED_MANUFACTURERS
|
||||
):
|
||||
return self.async_abort(reason="not_denonavr_manufacturer")
|
||||
|
||||
# Check if required information is present to set the unique_id
|
||||
if (
|
||||
ssdp.ATTR_UPNP_MODEL_NAME not in discovery_info
|
||||
or ssdp.ATTR_UPNP_SERIAL not in discovery_info
|
||||
):
|
||||
return self.async_abort(reason="not_denonavr_missing")
|
||||
|
||||
self.model_name = discovery_info[ssdp.ATTR_UPNP_MODEL_NAME].replace("*", "")
|
||||
self.serial_number = discovery_info[ssdp.ATTR_UPNP_SERIAL]
|
||||
self.host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname
|
||||
|
||||
unique_id = self.construct_unique_id(self.model_name, self.serial_number)
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context.update(
|
||||
{
|
||||
"title_placeholders": {
|
||||
"name": discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, self.host)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return await self.async_step_confirm()
|
||||
|
||||
@staticmethod
|
||||
def construct_unique_id(model_name, serial_number):
|
||||
"""Construct the unique id from the ssdp discovery or user_step."""
|
||||
return f"{model_name}-{serial_number}"
|
||||
|
||||
async def async_get_mac(self, host):
|
||||
"""Get the mac address of the DenonAVR receiver."""
|
||||
try:
|
||||
mac_address = await self.hass.async_add_executor_job(
|
||||
partial(get_mac_address, **{"ip": host})
|
||||
)
|
||||
if not mac_address:
|
||||
mac_address = await self.hass.async_add_executor_job(
|
||||
partial(get_mac_address, **{"hostname": host})
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.error("Unable to get mac address: %s", err)
|
||||
mac_address = None
|
||||
|
||||
if mac_address is not None:
|
||||
mac_address = format_mac(mac_address)
|
||||
return mac_address
|
|
@ -1,7 +1,46 @@
|
|||
{
|
||||
"domain": "denonavr",
|
||||
"name": "Denon AVR Network Receivers",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||
"requirements": ["denonavr==0.8.1"],
|
||||
"codeowners": ["@scarface-4711", "@starkillerOG"]
|
||||
"requirements": ["denonavr==0.9.3", "getmac==0.8.2"],
|
||||
"codeowners": ["@scarface-4711", "@starkillerOG"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1"
|
||||
},
|
||||
{
|
||||
"manufacturer": "DENON",
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Marantz",
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1"
|
||||
},
|
||||
{
|
||||
"manufacturer": "DENON",
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Marantz",
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
"deviceType": "urn:schemas-denon-com:device:AiosDevice:1"
|
||||
},
|
||||
{
|
||||
"manufacturer": "DENON",
|
||||
"deviceType": "urn:schemas-denon-com:device:AiosDevice:1"
|
||||
},
|
||||
{
|
||||
"manufacturer": "Marantz",
|
||||
"deviceType": "urn:schemas-denon-com:device:AiosDevice:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
"""Support for Denon AVR receivers using their HTTP interface."""
|
||||
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
import denonavr
|
||||
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_CHANNEL,
|
||||
MEDIA_TYPE_MUSIC,
|
||||
|
@ -25,10 +21,7 @@ from homeassistant.components.media_player.const import (
|
|||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_TIMEOUT,
|
||||
CONF_ZONE,
|
||||
CONF_MAC,
|
||||
ENTITY_MATCH_ALL,
|
||||
ENTITY_MATCH_NONE,
|
||||
STATE_OFF,
|
||||
|
@ -36,25 +29,22 @@ from homeassistant.const import (
|
|||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DOMAIN
|
||||
from . import CONF_RECEIVER
|
||||
from .config_flow import (
|
||||
CONF_MANUFACTURER,
|
||||
CONF_MODEL,
|
||||
CONF_SERIAL_NUMBER,
|
||||
CONF_TYPE,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_SOUND_MODE_RAW = "sound_mode_raw"
|
||||
|
||||
CONF_INVALID_ZONES_ERR = "Invalid Zone (expected Zone2 or Zone3)"
|
||||
CONF_SHOW_ALL_SOURCES = "show_all_sources"
|
||||
CONF_VALID_ZONES = ["Zone2", "Zone3"]
|
||||
CONF_ZONES = "zones"
|
||||
|
||||
DEFAULT_SHOW_SOURCES = False
|
||||
DEFAULT_TIMEOUT = 2
|
||||
|
||||
KEY_DENON_CACHE = "denonavr_hosts"
|
||||
|
||||
SUPPORT_DENON = (
|
||||
SUPPORT_VOLUME_STEP
|
||||
| SUPPORT_VOLUME_MUTE
|
||||
|
@ -73,99 +63,32 @@ SUPPORT_MEDIA_MODES = (
|
|||
| SUPPORT_PLAY
|
||||
)
|
||||
|
||||
DENON_ZONE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ZONE): vol.In(CONF_VALID_ZONES, CONF_INVALID_ZONES_ERR),
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES): cv.boolean,
|
||||
vol.Optional(CONF_ZONES): vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA]),
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
NewHost = namedtuple("NewHost", ["host", "name"])
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Denon platform."""
|
||||
# Initialize list with receivers to be started
|
||||
receivers = []
|
||||
|
||||
cache = hass.data.get(KEY_DENON_CACHE)
|
||||
if cache is None:
|
||||
cache = hass.data[KEY_DENON_CACHE] = set()
|
||||
|
||||
# Get config option for show_all_sources and timeout
|
||||
show_all_sources = config[CONF_SHOW_ALL_SOURCES]
|
||||
timeout = config[CONF_TIMEOUT]
|
||||
|
||||
# Get config option for additional zones
|
||||
zones = config.get(CONF_ZONES)
|
||||
if zones is not None:
|
||||
add_zones = {}
|
||||
for entry in zones:
|
||||
add_zones[entry[CONF_ZONE]] = entry.get(CONF_NAME)
|
||||
else:
|
||||
add_zones = None
|
||||
|
||||
# Start assignment of host and name
|
||||
new_hosts = []
|
||||
# 1. option: manual setting
|
||||
if config.get(CONF_HOST) is not None:
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
new_hosts.append(NewHost(host=host, name=name))
|
||||
|
||||
# 2. option: discovery using netdisco
|
||||
if discovery_info is not None:
|
||||
host = discovery_info.get("host")
|
||||
name = discovery_info.get("name")
|
||||
new_hosts.append(NewHost(host=host, name=name))
|
||||
|
||||
# 3. option: discovery using denonavr library
|
||||
if config.get(CONF_HOST) is None and discovery_info is None:
|
||||
d_receivers = denonavr.discover()
|
||||
# More than one receiver could be discovered by that method
|
||||
for d_receiver in d_receivers:
|
||||
host = d_receiver["host"]
|
||||
name = d_receiver["friendlyName"]
|
||||
new_hosts.append(NewHost(host=host, name=name))
|
||||
|
||||
for entry in new_hosts:
|
||||
# Check if host not in cache, append it and save for later
|
||||
# starting
|
||||
if entry.host not in cache:
|
||||
new_device = denonavr.DenonAVR(
|
||||
host=entry.host,
|
||||
name=entry.name,
|
||||
show_all_inputs=show_all_sources,
|
||||
timeout=timeout,
|
||||
add_zones=add_zones,
|
||||
)
|
||||
for new_zone in new_device.zones.values():
|
||||
receivers.append(DenonDevice(new_zone))
|
||||
cache.add(host)
|
||||
_LOGGER.info("Denon receiver at host %s initialized", host)
|
||||
|
||||
# Add all freshly discovered receivers
|
||||
if receivers:
|
||||
add_entities(receivers)
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the DenonAVR receiver from a config entry."""
|
||||
entities = []
|
||||
receiver = hass.data[DOMAIN][config_entry.entry_id][CONF_RECEIVER]
|
||||
for receiver_zone in receiver.zones.values():
|
||||
if config_entry.data[CONF_SERIAL_NUMBER] is not None:
|
||||
unique_id = f"{config_entry.unique_id}-{receiver_zone.zone}"
|
||||
else:
|
||||
unique_id = None
|
||||
entities.append(DenonDevice(receiver_zone, unique_id, config_entry))
|
||||
_LOGGER.debug(
|
||||
"%s receiver at host %s initialized", receiver.manufacturer, receiver.host
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class DenonDevice(MediaPlayerEntity):
|
||||
"""Representation of a Denon Media Player Device."""
|
||||
|
||||
def __init__(self, receiver):
|
||||
def __init__(self, receiver, unique_id, config_entry):
|
||||
"""Initialize the device."""
|
||||
self._receiver = receiver
|
||||
self._name = self._receiver.name
|
||||
self._unique_id = unique_id
|
||||
self._config_entry = config_entry
|
||||
self._muted = self._receiver.muted
|
||||
self._volume = self._receiver.volume
|
||||
self._current_source = self._receiver.input_func
|
||||
|
@ -237,6 +160,30 @@ class DenonDevice(MediaPlayerEntity):
|
|||
self._sound_mode = self._receiver.sound_mode
|
||||
self._sound_mode_raw = self._receiver.sound_mode_raw
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id of the zone."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info of the receiver."""
|
||||
if self._config_entry.data[CONF_SERIAL_NUMBER] is None:
|
||||
return None
|
||||
|
||||
device_info = {
|
||||
"identifiers": {(DOMAIN, self._config_entry.unique_id)},
|
||||
"manufacturer": self._config_entry.data[CONF_MANUFACTURER],
|
||||
"name": self._config_entry.title,
|
||||
"model": f"{self._config_entry.data[CONF_MODEL]}-{self._config_entry.data[CONF_TYPE]}",
|
||||
}
|
||||
if self._config_entry.data[CONF_MAC] is not None:
|
||||
device_info["connections"] = {
|
||||
(dr.CONNECTION_NETWORK_MAC, self._config_entry.data[CONF_MAC])
|
||||
}
|
||||
|
||||
return device_info
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
|
|
71
homeassistant/components/denonavr/receiver.py
Normal file
71
homeassistant/components/denonavr/receiver.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
"""Code to handle a DenonAVR receiver."""
|
||||
import logging
|
||||
|
||||
import denonavr
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConnectDenonAVR:
|
||||
"""Class to async connect to a DenonAVR receiver."""
|
||||
|
||||
def __init__(self, hass, host, timeout, show_all_inputs, zone2, zone3):
|
||||
"""Initialize the class."""
|
||||
self._hass = hass
|
||||
self._receiver = None
|
||||
self._host = host
|
||||
self._show_all_inputs = show_all_inputs
|
||||
self._timeout = timeout
|
||||
|
||||
self._zones = {}
|
||||
if zone2:
|
||||
self._zones["Zone2"] = None
|
||||
if zone3:
|
||||
self._zones["Zone3"] = None
|
||||
|
||||
@property
|
||||
def receiver(self):
|
||||
"""Return the class containing all connections to the receiver."""
|
||||
return self._receiver
|
||||
|
||||
async def async_connect_receiver(self):
|
||||
"""Connect to the DenonAVR receiver."""
|
||||
if not await self._hass.async_add_executor_job(self.init_receiver_class):
|
||||
return False
|
||||
|
||||
if (
|
||||
self._receiver.manufacturer is None
|
||||
or self._receiver.name is None
|
||||
or self._receiver.model_name is None
|
||||
or self._receiver.receiver_type is None
|
||||
):
|
||||
return False
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s receiver %s at host %s connected, model %s, serial %s, type %s",
|
||||
self._receiver.manufacturer,
|
||||
self._receiver.name,
|
||||
self._receiver.host,
|
||||
self._receiver.model_name,
|
||||
self._receiver.serial_number,
|
||||
self._receiver.receiver_type,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def init_receiver_class(self):
|
||||
"""Initialize the DenonAVR class in a way that can called by async_add_executor_job."""
|
||||
try:
|
||||
self._receiver = denonavr.DenonAVR(
|
||||
host=self._host,
|
||||
show_all_inputs=self._show_all_inputs,
|
||||
timeout=self._timeout,
|
||||
add_zones=self._zones,
|
||||
)
|
||||
except ConnectionError:
|
||||
_LOGGER.error(
|
||||
"ConnectionError during setup of denonavr with host %s", self._host
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
48
homeassistant/components/denonavr/strings.json
Normal file
48
homeassistant/components/denonavr/strings.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "Denon AVR Network Receiver: {name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Denon AVR Network Receivers",
|
||||
"description": "Connect to your receiver, if the IP address is not set, auto-discovery is used",
|
||||
"data": {
|
||||
"host": "IP address"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Denon AVR Network Receivers",
|
||||
"description": "Please confirm adding the receiver"
|
||||
},
|
||||
"select": {
|
||||
"title": "Select the receiver that you wish to connect",
|
||||
"description": "Run the setup again if you want to connect additional receivers",
|
||||
"data": {
|
||||
"select_host": "Receiver IP"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"discovery_error": "Failed to discover a Denon AVR Network Receiver"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "Config flow for this Denon AVR is already in progress",
|
||||
"connection_error": "Failed to connect, please try again",
|
||||
"not_denonavr_manufacturer": "Not a Denon AVR Network Receiver, discovered manafucturer did not match",
|
||||
"not_denonavr_missing": "Not a Denon AVR Network Receiver, discovery information not complete"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Denon AVR Network Receivers",
|
||||
"description": "Specify optional settings",
|
||||
"data": {
|
||||
"show_all_sources": "Show all sources",
|
||||
"zone2": "Set up Zone 2",
|
||||
"zone3": "Set up Zone 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
homeassistant/components/denonavr/translations/en.json
Normal file
48
homeassistant/components/denonavr/translations/en.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "Denon AVR Network Receiver: {name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Denon AVR Network Receivers",
|
||||
"description": "Connect to your receiver, if the IP address is not set, auto-discovery is used",
|
||||
"data": {
|
||||
"host": "IP address"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Denon AVR Network Receivers",
|
||||
"description": "Please confirm adding the receiver"
|
||||
},
|
||||
"select": {
|
||||
"title": "Select the receiver that you wish to connect",
|
||||
"description": "Run the setup again if you want to connect additional receivers",
|
||||
"data": {
|
||||
"select_host": "Receiver IP"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"discovery_error": "Failed to discover a Denon AVR Network Receiver"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Config flow for this Denon AVR is already in progress",
|
||||
"connection_error": "Failed to connect, please try again",
|
||||
"not_denonavr_manufacturer": "Not a Denon AVR Network Receiver, discovered manafucturer did not match",
|
||||
"not_denonavr_missing": "Not a Denon AVR Network Receiver, discovery information not complete"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Denon AVR Network Receivers",
|
||||
"description": "Specify optional settings",
|
||||
"data": {
|
||||
"show_all_sources": "Show all sources",
|
||||
"zone2": "Set up Zone 2",
|
||||
"zone3": "Set up Zone 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -66,7 +66,6 @@ SERVICE_HANDLERS = {
|
|||
SERVICE_YEELIGHT: ("yeelight", None),
|
||||
"yamaha": ("media_player", "yamaha"),
|
||||
"logitech_mediaserver": ("media_player", "squeezebox"),
|
||||
"denonavr": ("media_player", "denonavr"),
|
||||
"frontier_silicon": ("media_player", "frontier_silicon"),
|
||||
"openhome": ("media_player", "openhome"),
|
||||
"bose_soundtouch": ("media_player", "soundtouch"),
|
||||
|
@ -82,6 +81,7 @@ OPTIONAL_SERVICE_HANDLERS = {SERVICE_DLNA_DMR: ("media_player", "dlna_dmr")}
|
|||
MIGRATED_SERVICE_HANDLERS = [
|
||||
"axis",
|
||||
"deconz",
|
||||
"denonavr",
|
||||
"esphome",
|
||||
"google_cast",
|
||||
SERVICE_HEOS,
|
||||
|
|
|
@ -31,6 +31,7 @@ FLOWS = [
|
|||
"coronavirus",
|
||||
"daikin",
|
||||
"deconz",
|
||||
"denonavr",
|
||||
"devolo_home_control",
|
||||
"dialogflow",
|
||||
"directv",
|
||||
|
|
|
@ -17,6 +17,44 @@ SSDP = {
|
|||
"manufacturer": "Royal Philips Electronics"
|
||||
}
|
||||
],
|
||||
"denonavr": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
"manufacturer": "Denon"
|
||||
},
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
"manufacturer": "DENON"
|
||||
},
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
"manufacturer": "Marantz"
|
||||
},
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
"manufacturer": "Denon"
|
||||
},
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
"manufacturer": "DENON"
|
||||
},
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
"manufacturer": "Marantz"
|
||||
},
|
||||
{
|
||||
"deviceType": "urn:schemas-denon-com:device:AiosDevice:1",
|
||||
"manufacturer": "Denon"
|
||||
},
|
||||
{
|
||||
"deviceType": "urn:schemas-denon-com:device:AiosDevice:1",
|
||||
"manufacturer": "DENON"
|
||||
},
|
||||
{
|
||||
"deviceType": "urn:schemas-denon-com:device:AiosDevice:1",
|
||||
"manufacturer": "Marantz"
|
||||
}
|
||||
],
|
||||
"directv": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
|
|
@ -472,7 +472,7 @@ defusedxml==0.6.0
|
|||
deluge-client==1.7.1
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==0.8.1
|
||||
denonavr==0.9.3
|
||||
|
||||
# homeassistant.components.devolo_home_control
|
||||
devolo-home-control-api==0.11.0
|
||||
|
@ -634,6 +634,7 @@ georss_ign_sismologia_client==0.2
|
|||
# homeassistant.components.qld_bushfire
|
||||
georss_qld_bushfire_alert_client==0.3
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
# homeassistant.components.huawei_lte
|
||||
# homeassistant.components.kef
|
||||
# homeassistant.components.minecraft_server
|
||||
|
|
|
@ -215,7 +215,7 @@ datapoint==0.9.5
|
|||
defusedxml==0.6.0
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
denonavr==0.8.1
|
||||
denonavr==0.9.3
|
||||
|
||||
# homeassistant.components.devolo_home_control
|
||||
devolo-home-control-api==0.11.0
|
||||
|
@ -281,6 +281,7 @@ georss_ign_sismologia_client==0.2
|
|||
# homeassistant.components.qld_bushfire
|
||||
georss_qld_bushfire_alert_client==0.3
|
||||
|
||||
# homeassistant.components.denonavr
|
||||
# homeassistant.components.huawei_lte
|
||||
# homeassistant.components.kef
|
||||
# homeassistant.components.minecraft_server
|
||||
|
|
561
tests/components/denonavr/test_config_flow.py
Normal file
561
tests/components/denonavr/test_config_flow.py
Normal file
|
@ -0,0 +1,561 @@
|
|||
"""Test the DenonAVR config flow."""
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components import ssdp
|
||||
from homeassistant.components.denonavr.config_flow import (
|
||||
CONF_MANUFACTURER,
|
||||
CONF_MODEL,
|
||||
CONF_SERIAL_NUMBER,
|
||||
CONF_SHOW_ALL_SOURCES,
|
||||
CONF_TYPE,
|
||||
CONF_ZONE2,
|
||||
CONF_ZONE3,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_HOST = "1.2.3.4"
|
||||
TEST_MAC = "ab:cd:ef:gh"
|
||||
TEST_HOST2 = "5.6.7.8"
|
||||
TEST_NAME = "Test_Receiver"
|
||||
TEST_MODEL = "model5"
|
||||
TEST_RECEIVER_TYPE = "avr-x"
|
||||
TEST_SERIALNUMBER = "123456789"
|
||||
TEST_MANUFACTURER = "Denon"
|
||||
TEST_SSDP_LOCATION = f"http://{TEST_HOST}/"
|
||||
TEST_UNIQUE_ID = f"{TEST_MODEL}-{TEST_SERIALNUMBER}"
|
||||
TEST_DISCOVER_1_RECEIVER = [{CONF_HOST: TEST_HOST}]
|
||||
TEST_DISCOVER_2_RECEIVER = [{CONF_HOST: TEST_HOST}, {CONF_HOST: TEST_HOST2}]
|
||||
|
||||
|
||||
@pytest.fixture(name="denonavr_connect", autouse=True)
|
||||
def denonavr_connect_fixture():
|
||||
"""Mock denonavr connection and entry setup."""
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR._update_input_func_list",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR._get_receiver_name",
|
||||
return_value=TEST_NAME,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR._get_support_sound_mode",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR._update_avr_2016",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR._update_avr",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.get_device_info",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.name", TEST_NAME,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.model_name",
|
||||
TEST_MODEL,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
|
||||
TEST_SERIALNUMBER,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.manufacturer",
|
||||
TEST_MANUFACTURER,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.receiver_type",
|
||||
TEST_RECEIVER_TYPE,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.config_flow.get_mac_address",
|
||||
return_value=TEST_MAC,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.async_setup_entry", return_value=True
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_config_flow_manual_host_success(hass):
|
||||
"""
|
||||
Successful flow manually initialized by the user.
|
||||
|
||||
Host specified.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: TEST_HOST},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TEST_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: TEST_MAC,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_manual_discover_1_success(hass):
|
||||
"""
|
||||
Successful flow manually initialized by the user.
|
||||
|
||||
Without the host specified and 1 receiver discovered.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers",
|
||||
return_value=TEST_DISCOVER_1_RECEIVER,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {},)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TEST_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: TEST_MAC,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_manual_discover_2_success(hass):
|
||||
"""
|
||||
Successful flow manually initialized by the user.
|
||||
|
||||
Without the host specified and 2 receiver discovered.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers",
|
||||
return_value=TEST_DISCOVER_2_RECEIVER,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {},)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "select"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"select_host": TEST_HOST2},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TEST_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST2,
|
||||
CONF_MAC: TEST_MAC,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_manual_discover_error(hass):
|
||||
"""
|
||||
Failed flow manually initialized by the user.
|
||||
|
||||
Without the host specified and no receiver discovered.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers",
|
||||
return_value=[],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {},)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "discovery_error"}
|
||||
|
||||
|
||||
async def test_config_flow_manual_host_no_serial(hass):
|
||||
"""
|
||||
Successful flow manually initialized by the user.
|
||||
|
||||
Host specified and an error getting the serial number.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
|
||||
None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: TEST_HOST},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TEST_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: TEST_MAC,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: None,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_manual_host_no_mac(hass):
|
||||
"""
|
||||
Successful flow manually initialized by the user.
|
||||
|
||||
Host specified and an error getting the mac address.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.config_flow.get_mac_address",
|
||||
return_value=None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: TEST_HOST},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TEST_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: None,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_manual_host_no_serial_no_mac(hass):
|
||||
"""
|
||||
Successful flow manually initialized by the user.
|
||||
|
||||
Host specified and an error getting the serial number and mac address.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
|
||||
None,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.config_flow.get_mac_address",
|
||||
return_value=None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: TEST_HOST},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TEST_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: None,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: None,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_manual_host_no_serial_no_mac_exception(hass):
|
||||
"""
|
||||
Successful flow manually initialized by the user.
|
||||
|
||||
Host specified and an error getting the serial number and exception getting mac address.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
|
||||
None,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.config_flow.get_mac_address",
|
||||
side_effect=OSError,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: TEST_HOST},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TEST_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: None,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: None,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_manual_host_connection_error(hass):
|
||||
"""
|
||||
Failed flow manually initialized by the user.
|
||||
|
||||
Host specified and a connection error.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.get_device_info",
|
||||
side_effect=ConnectionError,
|
||||
), patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.receiver_type",
|
||||
None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: TEST_HOST},
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "connection_error"
|
||||
|
||||
|
||||
async def test_config_flow_manual_host_no_device_info(hass):
|
||||
"""
|
||||
Failed flow manually initialized by the user.
|
||||
|
||||
Host specified and no device info (due to receiver power off).
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.receiver_type",
|
||||
None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: TEST_HOST},
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "connection_error"
|
||||
|
||||
|
||||
async def test_config_flow_ssdp(hass):
|
||||
"""Successful flow initialized by ssdp discovery."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_SSDP},
|
||||
data={
|
||||
ssdp.ATTR_UPNP_MANUFACTURER: TEST_MANUFACTURER,
|
||||
ssdp.ATTR_UPNP_MODEL_NAME: TEST_MODEL,
|
||||
ssdp.ATTR_UPNP_SERIAL: TEST_SERIALNUMBER,
|
||||
ssdp.ATTR_SSDP_LOCATION: TEST_SSDP_LOCATION,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {},)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TEST_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: TEST_MAC,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_ssdp_not_denon(hass):
|
||||
"""
|
||||
Failed flow initialized by ssdp discovery.
|
||||
|
||||
Not supported manufacturer.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_SSDP},
|
||||
data={
|
||||
ssdp.ATTR_UPNP_MANUFACTURER: "NotSupported",
|
||||
ssdp.ATTR_UPNP_MODEL_NAME: TEST_MODEL,
|
||||
ssdp.ATTR_UPNP_SERIAL: TEST_SERIALNUMBER,
|
||||
ssdp.ATTR_SSDP_LOCATION: TEST_SSDP_LOCATION,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "not_denonavr_manufacturer"
|
||||
|
||||
|
||||
async def test_config_flow_ssdp_missing_info(hass):
|
||||
"""
|
||||
Failed flow initialized by ssdp discovery.
|
||||
|
||||
Missing information.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_SSDP},
|
||||
data={
|
||||
ssdp.ATTR_UPNP_MANUFACTURER: TEST_MANUFACTURER,
|
||||
ssdp.ATTR_SSDP_LOCATION: TEST_SSDP_LOCATION,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "not_denonavr_missing"
|
||||
|
||||
|
||||
async def test_options_flow(hass):
|
||||
"""Test specifying non default settings using options flow."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_UNIQUE_ID,
|
||||
data={
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: TEST_MAC,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||
},
|
||||
title=TEST_NAME,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, CONF_ZONE3: True},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert config_entry.options == {
|
||||
CONF_SHOW_ALL_SOURCES: True,
|
||||
CONF_ZONE2: True,
|
||||
CONF_ZONE3: True,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_manual_host_no_serial_double_config(hass):
|
||||
"""
|
||||
Failed flow manually initialized by the user twice.
|
||||
|
||||
Host specified and an error getting the serial number.
|
||||
"""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
|
||||
None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: TEST_HOST},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == TEST_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: TEST_MAC,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: None,
|
||||
}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR.serial_number",
|
||||
None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: TEST_HOST},
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
|
@ -1,57 +1,92 @@
|
|||
"""The tests for the denonavr media player platform."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import media_player
|
||||
from homeassistant.components.denonavr import ATTR_COMMAND, DOMAIN, SERVICE_GET_COMMAND
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PLATFORM
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components.denonavr import ATTR_COMMAND, SERVICE_GET_COMMAND
|
||||
from homeassistant.components.denonavr.config_flow import (
|
||||
CONF_MANUFACTURER,
|
||||
CONF_MODEL,
|
||||
CONF_SERIAL_NUMBER,
|
||||
CONF_TYPE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_MAC
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
NAME = "fake"
|
||||
ENTITY_ID = f"{media_player.DOMAIN}.{NAME}"
|
||||
TEST_HOST = "1.2.3.4"
|
||||
TEST_MAC = "ab:cd:ef:gh"
|
||||
TEST_NAME = "Test_Receiver"
|
||||
TEST_MODEL = "model5"
|
||||
TEST_SERIALNUMBER = "123456789"
|
||||
TEST_MANUFACTURER = "Denon"
|
||||
TEST_RECEIVER_TYPE = "avr-x"
|
||||
TEST_ZONE = "Main"
|
||||
TEST_UNIQUE_ID = f"{TEST_MODEL}-{TEST_SERIALNUMBER}"
|
||||
TEST_TIMEOUT = 2
|
||||
TEST_SHOW_ALL_SOURCES = False
|
||||
TEST_ZONE2 = False
|
||||
TEST_ZONE3 = False
|
||||
ENTITY_ID = f"{media_player.DOMAIN}.{TEST_NAME}"
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def client_fixture():
|
||||
"""Patch of client library for tests."""
|
||||
with patch(
|
||||
"homeassistant.components.denonavr.media_player.denonavr.DenonAVR",
|
||||
autospec=True,
|
||||
"homeassistant.components.denonavr.receiver.denonavr.DenonAVR", autospec=True,
|
||||
) as mock_client_class, patch(
|
||||
"homeassistant.components.denonavr.media_player.denonavr.discover"
|
||||
"homeassistant.components.denonavr.receiver.denonavr.discover"
|
||||
):
|
||||
mock_client_class.return_value.name = NAME
|
||||
mock_client_class.return_value.name = TEST_NAME
|
||||
mock_client_class.return_value.model_name = TEST_MODEL
|
||||
mock_client_class.return_value.serial_number = TEST_SERIALNUMBER
|
||||
mock_client_class.return_value.manufacturer = TEST_MANUFACTURER
|
||||
mock_client_class.return_value.receiver_type = TEST_RECEIVER_TYPE
|
||||
mock_client_class.return_value.zone = TEST_ZONE
|
||||
mock_client_class.return_value.input_func_list = []
|
||||
mock_client_class.return_value.sound_mode_list = []
|
||||
mock_client_class.return_value.zones = {"Main": mock_client_class.return_value}
|
||||
yield mock_client_class.return_value
|
||||
|
||||
|
||||
async def setup_denonavr(hass):
|
||||
"""Initialize webostv and media_player for tests."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
media_player.DOMAIN,
|
||||
{
|
||||
media_player.DOMAIN: {
|
||||
CONF_PLATFORM: "denonavr",
|
||||
CONF_HOST: "fake",
|
||||
CONF_NAME: NAME,
|
||||
}
|
||||
},
|
||||
"""Initialize media_player for tests."""
|
||||
entry_data = {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_MAC: TEST_MAC,
|
||||
CONF_MODEL: TEST_MODEL,
|
||||
CONF_TYPE: TEST_RECEIVER_TYPE,
|
||||
CONF_MANUFACTURER: TEST_MANUFACTURER,
|
||||
CONF_SERIAL_NUMBER: TEST_SERIALNUMBER,
|
||||
}
|
||||
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN, unique_id=TEST_UNIQUE_ID, data=entry_data,
|
||||
)
|
||||
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
|
||||
assert state
|
||||
assert state.name == TEST_NAME
|
||||
|
||||
|
||||
async def test_get_command(hass, client):
|
||||
"""Test generic command functionality."""
|
||||
|
||||
await setup_denonavr(hass)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: ENTITY_ID,
|
||||
ATTR_COMMAND: "test",
|
||||
ATTR_COMMAND: "test_command",
|
||||
}
|
||||
await hass.services.async_call(DOMAIN, SERVICE_GET_COMMAND, data)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client.send_get_command.assert_called_with("test")
|
||||
client.send_get_command.assert_called_with("test_command")
|
||||
|
|
Loading…
Add table
Reference in a new issue