IGD review fixes (#17400)

* Fix discovery-dependency for upnp

* Only delete port mappings on EVENT_HOMEASSISTANT_STOP + refactoring MockDevice

* Call and store local_ip from async_setup

* Don't depend on http-component

* Remove discovery dependency
This commit is contained in:
Steven Looman 2018-10-23 21:52:01 +02:00 committed by Paulus Schoutsen
parent 0f69be117f
commit fc8af22191
3 changed files with 118 additions and 67 deletions

View file

@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv
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.components.discovery import DOMAIN as DISCOVERY_DOMAIN 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,
@ -31,7 +31,6 @@ from .device import Device
REQUIREMENTS = ['async-upnp-client==0.12.7'] REQUIREMENTS = ['async-upnp-client==0.12.7']
DEPENDENCIES = ['http']
NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_ID = 'upnp_notification'
NOTIFICATION_TITLE = 'UPnP/IGD Setup' NOTIFICATION_TITLE = 'UPnP/IGD Setup'
@ -50,17 +49,36 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
def _substitute_hass_ports(ports, hass_port): def _substitute_hass_ports(ports, hass_port=None):
"""Substitute 'hass' for the hass_port.""" """
Substitute 'hass' for the hass_port.
This triggers a warning when hass_port is None.
"""
ports = ports.copy() ports = ports.copy()
# substitute 'hass' for hass_port, both keys and values # substitute 'hass' for hass_port, both keys and values
if CONF_HASS in ports: if CONF_HASS in ports:
if hass_port is None:
_LOGGER.warning(
'Could not determine Home Assistant http port, '
'not setting up port mapping from %s to %s. '
'Enable the http-component.',
CONF_HASS, ports[CONF_HASS])
else:
ports[hass_port] = ports[CONF_HASS] ports[hass_port] = ports[CONF_HASS]
del ports[CONF_HASS] del ports[CONF_HASS]
for port in ports: for port in ports:
if ports[port] == CONF_HASS: if ports[port] == CONF_HASS:
if hass_port is None:
_LOGGER.warning(
'Could not determine Home Assistant http port, '
'not setting up port mapping from %s to %s. '
'Enable the http-component.',
port, ports[port])
del ports[port]
else:
ports[port] = hass_port ports[port] = hass_port
return ports return ports
@ -74,15 +92,14 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
# ensure sane config # ensure sane config
if DOMAIN not in config: if DOMAIN not in config:
return True return True
upnp_config = config[DOMAIN]
if DISCOVERY_DOMAIN not in config:
_LOGGER.warning('UPNP needs discovery, please enable it')
return False
# overridden local ip # overridden local ip
upnp_config = config[DOMAIN]
if CONF_LOCAL_IP in upnp_config: if CONF_LOCAL_IP in upnp_config:
hass.data[DOMAIN]['local_ip'] = upnp_config[CONF_LOCAL_IP] hass.data[DOMAIN]['local_ip'] = upnp_config[CONF_LOCAL_IP]
else:
hass.data[DOMAIN]['local_ip'] = \
await hass.async_add_executor_job(get_local_ip)
# determine ports # determine ports
ports = {CONF_HASS: CONF_HASS} # default, port_mapping disabled by default ports = {CONF_HASS: CONF_HASS} # default, port_mapping disabled by default
@ -119,13 +136,15 @@ async def async_setup_entry(hass: HomeAssistantType,
# port mapping # port mapping
if data.get(CONF_ENABLE_PORT_MAPPING): if data.get(CONF_ENABLE_PORT_MAPPING):
local_ip = hass.data[DOMAIN].get('local_ip') local_ip = hass.data[DOMAIN]['local_ip']
ports = hass.data[DOMAIN]['auto_config']['ports'] ports = hass.data[DOMAIN]['auto_config']['ports']
_LOGGER.debug('Enabling port mappings: %s', ports) _LOGGER.debug('Enabling port mappings: %s', ports)
hass_port = None
if hasattr(hass, 'http'):
hass_port = hass.http.server_port hass_port = hass.http.server_port
ports = _substitute_hass_ports(ports, hass_port) ports = _substitute_hass_ports(ports, hass_port=hass_port)
await device.async_add_port_mappings(ports, local_ip=local_ip) await device.async_add_port_mappings(ports, local_ip)
# sensors # sensors
if data.get(CONF_ENABLE_SENSORS): if data.get(CONF_ENABLE_SENSORS):
@ -135,10 +154,12 @@ async def async_setup_entry(hass: HomeAssistantType,
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'))
async def unload_entry(event): async def delete_port_mapping(event):
"""Unload entry on quit.""" """Delete port mapping on quit."""
await async_unload_entry(hass, config_entry) if data.get(CONF_ENABLE_PORT_MAPPING):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unload_entry) _LOGGER.debug('Deleting port mappings')
await device.async_delete_port_mappings()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, delete_port_mapping)
return True return True

View file

@ -6,7 +6,6 @@ import aiohttp
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import get_local_ip
from .const import LOGGER as _LOGGER from .const import LOGGER as _LOGGER
@ -51,15 +50,13 @@ class Device:
"""Get the name.""" """Get the name."""
return self._igd_device.name return self._igd_device.name
async def async_add_port_mappings(self, ports, local_ip=None): async def async_add_port_mappings(self, ports, local_ip):
"""Add port mappings.""" """Add port mappings."""
# determine local ip, ensure sane IP
if local_ip is None:
local_ip = get_local_ip()
if local_ip == '127.0.0.1': if local_ip == '127.0.0.1':
_LOGGER.error( _LOGGER.error(
'Could not create port mapping, our IP is 127.0.0.1') 'Could not create port mapping, our IP is 127.0.0.1')
# determine local ip, ensure sane IP
local_ip = IPv4Address(local_ip) local_ip = IPv4Address(local_ip)
# create port mappings # create port mappings

View file

@ -9,6 +9,7 @@ from homeassistant.components.upnp.device import Device
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.common import MockDependency
from tests.common import mock_coro from tests.common import mock_coro
@ -17,7 +18,7 @@ class MockDevice(Device):
def __init__(self, udn): def __init__(self, udn):
"""Initializer.""" """Initializer."""
super().__init__(None) super().__init__(MagicMock())
self._udn = udn self._udn = udn
self.added_port_mappings = [] self.added_port_mappings = []
self.removed_port_mappings = [] self.removed_port_mappings = []
@ -49,7 +50,15 @@ class MockDevice(Device):
async def test_async_setup_no_auto_config(hass): async def test_async_setup_no_auto_config(hass):
"""Test async_setup.""" """Test async_setup."""
# setup component, enable auto_config # setup component, enable auto_config
await async_setup_component(hass, 'upnp') config = {
'discovery': {},
# no upnp
}
with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.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'] == { assert hass.data[upnp.DOMAIN]['auto_config'] == {
'active': False, 'active': False,
@ -62,7 +71,15 @@ async def test_async_setup_no_auto_config(hass):
async def test_async_setup_auto_config(hass): async def test_async_setup_auto_config(hass):
"""Test async_setup.""" """Test async_setup."""
# setup component, enable auto_config # setup component, enable auto_config
await async_setup_component(hass, 'upnp', {'upnp': {}, 'discovery': {}}) config = {
'discovery': {},
'upnp': {},
}
with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.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'] == { assert hass.data[upnp.DOMAIN]['auto_config'] == {
'active': True, 'active': True,
@ -75,12 +92,18 @@ async def test_async_setup_auto_config(hass):
async def test_async_setup_auto_config_port_mapping(hass): async def test_async_setup_auto_config_port_mapping(hass):
"""Test async_setup.""" """Test async_setup."""
# setup component, enable auto_config # setup component, enable auto_config
await async_setup_component(hass, 'upnp', { config = {
'discovery': {},
'upnp': { 'upnp': {
'port_mapping': True, 'port_mapping': True,
'ports': {'hass': 'hass'}, 'ports': {'hass': 'hass'},
}, },
'discovery': {}}) }
with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.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'] == { assert hass.data[upnp.DOMAIN]['auto_config'] == {
'active': True, 'active': True,
@ -93,9 +116,15 @@ async def test_async_setup_auto_config_port_mapping(hass):
async def test_async_setup_auto_config_no_sensors(hass): async def test_async_setup_auto_config_no_sensors(hass):
"""Test async_setup.""" """Test async_setup."""
# setup component, enable auto_config # setup component, enable auto_config
await async_setup_component(hass, 'upnp', { config = {
'discovery': {},
'upnp': {'sensors': False}, 'upnp': {'sensors': False},
'discovery': {}}) }
with MockDependency('netdisco.discovery'), \
patch('homeassistant.components.upnp.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'] == { assert hass.data[upnp.DOMAIN]['auto_config'] == {
'active': True, 'active': True,
@ -115,18 +144,23 @@ async def test_async_setup_entry_default(hass):
'port_mapping': False, 'port_mapping': False,
}) })
# ensure hass.http is available config = {
await async_setup_component(hass, 'upnp') 'http': {},
'discovery': {},
# no upnp
}
with MockDependency('netdisco.discovery'), \
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 homeassistant.components.upnp.device.Device # mock homeassistant.components.upnp.device.Device
mock_device = MagicMock() mock_device = MockDevice(udn)
mock_device.udn = udn with patch.object(Device, 'async_create_device') as create_device:
mock_device.async_add_port_mappings.return_value = mock_coro() create_device.return_value = mock_coro(return_value=mock_device)
mock_device.async_delete_port_mappings.return_value = mock_coro() with patch('homeassistant.components.upnp.get_local_ip',
with patch.object(Device, 'async_create_device') as mock_create_device:
mock_create_device.return_value = mock_coro(
return_value=mock_device)
with patch('homeassistant.components.upnp.device.get_local_ip',
return_value='192.168.1.10'): return_value='192.168.1.10'):
assert await upnp.async_setup_entry(hass, entry) is True assert await upnp.async_setup_entry(hass, entry) is True
@ -136,12 +170,9 @@ async def test_async_setup_entry_default(hass):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done() await hass.async_block_till_done()
# ensure cleaned up # ensure no port-mappings created or removed
assert udn not in hass.data[upnp.DOMAIN]['devices'] assert not mock_device.added_port_mappings
assert not mock_device.removed_port_mappings
# ensure no port-mapping-methods called
assert len(mock_device.async_add_port_mappings.mock_calls) == 0
assert len(mock_device.async_delete_port_mappings.mock_calls) == 0
async def test_async_setup_entry_port_mapping(hass): async def test_async_setup_entry_port_mapping(hass):
@ -154,20 +185,25 @@ async def test_async_setup_entry_port_mapping(hass):
'port_mapping': True, 'port_mapping': True,
}) })
# ensure hass.http is available config = {
await async_setup_component(hass, 'upnp', { 'http': {},
'discovery': {},
'upnp': { 'upnp': {
'port_mapping': True, 'port_mapping': True,
'ports': {'hass': 'hass'}, 'ports': {'hass': 'hass'},
}, },
'discovery': {}, }
}) with MockDependency('netdisco.discovery'), \
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) mock_device = MockDevice(udn)
with patch.object(Device, 'async_create_device') as mock_create_device: with patch.object(Device, 'async_create_device') as create_device:
mock_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.device.get_local_ip',
return_value='192.168.1.10'):
assert await upnp.async_setup_entry(hass, entry) is True assert await upnp.async_setup_entry(hass, entry) is True
# ensure device is stored/used # ensure device is stored/used
@ -181,8 +217,5 @@ async def test_async_setup_entry_port_mapping(hass):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done() await hass.async_block_till_done()
# ensure cleaned up
assert udn not in hass.data[upnp.DOMAIN]['devices']
# ensure delete-port-mapping-methods called # ensure delete-port-mapping-methods called
assert mock_device.removed_port_mappings == [8123] assert mock_device.removed_port_mappings == [8123]