""" 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.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv from homeassistant.helpers import dispatcher from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import HomeAssistantType 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.0'] NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_TITLE = 'UPnP/IGD Setup' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ 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.port): vol.Any(CONF_HASS, cv.port) }) }), }, extra=vol.ALLOW_EXTRA) def _substitute_hass_ports(ports, hass_port=None): """ Substitute 'hass' for the hass_port. This triggers a warning when hass_port is None. """ ports = ports.copy() # substitute 'hass' for hass_port, both keys and values 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] del ports[CONF_HASS] for port in ports: 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 return ports # config 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, } 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 # 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') return False hass.data[DOMAIN]['devices'][device.udn] = device # 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) 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): _LOGGER.debug('Enabling sensors') # register sensor setup handlers hass.async_create_task(hass.config_entries.async_forward_entry_setup( config_entry, 'sensor')) 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() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, delete_port_mapping) return True 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 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() # 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] return True