diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 5ba5006aeda..17425309943 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -44,6 +44,8 @@ SERVICE_SABNZBD = 'sabnzbd' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HOMEKIT = 'homekit' SERVICE_OCTOPRINT = 'octoprint' +SERVICE_IGD = 'igd' +SERVICE_DLNA_DMR = 'dlna_dmr' CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: 'daikin', @@ -53,6 +55,7 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_TELLDUSLIVE: 'tellduslive', SERVICE_IKEA_TRADFRI: 'tradfri', 'sonos': 'sonos', + SERVICE_IGD: 'upnp', } SERVICE_HANDLERS = { @@ -92,7 +95,7 @@ SERVICE_HANDLERS = { OPTIONAL_SERVICE_HANDLERS = { SERVICE_HOMEKIT: ('homekit_controller', None), - 'dlna_dmr': ('media_player', 'dlna_dmr'), + SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'), } CONF_IGNORE = 'ignore' diff --git a/homeassistant/components/media_player/dlna_dmr.py b/homeassistant/components/media_player/dlna_dmr.py index 941b8844f86..cb8d3f50d3a 100644 --- a/homeassistant/components/media_player/dlna_dmr.py +++ b/homeassistant/components/media_player/dlna_dmr.py @@ -26,7 +26,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv 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__) diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/upnp.py index c05e2ce0ade..8eccf8834a1 100644 --- a/homeassistant/components/sensor/upnp.py +++ b/homeassistant/components/sensor/upnp.py @@ -10,7 +10,7 @@ import logging from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 @@ -73,8 +73,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, True) data = config_entry.data - udn = data['udn'] - device = hass.data[DATA_UPNP]['devices'][udn] + if 'udn' in data: + 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) @@ -100,6 +105,17 @@ class UpnpSensor(Entity): 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): """Representation of a UPnP/IGD sensor.""" diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 925ca561eb9..e37d237697b 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -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 https://home-assistant.io/components/upnp/ """ -import asyncio from ipaddress import ip_address -import aiohttp import voluptuous as vol +from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry 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 device_registry as dr from homeassistant.helpers import dispatcher from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import get_local_ip from .const import ( CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS, CONF_HASS, CONF_LOCAL_IP, CONF_PORTS, - CONF_UDN, CONF_SSDP_DESCRIPTION, SIGNAL_REMOVE_SENSOR, ) from .const import DOMAIN from .const import LOGGER as _LOGGER -from .config_flow import async_ensure_domain_data from .device import Device - -REQUIREMENTS = ['async-upnp-client==0.13.2'] +REQUIREMENTS = ['async-upnp-client==0.13.7'] NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' @@ -83,78 +82,111 @@ def _substitute_hass_ports(ports, hass_port=None): 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): - """Register a port mapping for Home Assistant via UPnP.""" - await async_ensure_domain_data(hass) - - # ensure sane config - if DOMAIN not in config: - return True - upnp_config = config[DOMAIN] - - # overridden local ip - 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, + """Set up UPnP component.""" + conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] + conf = config.get(DOMAIN, conf_default) + local_ip = await hass.async_add_executor_job(get_local_ip) + hass.data[DOMAIN] = { + 'config': conf, + 'devices': {}, + 'local_ip': config.get(CONF_LOCAL_IP, local_ip), + 'ports': conf.get('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 -# config flow async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): - """Set up UPnP/IGD-device from a config entry.""" - await async_ensure_domain_data(hass) - data = config_entry.data + """Set up UPnP/IGD device from a config entry.""" + domain_data = hass.data[DOMAIN] + conf = domain_data['config'] - # build UPnP/IGD device - ssdp_description = data[CONF_SSDP_DESCRIPTION] - try: - device = await Device.async_create_device(hass, ssdp_description) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error('Unable to create upnp-device') + # discover and construct + device = await async_discover_and_construct(hass, + config_entry.data.get('udn')) + if not device: + _LOGGER.info('Unable to create UPnP/IGD, aborting') return False + # 'register'/save UDN + config_entry.data['udn'] = device.udn hass.data[DOMAIN]['devices'][device.udn] = device + hass.config_entries.async_update_entry(entry=config_entry, + data=config_entry.data) - # port mapping - if data.get(CONF_ENABLE_PORT_MAPPING): - local_ip = hass.data[DOMAIN]['local_ip'] - ports = hass.data[DOMAIN]['auto_config']['ports'] - _LOGGER.debug('Enabling port mappings: %s', ports) + # create device registry entry + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={ + (dr.CONNECTION_UPNP, device.udn) + }, + identifiers={ + (DOMAIN, device.udn) + }, + name=device.name, + manufacturer=device.manufacturer, + ) - 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) - - # sensors - if data.get(CONF_ENABLE_SENSORS): + # set up sensors + if conf.get(CONF_ENABLE_SENSORS): _LOGGER.debug('Enabling sensors') # register sensor setup handlers hass.async_create_task(hass.config_entries.async_forward_entry_setup( 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): """Delete port mapping on quit.""" - if data.get(CONF_ENABLE_PORT_MAPPING): - _LOGGER.debug('Deleting port mappings') - await device.async_delete_port_mappings() + _LOGGER.debug('Deleting port mappings') + await device.async_delete_port_mappings() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, delete_port_mapping) return True @@ -162,25 +194,23 @@ async def async_setup_entry(hass: HomeAssistantType, async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry): - """Unload a config entry.""" - data = config_entry.data - udn = data[CONF_UDN] - - if udn not in hass.data[DOMAIN]['devices']: - return True + """Unload a UPnP/IGD device from a config entry.""" + udn = config_entry.data['udn'] device = hass.data[DOMAIN]['devices'][udn] - # port mapping - if data.get(CONF_ENABLE_PORT_MAPPING): - _LOGGER.debug('Deleting port mappings') - await device.async_delete_port_mappings() + # remove port mapping + _LOGGER.debug('Deleting port mappings') + await device.async_delete_port_mappings() - # sensors - if data.get(CONF_ENABLE_SENSORS): - _LOGGER.debug('Deleting sensors') - dispatcher.async_dispatcher_send(hass, SIGNAL_REMOVE_SENSOR, device) - - # clear stored device - del hass.data[DOMAIN]['devices'][udn] + # remove sensors + _LOGGER.debug('Deleting sensors') + dispatcher.async_dispatcher_send(hass, SIGNAL_REMOVE_SENSOR, device) return True + + +config_entry_flow.register_discovery_flow( + DOMAIN, + 'UPnP/IGD', + Device.async_discover, + config_entries.CONN_CLASS_LOCAL_POLL) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py deleted file mode 100644 index 1a6526638ba..00000000000 --- a/homeassistant/components/upnp/config_flow.py +++ /dev/null @@ -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'], - }, - ) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 7a906ae02be..04932488acd 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -7,8 +7,6 @@ CONF_ENABLE_SENSORS = 'sensors' CONF_HASS = 'hass' CONF_LOCAL_IP = 'local_ip' CONF_PORTS = 'ports' -CONF_SSDP_DESCRIPTION = 'ssdp_description' -CONF_UDN = 'udn' DOMAIN = 'upnp' LOGGER = logging.getLogger('homeassistant.components.upnp') SIGNAL_REMOVE_SENSOR = 'upnp_remove_sensor' diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 4a0b7b61dd4..a99123129aa 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -18,6 +18,27 @@ class Device: self._igd_device = igd_device 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 async def async_create_device(cls, hass: HomeAssistantType, @@ -34,7 +55,7 @@ class Device: disable_state_variable_validation=True) 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 igd_device = IgdDevice(upnp_device, None) @@ -50,6 +71,11 @@ class Device: """Get the 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): """Add port mappings.""" if local_ip == '127.0.0.1': diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index 40bcb46d386..f4de9ad4c0d 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -1,24 +1,15 @@ { - "config": { + "config": { + "title": "UPnP/IGD", + "step": { + "confirm": { "title": "UPnP/IGD", - "step": { - "init": { - "title": "UPnP/IGD" - }, - "user": { - "title": "Configuration options for the UPnP/IGD", - "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" - } + "description": "Do you want to set up UPnP/IGD?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary.", + "no_devices_found": "No UPnP/IGD devices found on the network." } + } } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 78d15e57f38..ce3700ea174 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -19,6 +19,7 @@ STORAGE_VERSION = 1 SAVE_DELAY = 10 CONNECTION_NETWORK_MAC = 'mac' +CONNECTION_UPNP = 'upnp' CONNECTION_ZIGBEE = 'zigbee' diff --git a/requirements_all.txt b/requirements_all.txt index f0b3e9d43c4..d1c3a16659c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr -async-upnp-client==0.13.2 +async-upnp-client==0.13.7 # homeassistant.components.light.avion # avion==0.10 diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py deleted file mode 100644 index 968c59955e2..00000000000 --- a/tests/components/upnp/test_config_flow.py +++ /dev/null @@ -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' diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 6b2611b2509..8762811059a 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -26,7 +26,7 @@ class MockDevice(Device): @classmethod async def async_create_device(cls, hass, ssdp_description): """Return self.""" - return cls() + return cls('UDN') @property def udn(self): @@ -47,102 +47,10 @@ class MockDevice(Device): 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): """Test async_setup_entry.""" udn = 'uuid:device_1' - entry = MockConfigEntry(domain=upnp.DOMAIN, data={ - 'ssdp_description': 'http://192.168.1.1/desc.xml', - 'udn': udn, - 'sensors': True, - 'port_mapping': False, - }) + entry = MockConfigEntry(domain=upnp.DOMAIN) config = { 'http': {}, @@ -150,7 +58,7 @@ async def test_async_setup_entry_default(hass): # no upnp } 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'): await async_setup_component(hass, 'http', 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_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) - with patch('homeassistant.components.upnp.config_flow.get_local_ip', - return_value='192.168.1.10'): - assert await upnp.async_setup_entry(hass, entry) is True + async_discover.return_value = mock_coro(return_value=discovery_infos) - # ensure device is stored/used - assert hass.data[upnp.DOMAIN]['devices'][udn] == mock_device + assert await upnp.async_setup_entry(hass, entry) is True - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() + # ensure device is stored/used + 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 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): """Test async_setup_entry.""" + # pylint: disable=invalid-name udn = 'uuid:device_1' - entry = MockConfigEntry(domain=upnp.DOMAIN, data={ - 'ssdp_description': 'http://192.168.1.1/desc.xml', - 'udn': udn, - 'sensors': False, - 'port_mapping': True, - }) + entry = MockConfigEntry(domain=upnp.DOMAIN) config = { 'http': {}, @@ -194,15 +104,22 @@ async def test_async_setup_entry_port_mapping(hass): }, } 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'): await async_setup_component(hass, 'http', config) await async_setup_component(hass, 'upnp', config) await hass.async_block_till_done() 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) + async_discover.return_value = mock_coro(return_value=discovery_infos) assert await upnp.async_setup_entry(hass, entry) is True