Disable creating port mappings from UI, add discovery from component (#18565)

* Disable creating port mappings from UI, add discovery from component

* Remove unused constant

* Upgrade to async_upnp_client==0.13.6 and use manufacturer from device

* Upgrade to async_upnp_client==0.13.7
This commit is contained in:
Steven Looman 2018-12-21 18:25:23 +01:00 committed by Diogo Gomes
parent 5efc61feaf
commit 501b3f9927
12 changed files with 195 additions and 653 deletions

View file

@ -44,6 +44,8 @@ SERVICE_SABNZBD = 'sabnzbd'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
SERVICE_HOMEKIT = 'homekit' SERVICE_HOMEKIT = 'homekit'
SERVICE_OCTOPRINT = 'octoprint' SERVICE_OCTOPRINT = 'octoprint'
SERVICE_IGD = 'igd'
SERVICE_DLNA_DMR = 'dlna_dmr'
CONFIG_ENTRY_HANDLERS = { CONFIG_ENTRY_HANDLERS = {
SERVICE_DAIKIN: 'daikin', SERVICE_DAIKIN: 'daikin',
@ -53,6 +55,7 @@ CONFIG_ENTRY_HANDLERS = {
SERVICE_TELLDUSLIVE: 'tellduslive', SERVICE_TELLDUSLIVE: 'tellduslive',
SERVICE_IKEA_TRADFRI: 'tradfri', SERVICE_IKEA_TRADFRI: 'tradfri',
'sonos': 'sonos', 'sonos': 'sonos',
SERVICE_IGD: 'upnp',
} }
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
@ -92,7 +95,7 @@ SERVICE_HANDLERS = {
OPTIONAL_SERVICE_HANDLERS = { OPTIONAL_SERVICE_HANDLERS = {
SERVICE_HOMEKIT: ('homekit_controller', None), SERVICE_HOMEKIT: ('homekit_controller', None),
'dlna_dmr': ('media_player', 'dlna_dmr'), SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
} }
CONF_IGNORE = 'ignore' CONF_IGNORE = 'ignore'

View file

@ -26,7 +26,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import get_local_ip from homeassistant.util import get_local_ip
REQUIREMENTS = ['async-upnp-client==0.13.2'] REQUIREMENTS = ['async-upnp-client==0.13.7']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -10,7 +10,7 @@ import logging
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.upnp.const import DOMAIN as DATA_UPNP from homeassistant.components.upnp.const import DOMAIN as DOMAIN_UPNP
from homeassistant.components.upnp.const import SIGNAL_REMOVE_SENSOR from homeassistant.components.upnp.const import SIGNAL_REMOVE_SENSOR
@ -73,8 +73,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(sensors, True) async_add_entities(sensors, True)
data = config_entry.data data = config_entry.data
udn = data['udn'] if 'udn' in data:
device = hass.data[DATA_UPNP]['devices'][udn] udn = data['udn']
else:
# any device will do
udn = list(hass.data[DOMAIN_UPNP]['devices'].keys())[0]
device = hass.data[DOMAIN_UPNP]['devices'][udn]
async_add_sensor(device) async_add_sensor(device)
@ -100,6 +105,17 @@ class UpnpSensor(Entity):
self.hass.async_create_task(self.async_remove()) self.hass.async_create_task(self.async_remove())
@property
def device_info(self):
"""Get device info."""
return {
'identifiers': {
(DOMAIN_UPNP, self.unique_id)
},
'name': self.name,
'via_hub': (DOMAIN_UPNP, self._device.udn),
}
class RawUPnPIGDSensor(UpnpSensor): class RawUPnPIGDSensor(UpnpSensor):
"""Representation of a UPnP/IGD sensor.""" """Representation of a UPnP/IGD sensor."""

View file

@ -4,32 +4,31 @@ Will open a port in your router for Home Assistant and provide statistics.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/upnp/ https://home-assistant.io/components/upnp/
""" """
import asyncio
from ipaddress import ip_address from ipaddress import ip_address
import aiohttp
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers import config_entry_flow
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import dispatcher from homeassistant.helpers import dispatcher
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import get_local_ip
from .const import ( from .const import (
CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS, CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS,
CONF_HASS, CONF_LOCAL_IP, CONF_PORTS, CONF_HASS, CONF_LOCAL_IP, CONF_PORTS,
CONF_UDN, CONF_SSDP_DESCRIPTION,
SIGNAL_REMOVE_SENSOR, SIGNAL_REMOVE_SENSOR,
) )
from .const import DOMAIN from .const import DOMAIN
from .const import LOGGER as _LOGGER from .const import LOGGER as _LOGGER
from .config_flow import async_ensure_domain_data
from .device import Device from .device import Device
REQUIREMENTS = ['async-upnp-client==0.13.7']
REQUIREMENTS = ['async-upnp-client==0.13.2']
NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_ID = 'upnp_notification'
NOTIFICATION_TITLE = 'UPnP/IGD Setup' NOTIFICATION_TITLE = 'UPnP/IGD Setup'
@ -83,78 +82,111 @@ def _substitute_hass_ports(ports, hass_port=None):
return ports return ports
# config async def async_discover_and_construct(hass, udn=None) -> Device:
"""Discovery devices and construct a Device for one."""
discovery_infos = await Device.async_discover(hass)
if not discovery_infos:
_LOGGER.info('No UPnP/IGD devices discovered')
return None
if udn:
# get the discovery info with specified UDN
filtered = [di for di in discovery_infos if di['udn'] == udn]
if not filtered:
_LOGGER.warning('Wanted UPnP/IGD device with UDN "%s" not found, '
'aborting', udn)
return None
discovery_info = filtered[0]
else:
# get the first/any
discovery_info = discovery_infos[0]
if len(discovery_infos) > 1:
_LOGGER.info('Detected multiple UPnP/IGD devices, using: %s',
discovery_info['igd_name'])
ssdp_description = discovery_info['ssdp_description']
return await Device.async_create_device(hass, ssdp_description)
async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Register a port mapping for Home Assistant via UPnP.""" """Set up UPnP component."""
await async_ensure_domain_data(hass) conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
conf = config.get(DOMAIN, conf_default)
# ensure sane config local_ip = await hass.async_add_executor_job(get_local_ip)
if DOMAIN not in config: hass.data[DOMAIN] = {
return True 'config': conf,
upnp_config = config[DOMAIN] 'devices': {},
'local_ip': config.get(CONF_LOCAL_IP, local_ip),
# overridden local ip 'ports': conf.get('ports', {}),
if CONF_LOCAL_IP in upnp_config:
hass.data[DOMAIN]['local_ip'] = upnp_config[CONF_LOCAL_IP]
# determine ports
ports = {CONF_HASS: CONF_HASS} # default, port_mapping disabled by default
if CONF_PORTS in upnp_config:
# copy from config
ports = upnp_config[CONF_PORTS]
hass.data[DOMAIN]['auto_config'] = {
'active': True,
'enable_sensors': upnp_config[CONF_ENABLE_SENSORS],
'enable_port_mapping': upnp_config[CONF_ENABLE_PORT_MAPPING],
'ports': ports,
} }
if conf is not None:
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
return True return True
# config flow
async def async_setup_entry(hass: HomeAssistantType, async def async_setup_entry(hass: HomeAssistantType,
config_entry: ConfigEntry): config_entry: ConfigEntry):
"""Set up UPnP/IGD-device from a config entry.""" """Set up UPnP/IGD device from a config entry."""
await async_ensure_domain_data(hass) domain_data = hass.data[DOMAIN]
data = config_entry.data conf = domain_data['config']
# build UPnP/IGD device # discover and construct
ssdp_description = data[CONF_SSDP_DESCRIPTION] device = await async_discover_and_construct(hass,
try: config_entry.data.get('udn'))
device = await Device.async_create_device(hass, ssdp_description) if not device:
except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.info('Unable to create UPnP/IGD, aborting')
_LOGGER.error('Unable to create upnp-device')
return False return False
# 'register'/save UDN
config_entry.data['udn'] = device.udn
hass.data[DOMAIN]['devices'][device.udn] = device hass.data[DOMAIN]['devices'][device.udn] = device
hass.config_entries.async_update_entry(entry=config_entry,
data=config_entry.data)
# port mapping # create device registry entry
if data.get(CONF_ENABLE_PORT_MAPPING): device_registry = await dr.async_get_registry(hass)
local_ip = hass.data[DOMAIN]['local_ip'] device_registry.async_get_or_create(
ports = hass.data[DOMAIN]['auto_config']['ports'] config_entry_id=config_entry.entry_id,
_LOGGER.debug('Enabling port mappings: %s', ports) connections={
(dr.CONNECTION_UPNP, device.udn)
},
identifiers={
(DOMAIN, device.udn)
},
name=device.name,
manufacturer=device.manufacturer,
)
hass_port = None # set up sensors
if hasattr(hass, 'http'): if conf.get(CONF_ENABLE_SENSORS):
hass_port = hass.http.server_port
ports = _substitute_hass_ports(ports, hass_port=hass_port)
await device.async_add_port_mappings(ports, local_ip)
# sensors
if data.get(CONF_ENABLE_SENSORS):
_LOGGER.debug('Enabling sensors') _LOGGER.debug('Enabling sensors')
# register sensor setup handlers # register sensor setup handlers
hass.async_create_task(hass.config_entries.async_forward_entry_setup( hass.async_create_task(hass.config_entries.async_forward_entry_setup(
config_entry, 'sensor')) config_entry, 'sensor'))
# set up port mapping
if conf.get(CONF_ENABLE_PORT_MAPPING):
_LOGGER.debug('Enabling port mapping')
local_ip = domain_data['local_ip']
ports = conf.get('ports', {})
hass_port = None
if hasattr(hass, 'http'):
hass_port = hass.http.server_port
ports = _substitute_hass_ports(ports, hass_port=hass_port)
await device.async_add_port_mappings(ports, local_ip)
# set up port mapping deletion on stop-hook
async def delete_port_mapping(event): async def delete_port_mapping(event):
"""Delete port mapping on quit.""" """Delete port mapping on quit."""
if data.get(CONF_ENABLE_PORT_MAPPING): _LOGGER.debug('Deleting port mappings')
_LOGGER.debug('Deleting port mappings') await device.async_delete_port_mappings()
await device.async_delete_port_mappings()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, delete_port_mapping) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, delete_port_mapping)
return True return True
@ -162,25 +194,23 @@ async def async_setup_entry(hass: HomeAssistantType,
async def async_unload_entry(hass: HomeAssistantType, async def async_unload_entry(hass: HomeAssistantType,
config_entry: ConfigEntry): config_entry: ConfigEntry):
"""Unload a config entry.""" """Unload a UPnP/IGD device from a config entry."""
data = config_entry.data udn = config_entry.data['udn']
udn = data[CONF_UDN]
if udn not in hass.data[DOMAIN]['devices']:
return True
device = hass.data[DOMAIN]['devices'][udn] device = hass.data[DOMAIN]['devices'][udn]
# port mapping # remove port mapping
if data.get(CONF_ENABLE_PORT_MAPPING): _LOGGER.debug('Deleting port mappings')
_LOGGER.debug('Deleting port mappings') await device.async_delete_port_mappings()
await device.async_delete_port_mappings()
# sensors # remove sensors
if data.get(CONF_ENABLE_SENSORS): _LOGGER.debug('Deleting sensors')
_LOGGER.debug('Deleting sensors') dispatcher.async_dispatcher_send(hass, SIGNAL_REMOVE_SENSOR, device)
dispatcher.async_dispatcher_send(hass, SIGNAL_REMOVE_SENSOR, device)
# clear stored device
del hass.data[DOMAIN]['devices'][udn]
return True return True
config_entry_flow.register_discovery_flow(
DOMAIN,
'UPnP/IGD',
Device.async_discover,
config_entries.CONN_CLASS_LOCAL_POLL)

View file

@ -1,179 +0,0 @@
"""Config flow for UPNP."""
import logging
from collections import OrderedDict
import voluptuous as vol
from homeassistant import config_entries
from homeassistant import data_entry_flow
from homeassistant.util import get_local_ip
from .const import (
CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS,
CONF_SSDP_DESCRIPTION, CONF_UDN
)
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_ensure_domain_data(hass):
"""Ensure hass.data is filled properly."""
hass.data[DOMAIN] = hass.data.get(DOMAIN, {})
hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {})
hass.data[DOMAIN]['discovered'] = hass.data[DOMAIN].get('discovered', {})
hass.data[DOMAIN]['auto_config'] = hass.data[DOMAIN].get('auto_config', {
'active': False,
'enable_sensors': False,
'enable_port_mapping': False,
'ports': {'hass': 'hass'},
})
if 'local_ip' not in hass.data[DOMAIN]:
hass.data[DOMAIN]['local_ip'] = \
await hass.async_add_executor_job(get_local_ip)
@config_entries.HANDLERS.register(DOMAIN)
class UpnpFlowHandler(data_entry_flow.FlowHandler):
"""Handle a UPnP/IGD config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
@property
def _configured_upnp_igds(self):
"""Get all configured IGDs."""
return {
entry.data[CONF_UDN]: {
'udn': entry.data[CONF_UDN],
}
for entry in self.hass.config_entries.async_entries(DOMAIN)
}
@property
def _discovered_upnp_igds(self):
"""Get all discovered entries."""
return self.hass.data[DOMAIN]['discovered']
def _store_discovery_info(self, discovery_info):
"""Add discovery info."""
udn = discovery_info['udn']
self.hass.data[DOMAIN]['discovered'][udn] = discovery_info
def _auto_config_settings(self):
"""Check if auto_config has been enabled."""
return self.hass.data[DOMAIN]['auto_config']
async def async_step_discovery(self, discovery_info):
"""
Handle a discovered UPnP/IGD.
This flow is triggered by the discovery component. It will check if the
host is already configured and delegate to the import step if not.
"""
await async_ensure_domain_data(self.hass)
if not discovery_info.get('udn') or not discovery_info.get('host'):
# Silently ignore incomplete/broken devices to prevent constant
# errors/warnings
_LOGGER.debug('UPnP device is missing the udn. Provided info: %r',
discovery_info)
return self.async_abort(reason='incomplete_device')
# store discovered device
discovery_info['friendly_name'] = discovery_info.get('host', '')
# add name if available
if discovery_info.get('name'):
discovery_info['friendly_name'] += ' ({name})'.format(
**discovery_info)
self._store_discovery_info(discovery_info)
# ensure not already discovered/configured
if discovery_info.get('udn') in self._configured_upnp_igds:
return self.async_abort(reason='already_configured')
# auto config?
auto_config = self._auto_config_settings()
if auto_config['active']:
import_info = {
'name': discovery_info['friendly_name'],
'enable_sensors': auto_config['enable_sensors'],
'enable_port_mapping': auto_config['enable_port_mapping'],
}
return await self._async_save_entry(import_info)
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Manual set up."""
await async_ensure_domain_data(self.hass)
# if user input given, handle it
user_input = user_input or {}
if 'name' in user_input:
if not user_input['enable_sensors'] and \
not user_input['enable_port_mapping']:
return self.async_abort(reason='no_sensors_or_port_mapping')
# ensure not already configured
configured_names = [
entry['friendly_name']
for udn, entry in self._discovered_upnp_igds.items()
if udn in self._configured_upnp_igds
]
if user_input['name'] in configured_names:
return self.async_abort(reason='already_configured')
return await self._async_save_entry(user_input)
# let user choose from all discovered, non-configured, UPnP/IGDs
names = [
entry['friendly_name']
for udn, entry in self._discovered_upnp_igds.items()
if udn not in self._configured_upnp_igds
]
if not names:
return self.async_abort(reason='no_devices_discovered')
return self.async_show_form(
step_id='user',
data_schema=vol.Schema(
OrderedDict([
(vol.Required('name'), vol.In(names)),
(vol.Optional('enable_sensors', default=False), bool),
(vol.Optional('enable_port_mapping', default=False), bool),
])
))
async def async_step_import(self, import_info):
"""Import a new UPnP/IGD as a config entry."""
await async_ensure_domain_data(self.hass)
return await self._async_save_entry(import_info)
async def _async_save_entry(self, import_info):
"""Store UPNP/IGD as new entry."""
await async_ensure_domain_data(self.hass)
# ensure we know the host
name = import_info['name']
discovery_infos = [info
for info in self._discovered_upnp_igds.values()
if info['friendly_name'] == name]
if not discovery_infos:
return self.async_abort(reason='host_not_found')
discovery_info = discovery_infos[0]
return self.async_create_entry(
title=discovery_info['name'],
data={
CONF_SSDP_DESCRIPTION: discovery_info['ssdp_description'],
CONF_UDN: discovery_info['udn'],
CONF_ENABLE_SENSORS: import_info['enable_sensors'],
CONF_ENABLE_PORT_MAPPING: import_info['enable_port_mapping'],
},
)

View file

@ -7,8 +7,6 @@ CONF_ENABLE_SENSORS = 'sensors'
CONF_HASS = 'hass' CONF_HASS = 'hass'
CONF_LOCAL_IP = 'local_ip' CONF_LOCAL_IP = 'local_ip'
CONF_PORTS = 'ports' CONF_PORTS = 'ports'
CONF_SSDP_DESCRIPTION = 'ssdp_description'
CONF_UDN = 'udn'
DOMAIN = 'upnp' DOMAIN = 'upnp'
LOGGER = logging.getLogger('homeassistant.components.upnp') LOGGER = logging.getLogger('homeassistant.components.upnp')
SIGNAL_REMOVE_SENSOR = 'upnp_remove_sensor' SIGNAL_REMOVE_SENSOR = 'upnp_remove_sensor'

View file

@ -18,6 +18,27 @@ class Device:
self._igd_device = igd_device self._igd_device = igd_device
self._mapped_ports = [] self._mapped_ports = []
@classmethod
async def async_discover(cls, hass: HomeAssistantType):
"""Discovery UPNP/IGD devices."""
_LOGGER.debug('Discovering UPnP/IGD devices')
# discover devices
from async_upnp_client.igd import IgdDevice
discovery_infos = await IgdDevice.async_discover()
# add extra info and store devices
devices = []
for discovery_info in discovery_infos:
discovery_info['udn'] = discovery_info['usn'].split('::')[0]
discovery_info['ssdp_description'] = discovery_info['location']
discovery_info['source'] = 'async_upnp_client'
_LOGGER.debug('Discovered device: %s', discovery_info)
devices.append(discovery_info)
return devices
@classmethod @classmethod
async def async_create_device(cls, async def async_create_device(cls,
hass: HomeAssistantType, hass: HomeAssistantType,
@ -34,7 +55,7 @@ class Device:
disable_state_variable_validation=True) disable_state_variable_validation=True)
upnp_device = await factory.async_create_device(ssdp_description) upnp_device = await factory.async_create_device(ssdp_description)
# wrap with async_upnp_client IgdDevice # wrap with async_upnp_client.IgdDevice
from async_upnp_client.igd import IgdDevice from async_upnp_client.igd import IgdDevice
igd_device = IgdDevice(upnp_device, None) igd_device = IgdDevice(upnp_device, None)
@ -50,6 +71,11 @@ class Device:
"""Get the name.""" """Get the name."""
return self._igd_device.name return self._igd_device.name
@property
def manufacturer(self):
"""Get the manufacturer."""
return self._igd_device.manufacturer
async def async_add_port_mappings(self, ports, local_ip): async def async_add_port_mappings(self, ports, local_ip):
"""Add port mappings.""" """Add port mappings."""
if local_ip == '127.0.0.1': if local_ip == '127.0.0.1':

View file

@ -1,24 +1,15 @@
{ {
"config": { "config": {
"title": "UPnP/IGD",
"step": {
"confirm": {
"title": "UPnP/IGD", "title": "UPnP/IGD",
"step": { "description": "Do you want to set up UPnP/IGD?"
"init": { }
"title": "UPnP/IGD" },
}, "abort": {
"user": { "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary.",
"title": "Configuration options for the UPnP/IGD", "no_devices_found": "No UPnP/IGD devices found on the network."
"data":{
"igd": "UPnP/IGD",
"enable_sensors": "Add traffic sensors",
"enable_port_mapping": "Enable port mapping for Home Assistant"
}
}
},
"abort": {
"no_devices_discovered": "No UPnP/IGDs discovered",
"incomplete_device": "Ignoring incomplete UPnP device",
"already_configured": "UPnP/IGD is already configured",
"no_sensors_or_port_mapping": "Enable at least sensors or port mapping"
}
} }
}
} }

View file

@ -19,6 +19,7 @@ STORAGE_VERSION = 1
SAVE_DELAY = 10 SAVE_DELAY = 10
CONNECTION_NETWORK_MAC = 'mac' CONNECTION_NETWORK_MAC = 'mac'
CONNECTION_UPNP = 'upnp'
CONNECTION_ZIGBEE = 'zigbee' CONNECTION_ZIGBEE = 'zigbee'

View file

@ -161,7 +161,7 @@ asterisk_mbox==0.5.0
# homeassistant.components.upnp # homeassistant.components.upnp
# homeassistant.components.media_player.dlna_dmr # homeassistant.components.media_player.dlna_dmr
async-upnp-client==0.13.2 async-upnp-client==0.13.7
# homeassistant.components.light.avion # homeassistant.components.light.avion
# avion==0.10 # avion==0.10

View file

@ -1,261 +0,0 @@
"""Tests for UPnP/IGD config flow."""
from homeassistant.components import upnp
from homeassistant.components.upnp import config_flow as upnp_config_flow
from tests.common import MockConfigEntry
async def test_flow_none_discovered(hass):
"""Test no device discovered flow."""
flow = upnp_config_flow.UpnpFlowHandler()
flow.hass = hass
hass.data[upnp.DOMAIN] = {
'discovered': {}
}
result = await flow.async_step_user()
assert result['type'] == 'abort'
assert result['reason'] == 'no_devices_discovered'
async def test_flow_already_configured(hass):
"""Test device already configured flow."""
flow = upnp_config_flow.UpnpFlowHandler()
flow.hass = hass
# discovered device
udn = 'uuid:device_1'
hass.data[upnp.DOMAIN] = {
'discovered': {
udn: {
'friendly_name': '192.168.1.1 (Test device)',
'host': '192.168.1.1',
'udn': udn,
},
},
}
# configured entry
MockConfigEntry(domain=upnp.DOMAIN, data={
'udn': udn,
'host': '192.168.1.1',
}).add_to_hass(hass)
result = await flow.async_step_user({
'name': '192.168.1.1 (Test device)',
'enable_sensors': True,
'enable_port_mapping': False,
})
assert result['type'] == 'abort'
assert result['reason'] == 'already_configured'
async def test_flow_no_sensors_no_port_mapping(hass):
"""Test single device, no sensors, no port_mapping."""
flow = upnp_config_flow.UpnpFlowHandler()
flow.hass = hass
# discovered device
udn = 'uuid:device_1'
hass.data[upnp.DOMAIN] = {
'discovered': {
udn: {
'friendly_name': '192.168.1.1 (Test device)',
'host': '192.168.1.1',
'udn': udn,
},
},
}
# configured entry
MockConfigEntry(domain=upnp.DOMAIN, data={
'udn': udn,
'host': '192.168.1.1',
}).add_to_hass(hass)
result = await flow.async_step_user({
'name': '192.168.1.1 (Test device)',
'enable_sensors': False,
'enable_port_mapping': False,
})
assert result['type'] == 'abort'
assert result['reason'] == 'no_sensors_or_port_mapping'
async def test_flow_discovered_form(hass):
"""Test single device discovered, show form flow."""
flow = upnp_config_flow.UpnpFlowHandler()
flow.hass = hass
# discovered device
udn = 'uuid:device_1'
hass.data[upnp.DOMAIN] = {
'discovered': {
udn: {
'friendly_name': '192.168.1.1 (Test device)',
'host': '192.168.1.1',
'udn': udn,
},
},
}
result = await flow.async_step_user()
assert result['type'] == 'form'
assert result['step_id'] == 'user'
async def test_flow_two_discovered_form(hass):
"""Test two devices discovered, show form flow with two devices."""
flow = upnp_config_flow.UpnpFlowHandler()
flow.hass = hass
# discovered device
udn_1 = 'uuid:device_1'
udn_2 = 'uuid:device_2'
hass.data[upnp.DOMAIN] = {
'discovered': {
udn_1: {
'friendly_name': '192.168.1.1 (Test device)',
'host': '192.168.1.1',
'udn': udn_1,
},
udn_2: {
'friendly_name': '192.168.2.1 (Test device)',
'host': '192.168.2.1',
'udn': udn_2,
},
},
}
result = await flow.async_step_user()
assert result['type'] == 'form'
assert result['step_id'] == 'user'
assert result['data_schema']({
'name': '192.168.1.1 (Test device)',
'enable_sensors': True,
'enable_port_mapping': False,
})
assert result['data_schema']({
'name': '192.168.2.1 (Test device)',
'enable_sensors': True,
'enable_port_mapping': False,
})
async def test_config_entry_created(hass):
"""Test config entry is created."""
flow = upnp_config_flow.UpnpFlowHandler()
flow.hass = hass
# discovered device
hass.data[upnp.DOMAIN] = {
'discovered': {
'uuid:device_1': {
'friendly_name': '192.168.1.1 (Test device)',
'name': 'Test device 1',
'host': '192.168.1.1',
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': 'uuid:device_1',
},
},
}
result = await flow.async_step_user({
'name': '192.168.1.1 (Test device)',
'enable_sensors': True,
'enable_port_mapping': False,
})
assert result['type'] == 'create_entry'
assert result['data'] == {
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': 'uuid:device_1',
'port_mapping': False,
'sensors': True,
}
assert result['title'] == 'Test device 1'
async def test_flow_discovery_no_data(hass):
"""Test creation of device with auto_config."""
flow = upnp_config_flow.UpnpFlowHandler()
flow.hass = hass
# auto_config active
hass.data[upnp.DOMAIN] = {
'auto_config': {
'active': True,
'enable_port_mapping': False,
'enable_sensors': True,
},
}
# discovered device
result = await flow.async_step_discovery({})
assert result['type'] == 'abort'
assert result['reason'] == 'incomplete_device'
async def test_flow_discovery_auto_config_sensors(hass):
"""Test creation of device with auto_config."""
flow = upnp_config_flow.UpnpFlowHandler()
flow.hass = hass
# auto_config active
hass.data[upnp.DOMAIN] = {
'auto_config': {
'active': True,
'enable_port_mapping': False,
'enable_sensors': True,
},
}
# discovered device
result = await flow.async_step_discovery({
'name': 'Test device 1',
'host': '192.168.1.1',
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': 'uuid:device_1',
})
assert result['type'] == 'create_entry'
assert result['data'] == {
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': 'uuid:device_1',
'sensors': True,
'port_mapping': False,
}
assert result['title'] == 'Test device 1'
async def test_flow_discovery_auto_config_sensors_port_mapping(hass):
"""Test creation of device with auto_config, with port mapping."""
flow = upnp_config_flow.UpnpFlowHandler()
flow.hass = hass
# auto_config active, with port_mapping
hass.data[upnp.DOMAIN] = {
'auto_config': {
'active': True,
'enable_port_mapping': True,
'enable_sensors': True,
},
}
# discovered device
result = await flow.async_step_discovery({
'name': 'Test device 1',
'host': '192.168.1.1',
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': 'uuid:device_1',
})
assert result['type'] == 'create_entry'
assert result['data'] == {
'udn': 'uuid:device_1',
'ssdp_description': 'http://192.168.1.1/desc.xml',
'sensors': True,
'port_mapping': True,
}
assert result['title'] == 'Test device 1'

View file

@ -26,7 +26,7 @@ class MockDevice(Device):
@classmethod @classmethod
async def async_create_device(cls, hass, ssdp_description): async def async_create_device(cls, hass, ssdp_description):
"""Return self.""" """Return self."""
return cls() return cls('UDN')
@property @property
def udn(self): def udn(self):
@ -47,102 +47,10 @@ class MockDevice(Device):
self.removed_port_mappings.append(entry) self.removed_port_mappings.append(entry)
async def test_async_setup_no_auto_config(hass):
"""Test async_setup."""
# setup component, enable auto_config
config = {
'discovery': {},
# no upnp
}
with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.config_flow.get_local_ip',
return_value='192.168.1.10'):
await async_setup_component(hass, 'upnp', config)
await hass.async_block_till_done()
assert hass.data[upnp.DOMAIN]['auto_config'] == {
'active': False,
'enable_sensors': False,
'enable_port_mapping': False,
'ports': {'hass': 'hass'},
}
async def test_async_setup_auto_config(hass):
"""Test async_setup."""
# setup component, enable auto_config
config = {
'discovery': {},
'upnp': {},
}
with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.config_flow.get_local_ip',
return_value='192.168.1.10'):
await async_setup_component(hass, 'upnp', config)
await hass.async_block_till_done()
assert hass.data[upnp.DOMAIN]['auto_config'] == {
'active': True,
'enable_sensors': True,
'enable_port_mapping': False,
'ports': {'hass': 'hass'},
}
async def test_async_setup_auto_config_port_mapping(hass):
"""Test async_setup."""
# setup component, enable auto_config
config = {
'discovery': {},
'upnp': {
'port_mapping': True,
'ports': {'hass': 'hass'},
},
}
with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.config_flow.get_local_ip',
return_value='192.168.1.10'):
await async_setup_component(hass, 'upnp', config)
await hass.async_block_till_done()
assert hass.data[upnp.DOMAIN]['auto_config'] == {
'active': True,
'enable_sensors': True,
'enable_port_mapping': True,
'ports': {'hass': 'hass'},
}
async def test_async_setup_auto_config_no_sensors(hass):
"""Test async_setup."""
# setup component, enable auto_config
config = {
'discovery': {},
'upnp': {'sensors': False},
}
with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.config_flow.get_local_ip',
return_value='192.168.1.10'):
await async_setup_component(hass, 'upnp', config)
await hass.async_block_till_done()
assert hass.data[upnp.DOMAIN]['auto_config'] == {
'active': True,
'enable_sensors': False,
'enable_port_mapping': False,
'ports': {'hass': 'hass'},
}
async def test_async_setup_entry_default(hass): async def test_async_setup_entry_default(hass):
"""Test async_setup_entry.""" """Test async_setup_entry."""
udn = 'uuid:device_1' udn = 'uuid:device_1'
entry = MockConfigEntry(domain=upnp.DOMAIN, data={ entry = MockConfigEntry(domain=upnp.DOMAIN)
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': udn,
'sensors': True,
'port_mapping': False,
})
config = { config = {
'http': {}, 'http': {},
@ -150,7 +58,7 @@ async def test_async_setup_entry_default(hass):
# no upnp # no upnp
} }
with MockDependency('netdisco.discovery'), \ with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.config_flow.get_local_ip', patch('homeassistant.components.upnp.get_local_ip',
return_value='192.168.1.10'): return_value='192.168.1.10'):
await async_setup_component(hass, 'http', config) await async_setup_component(hass, 'http', config)
await async_setup_component(hass, 'upnp', config) await async_setup_component(hass, 'upnp', config)
@ -158,17 +66,23 @@ async def test_async_setup_entry_default(hass):
# mock homeassistant.components.upnp.device.Device # mock homeassistant.components.upnp.device.Device
mock_device = MockDevice(udn) mock_device = MockDevice(udn)
with patch.object(Device, 'async_create_device') as create_device: discovery_infos = [{
'udn': udn,
'ssdp_description': 'http://192.168.1.1/desc.xml',
}]
with patch.object(Device, 'async_create_device') as create_device, \
patch.object(Device, 'async_discover') as async_discover: # noqa:E125
create_device.return_value = mock_coro(return_value=mock_device) create_device.return_value = mock_coro(return_value=mock_device)
with patch('homeassistant.components.upnp.config_flow.get_local_ip', async_discover.return_value = mock_coro(return_value=discovery_infos)
return_value='192.168.1.10'):
assert await upnp.async_setup_entry(hass, entry) is True
# ensure device is stored/used assert await upnp.async_setup_entry(hass, entry) is True
assert hass.data[upnp.DOMAIN]['devices'][udn] == mock_device
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) # ensure device is stored/used
await hass.async_block_till_done() assert hass.data[upnp.DOMAIN]['devices'][udn] == mock_device
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
# ensure no port-mappings created or removed # ensure no port-mappings created or removed
assert not mock_device.added_port_mappings assert not mock_device.added_port_mappings
@ -177,13 +91,9 @@ async def test_async_setup_entry_default(hass):
async def test_async_setup_entry_port_mapping(hass): async def test_async_setup_entry_port_mapping(hass):
"""Test async_setup_entry.""" """Test async_setup_entry."""
# pylint: disable=invalid-name
udn = 'uuid:device_1' udn = 'uuid:device_1'
entry = MockConfigEntry(domain=upnp.DOMAIN, data={ entry = MockConfigEntry(domain=upnp.DOMAIN)
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': udn,
'sensors': False,
'port_mapping': True,
})
config = { config = {
'http': {}, 'http': {},
@ -194,15 +104,22 @@ async def test_async_setup_entry_port_mapping(hass):
}, },
} }
with MockDependency('netdisco.discovery'), \ with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.config_flow.get_local_ip', patch('homeassistant.components.upnp.get_local_ip',
return_value='192.168.1.10'): return_value='192.168.1.10'):
await async_setup_component(hass, 'http', config) await async_setup_component(hass, 'http', config)
await async_setup_component(hass, 'upnp', config) await async_setup_component(hass, 'upnp', config)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_device = MockDevice(udn) mock_device = MockDevice(udn)
with patch.object(Device, 'async_create_device') as create_device: discovery_infos = [{
'udn': udn,
'ssdp_description': 'http://192.168.1.1/desc.xml',
}]
with patch.object(Device, 'async_create_device') as create_device, \
patch.object(Device, 'async_discover') as async_discover: # noqa:E125
create_device.return_value = mock_coro(return_value=mock_device) create_device.return_value = mock_coro(return_value=mock_device)
async_discover.return_value = mock_coro(return_value=discovery_infos)
assert await upnp.async_setup_entry(hass, entry) is True assert await upnp.async_setup_entry(hass, entry) is True