Working on IGD

pull/16300/head
Steven Looman 2018-08-30 16:38:43 +02:00
parent 3db766e2ec
commit 20879726b0
9 changed files with 284 additions and 142 deletions

View File

@ -4,52 +4,34 @@ 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/
"""
# XXX TODO:
# + flow:
# + discovery
# + adding device
# + removing device
# - configured:
# - adding
# - sensors:
# + adding
# + handle overflow
# - removing
# - port forward:
# - adding
# - removing
# - shutdown
import asyncio
from ipaddress import IPv4Address
from ipaddress import ip_address
import aiohttp
import asyncio
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.const import CONF_URL
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util import get_local_ip
from homeassistant.components.discovery import DOMAIN as DISCOVERY_DOMAIN
from .config_flow import configured_udns
from .const import CONF_PORT_FORWARD, CONF_SENSORS
from .const import CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS
from .const import DOMAIN
from .const import LOGGER as _LOGGER
REQUIREMENTS = ['async-upnp-client==0.12.4']
DEPENDENCIES = ['http']
DEPENDENCIES = ['http'] # ,'discovery']
CONF_LOCAL_IP = 'local_ip'
CONF_ENABLE_PORT_MAPPING = 'port_mapping'
CONF_PORTS = 'ports'
CONF_UNITS = 'unit'
CONF_HASS = 'hass'
@ -66,17 +48,16 @@ UNITS = {
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_URL): cv.url,
vol.Optional(CONF_ENABLE_PORT_MAPPING, default=True): cv.boolean,
vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS),
vol.Optional(CONF_ENABLE_PORT_MAPPING, default=False): cv.boolean,
vol.Optional(CONF_ENABLE_SENSORS, default=True): cv.boolean,
vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string),
vol.Optional(CONF_PORTS):
vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int})
vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS),
}),
}, extra=vol.ALLOW_EXTRA)
async def _async_create_igd_device(hass: HomeAssistantType, ssdp_description: str):
async def _async_create_igd_device(hass: HomeAssistantType,
ssdp_description: str):
"""."""
# build requester
from async_upnp_client.aiohttp import AiohttpSessionRequester
@ -111,19 +92,22 @@ def _get_device(hass: HomeAssistantType, udn):
return hass.data[DOMAIN]['devices'][udn]
async def _async_create_port_forward(hass: HomeAssistantType, igd_device):
"""Create a port forward."""
_LOGGER.debug('Creating port forward: %s', igd_device)
async def _async_add_port_mapping(hass: HomeAssistantType,
igd_device,
local_ip=None):
"""Create a port mapping."""
# determine local ip, ensure sane IP
local_ip = get_local_ip()
if local_ip is None:
local_ip = get_local_ip()
if local_ip == '127.0.0.1':
_LOGGER.warning('Could not create port forward, our IP is 127.0.0.1')
_LOGGER.warning('Could not create port mapping, our IP is 127.0.0.1')
return False
local_ip = IPv4Address(local_ip)
# create port mapping
port = hass.http.server_port
_LOGGER.debug('Creating port mapping %s:%s:%s (TCP)', port, local_ip, port)
await igd_device.async_add_port_mapping(remote_host=None,
external_port=port,
protocol='TCP',
@ -136,48 +120,52 @@ async def _async_create_port_forward(hass: HomeAssistantType, igd_device):
return True
async def _async_remove_port_forward(hass: HomeAssistantType, igd_device):
"""Remove a port forward."""
_LOGGER.debug('Removing port forward: %s', igd_device)
# remove port mapping
async def _async_delete_port_mapping(hass: HomeAssistantType, igd_device):
"""Remove a port mapping."""
port = hass.http.server_port
await igd_device.async_remove_port_mapping(remote_host=None,
await igd_device.async_delete_port_mapping(remote_host=None,
external_port=port,
protocol='TCP')
# config
async def async_setup(hass: HomeAssistantType, config):
async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Register a port mapping for Home Assistant via UPnP."""
_LOGGER.debug('async_setup: config: %s', config)
conf = config.get(DOMAIN, {})
# defaults
hass.data[DOMAIN] = {
'auto_config': {
'active': False,
'port_forward': False,
'sensors': False,
}
}
hass.data[DOMAIN] = hass.data.get(DOMAIN, {})
configured = configured_udns(hass)
_LOGGER.debug('configured: %s', configured)
# ensure sane config
if DOMAIN not in config:
return False
# if no ssdp given: take any discovered - by flow - IGD entry
# if none discovered, raise PlatformNotReady
# if ssdp given: use the SSDP
if DISCOVERY_DOMAIN not in config:
_LOGGER.warning('IGD needs discovery, please enable it')
return False
igds = [] # XXX TODO
for igd_conf in igds:
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
data={
'ssdp_description': igd_conf['ssdp_description'],
}
))
igd_config = config[DOMAIN]
if CONF_LOCAL_IP in igd_config:
hass.data[DOMAIN]['local_ip'] = igd_config[CONF_LOCAL_IP]
hass.data[DOMAIN]['auto_config'] = {
'active': True,
'port_forward': igd_config[CONF_ENABLE_PORT_MAPPING],
'sensors': igd_config[CONF_ENABLE_SENSORS],
}
_LOGGER.debug('Enabled auto_config: %s', hass.data[DOMAIN]['auto_config'])
return True
# config flow
async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry):
async def async_setup_entry(hass: HomeAssistantType,
config_entry: ConfigEntry):
"""Set up a bridge from a config entry."""
_LOGGER.debug('async_setup_entry: title: %s, data: %s', config_entry.title, config_entry.data)
data = config_entry.data
ssdp_description = data['ssdp_description']
@ -189,44 +177,49 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry):
_store_device(hass, igd_device.udn, igd_device)
# port forward
if data.get(CONF_PORT_FORWARD):
await _async_create_port_forward(hass, igd_device)
# port mapping
if data.get(CONF_ENABLE_PORT_MAPPING):
local_ip = hass.data[DOMAIN].get('local_ip')
await _async_add_port_mapping(hass, igd_device, local_ip=local_ip)
# sensors
if data.get(CONF_SENSORS):
if data.get(CONF_ENABLE_SENSORS):
discovery_info = {
'unit': 'MBytes',
'udn': data['udn'],
}
hass_config = config_entry.data
hass.async_create_task(discovery.async_load_platform(
hass, 'sensor', DOMAIN, discovery_info, hass_config))
hass.async_create_task(
discovery.async_load_platform(
hass, 'sensor', DOMAIN, discovery_info, hass_config))
async def unload_entry(event):
"""Unload entry on quit."""
await async_unload_entry(hass, config_entry)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unload_entry)
return True
async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistantType,
config_entry: ConfigEntry):
"""Unload a config entry."""
_LOGGER.debug('async_unload_entry: title: %s, data: %s', config_entry.title, config_entry.data)
data = config_entry.data
udn = data['udn']
igd_device = _get_device(hass, udn)
# port forward
if data.get(CONF_PORT_FORWARD):
_LOGGER.debug('Removing port forward for: %s', igd_device)
_async_remove_port_forward(hass, igd_device)
igd_device = _get_device(hass, udn)
if igd_device is None:
return True
# port mapping
if data.get(CONF_ENABLE_PORT_MAPPING):
await _async_delete_port_mapping(hass, igd_device)
# sensors
if data.get(CONF_SENSORS):
if data.get(CONF_ENABLE_SENSORS):
# XXX TODO: remove sensors
pass
_store_device(hass, udn, None)
return True

View File

@ -1,11 +1,12 @@
"""Config flow for IGD."""
import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
from homeassistant.core import callback
import voluptuous as vol
from .const import CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS
from .const import DOMAIN
from .const import LOGGER as _LOGGER
@callback
def configured_udns(hass):
@ -29,24 +30,23 @@ class IgdFlowHandler(data_entry_flow.FlowHandler):
@property
def _discovereds(self):
"""Get all discovered entries."""
if DOMAIN not in self.hass.data:
_LOGGER.debug('DOMAIN not in hass.data')
if 'discovered' not in self.hass.data.get(DOMAIN, {}):
_LOGGER.debug('discovered not in hass.data[DOMAIN]')
return self.hass.data.get(DOMAIN, {}).get('discovered', {})
def _store_discovery_info(self, discovery_info):
"""Add discovery info."""
udn = discovery_info['udn']
if DOMAIN not in self.hass.data:
_LOGGER.debug('DOMAIN not in hass.data')
self.hass.data[DOMAIN] = self.hass.data.get(DOMAIN, {})
if 'discovered' not in self.hass.data[DOMAIN]:
_LOGGER.debug('Creating new discovered: %s', self.hass.data[DOMAIN])
self.hass.data[DOMAIN]['discovered'] = self.hass.data[DOMAIN].get('discovered', {})
self.hass.data[DOMAIN]['discovered'] = \
self.hass.data[DOMAIN].get('discovered', {})
self.hass.data[DOMAIN]['discovered'][udn] = discovery_info
def _auto_config_settings(self):
"""Check if auto_config has been enabled."""
self.hass.data[DOMAIN] = self.hass.data.get(DOMAIN, {})
return self.hass.data[DOMAIN].get('auto_config', {
'active': False,
})
async def async_step_discovery(self, discovery_info):
"""
Handle a discovered IGD.
@ -54,32 +54,33 @@ class IgdFlowHandler(data_entry_flow.FlowHandler):
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.
"""
_LOGGER.debug('async_step_discovery %s: %s', id(self), discovery_info)
# ensure not already discovered/configured
udn = discovery_info['udn']
if udn in configured_udns(self.hass):
_LOGGER.debug('Already configured: %s', discovery_info)
return self.async_abort(reason='already_configured')
# store discovered device
self._store_discovery_info(discovery_info)
# abort --> not showing up in discovered things
# return self.async_abort(reason='user_input_required')
# auto config?
auto_config = self._auto_config_settings()
if auto_config['active']:
import_info = {
'igd_host': discovery_info['host'],
'sensors': auto_config['sensors'],
'port_forward': auto_config['port_forward'],
}
return await self._async_save_entry(import_info)
# user -> showing up in discovered things
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Manual set up."""
_LOGGER.debug('async_step_user %s: %s', id(self), user_input)
# if user input given, handle it
user_input = user_input or {}
if 'igd_host' in user_input:
if not user_input['sensors'] and not user_input['port_forward']:
_LOGGER.debug('Aborting, no sensors and no portforward')
return self.async_abort(reason='no_sensors_or_port_forward')
configured_hosts = [
@ -90,10 +91,9 @@ class IgdFlowHandler(data_entry_flow.FlowHandler):
if user_input['igd_host'] in configured_hosts:
return self.async_abort(reason='already_configured')
return await self._async_save(user_input)
return await self._async_save_entry(user_input)
# let user choose from all discovered IGDs
_LOGGER.debug('Discovered devices: %s', self._discovereds)
igd_hosts = [
entry['host']
for entry in self._discovereds.values()
@ -111,10 +111,12 @@ class IgdFlowHandler(data_entry_flow.FlowHandler):
})
)
async def _async_save(self, import_info):
"""Store IGD as new entry."""
_LOGGER.debug('async_step_import %s: %s', id(self), import_info)
async def async_step_import(self, import_info):
"""Import a new IGD as a config entry."""
return await self._async_save_entry(import_info)
async def _async_save_entry(self, import_info):
"""Store IGD as new entry."""
# ensure we know the host
igd_host = import_info['igd_host']
discovery_infos = [info
@ -129,7 +131,7 @@ class IgdFlowHandler(data_entry_flow.FlowHandler):
data={
'ssdp_description': discovery_info['ssdp_description'],
'udn': discovery_info['udn'],
'sensors': import_info['sensors'],
'port_forward': import_info['port_forward'],
CONF_ENABLE_SENSORS: import_info['sensors'],
CONF_ENABLE_PORT_MAPPING: import_info['port_forward'],
},
)

View File

@ -3,5 +3,5 @@ import logging
DOMAIN = 'igd'
LOGGER = logging.getLogger('homeassistant.components.igd')
CONF_PORT_FORWARD = 'port_forward'
CONF_SENSORS = 'sensors'
CONF_ENABLE_PORT_MAPPING = 'port_forward'
CONF_ENABLE_SENSORS = 'sensors'

View File

@ -1,15 +1,17 @@
"""
Support for UPnP Sensors (IGD).
Support for IGD Sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.upnp/
https://home-assistant.io/components/sensor.igd/
"""
# pylint: disable=invalid-name
import logging
from homeassistant.components import history
from homeassistant.components.igd import DOMAIN, UNITS
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['igd', 'history']
@ -98,9 +100,6 @@ class IGDSensor(Entity):
self._handle_new_value(new_value)
# _LOGGER.debug('Removing self: %s', self)
# await self.async_remove() # XXX TODO: does not remove from the UI
@property
def _last_state(self):
"""Get the last state reported to hass."""
@ -126,17 +125,11 @@ class IGDSensor(Entity):
try:
state = coercer(float(last_state.state)) * self.unit_factor
except ValueError:
_LOGGER.debug('%s: value error, coercer: %s, state: %s', self.entity_id, coercer, last_state.state)
raise
state = coercer(0.0)
return state
def _handle_new_value(self, new_value):
_LOGGER.debug('%s: handle_new_value: state: %s, new_value: %s, last_value: %s',
self.entity_id, self._state, new_value, self._last_value)
# upnp-client --debug --pprint --device http://192.168.178.1/RootDevice.xml call-action WANCIFC/GetTotalBytesReceived
if self.entity_id is None:
# don't know our entity ID yet, do nothing but store value
self._last_value = new_value
@ -161,7 +154,8 @@ class IGDSensor(Entity):
if new_value >= 0:
diff += new_value
else:
# some devices don't overflow and start at 0, but somewhere to -2**32
# some devices don't overflow and start at 0,
# but somewhere to -2**32
diff += new_value - -OVERFLOW_AT
self._state += diff

View File

@ -141,6 +141,7 @@ apns2==0.3.0
# homeassistant.components.asterisk_mbox
asterisk_mbox==0.4.0
# homeassistant.components.igd
# homeassistant.components.media_player.dlna_dmr
async-upnp-client==0.12.4
@ -1183,9 +1184,6 @@ pytrafikverket==0.1.5.8
# homeassistant.components.device_tracker.unifi
pyunifi==2.13
# homeassistant.components.upnp
pyupnp-async==0.1.1.1
# homeassistant.components.binary_sensor.uptimerobot
pyuptimerobot==0.0.5

View File

@ -177,9 +177,6 @@ pytradfri[async]==5.5.1
# homeassistant.components.device_tracker.unifi
pyunifi==2.13
# homeassistant.components.upnp
pyupnp-async==0.1.1.1
# homeassistant.components.notify.html5
pywebpush==1.6.0

View File

@ -1 +1 @@
"""Tests for the IGD component."""
"""Tests for the IGD component."""

View File

@ -1,13 +1,14 @@
"""Tests for IGD config flow."""
from homeassistant.components import igd
from homeassistant.components.igd import config_flow as igd_config_flow
from tests.common import MockConfigEntry
async def test_flow_none_discovered(hass):
"""Test no device discovered flow."""
flow = igd.config_flow.IgdFlowHandler()
flow = igd_config_flow.IgdFlowHandler()
flow.hass = hass
result = await flow.async_step_user()
@ -17,7 +18,7 @@ async def test_flow_none_discovered(hass):
async def test_flow_already_configured(hass):
"""Test device already configured flow."""
flow = igd.config_flow.IgdFlowHandler()
flow = igd_config_flow.IgdFlowHandler()
flow.hass = hass
# discovered device
@ -48,7 +49,7 @@ async def test_flow_already_configured(hass):
async def test_flow_no_sensors_no_port_forward(hass):
"""Test single device, no sensors, no port_forward."""
flow = igd.config_flow.IgdFlowHandler()
flow = igd_config_flow.IgdFlowHandler()
flow.hass = hass
# discovered device
@ -79,7 +80,7 @@ async def test_flow_no_sensors_no_port_forward(hass):
async def test_flow_discovered_form(hass):
"""Test single device discovered, show form flow."""
flow = igd.config_flow.IgdFlowHandler()
flow = igd_config_flow.IgdFlowHandler()
flow.hass = hass
# discovered device
@ -100,7 +101,7 @@ async def test_flow_discovered_form(hass):
async def test_flow_two_discovered_form(hass):
"""Test single device discovered, show form flow."""
flow = igd.config_flow.IgdFlowHandler()
flow = igd_config_flow.IgdFlowHandler()
flow.hass = hass
# discovered device
@ -135,18 +136,18 @@ async def test_flow_two_discovered_form(hass):
async def test_config_entry_created(hass):
flow = igd.config_flow.IgdFlowHandler()
"""Test config entry is created."""
flow = igd_config_flow.IgdFlowHandler()
flow.hass = hass
# discovered device
udn = 'uuid:device_1'
hass.data[igd.DOMAIN] = {
'discovered': {
udn: {
'uuid:device_1': {
'name': 'Test device 1',
'host': '192.168.1.1',
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': udn,
'udn': 'uuid:device_1',
},
},
}
@ -156,6 +157,7 @@ async def test_config_entry_created(hass):
'sensors': True,
'port_forward': False,
})
assert result['type'] == 'create_entry'
assert result['data'] == {
'port_forward': False,
'sensors': True,
@ -163,3 +165,67 @@ async def test_config_entry_created(hass):
'udn': 'uuid:device_1',
}
assert result['title'] == 'Test device 1'
async def test_flow_discovery_auto_config_sensors(hass):
"""Test creation of device with auto_config."""
flow = igd_config_flow.IgdFlowHandler()
flow.hass = hass
# auto_config active
hass.data[igd.DOMAIN] = {
'auto_config': {
'active': True,
'port_forward': False,
'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'] == {
'port_forward': False,
'sensors': True,
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': 'uuid:device_1',
}
assert result['title'] == 'Test device 1'
async def test_flow_discovery_auto_config_sensors_port_forward(hass):
"""Test creation of device with auto_config, with port forward."""
flow = igd_config_flow.IgdFlowHandler()
flow.hass = hass
# auto_config active, with port_forward
hass.data[igd.DOMAIN] = {
'auto_config': {
'active': True,
'port_forward': True,
'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'] == {
'port_forward': True,
'sensors': True,
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': 'uuid:device_1',
}
assert result['title'] == 'Test device 1'

View File

@ -10,9 +10,96 @@ from tests.common import MockConfigEntry
from tests.common import mock_coro
async def test_async_setup_entry_port_forward_created(hass):
"""Test async_setup_entry."""
async def test_async_setup_no_auto_config(hass):
"""Test async_setup."""
# setup component, enable auto_config
await async_setup_component(hass, 'igd')
assert hass.data[igd.DOMAIN]['auto_config'] == {
'active': False,
'port_forward': False,
'sensors': False,
}
async def test_async_setup_auto_config(hass):
"""Test async_setup."""
# setup component, enable auto_config
await async_setup_component(hass, 'igd', {'igd': {}, 'discovery': {}})
assert hass.data[igd.DOMAIN]['auto_config'] == {
'active': True,
'port_forward': False,
'sensors': True,
}
async def test_async_setup_auto_config_port_forward(hass):
"""Test async_setup."""
# setup component, enable auto_config
await async_setup_component(hass, 'igd', {
'igd': {'port_forward': True},
'discovery': {}})
assert hass.data[igd.DOMAIN]['auto_config'] == {
'active': True,
'port_forward': True,
'sensors': True,
}
async def test_async_setup_auto_config_no_sensors(hass):
"""Test async_setup."""
# setup component, enable auto_config
await async_setup_component(hass, 'igd', {
'igd': {'sensors': False},
'discovery': {}})
assert hass.data[igd.DOMAIN]['auto_config'] == {
'active': True,
'port_forward': False,
'sensors': False,
}
async def test_async_setup_entry_default(hass):
"""Test async_setup_entry."""
udn = 'uuid:device_1'
entry = MockConfigEntry(domain=igd.DOMAIN, data={
'ssdp_description': 'http://192.168.1.1/desc.xml',
'udn': udn,
'sensors': True,
'port_forward': False,
})
# ensure hass.http is available
await async_setup_component(hass, 'igd')
# mock async_upnp_client.igd.IgdDevice
mock_igd_device = MagicMock()
mock_igd_device.udn = udn
mock_igd_device.async_add_port_mapping.return_value = mock_coro()
mock_igd_device.async_delete_port_mapping.return_value = mock_coro()
with patch.object(igd, '_async_create_igd_device') as mock_create_device:
mock_create_device.return_value = mock_coro(
return_value=mock_igd_device)
with patch('homeassistant.components.igd.get_local_ip',
return_value='192.168.1.10'):
assert await igd.async_setup_entry(hass, entry) is True
# ensure device is stored/used
assert hass.data[igd.DOMAIN]['devices'][udn] == mock_igd_device
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert hass.data[igd.DOMAIN]['devices'][udn] is None
assert len(mock_igd_device.async_add_port_mapping.mock_calls) == 0
assert len(mock_igd_device.async_delete_port_mapping.mock_calls) == 0
async def test_async_setup_entry_port_forward(hass):
"""Test async_setup_entry."""
udn = 'uuid:device_1'
entry = MockConfigEntry(domain=igd.DOMAIN, data={
'ssdp_description': 'http://192.168.1.1/desc.xml',
@ -27,15 +114,20 @@ async def test_async_setup_entry_port_forward_created(hass):
mock_igd_device = MagicMock()
mock_igd_device.udn = udn
mock_igd_device.async_add_port_mapping.return_value = mock_coro()
mock_igd_device.async_remove_port_mapping.return_value = mock_coro()
mock_igd_device.async_delete_port_mapping.return_value = mock_coro()
with patch.object(igd, '_async_create_igd_device') as mock_create_device:
mock_create_device.return_value = mock_coro(return_value=mock_igd_device)
with patch('homeassistant.components.igd.get_local_ip', return_value='192.168.1.10'):
mock_create_device.return_value = mock_coro(
return_value=mock_igd_device)
with patch('homeassistant.components.igd.get_local_ip',
return_value='192.168.1.10'):
assert await igd.async_setup_entry(hass, entry) is True
# ensure device is stored/used
assert hass.data[igd.DOMAIN]['devices'][udn] == mock_igd_device
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert hass.data[igd.DOMAIN]['devices'][udn] == mock_igd_device
assert hass.data[igd.DOMAIN]['devices'][udn] is None
assert len(mock_igd_device.async_add_port_mapping.mock_calls) > 0
assert len(mock_igd_device.async_delete_port_mapping.mock_calls) > 0