2018-08-17 19:28:29 +00:00
|
|
|
"""
|
|
|
|
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
|
2018-09-17 20:08:09 +00:00
|
|
|
https://home-assistant.io/components/upnp/
|
2018-08-17 19:28:29 +00:00
|
|
|
"""
|
2018-08-30 14:38:43 +00:00
|
|
|
import asyncio
|
2018-08-17 19:28:29 +00:00
|
|
|
from ipaddress import ip_address
|
|
|
|
|
2018-08-30 14:38:43 +00:00
|
|
|
import aiohttp
|
2018-08-17 19:28:29 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2018-08-29 19:19:04 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
|
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
2018-08-17 19:28:29 +00:00
|
|
|
from homeassistant.helpers import config_validation as cv
|
2018-10-01 16:25:54 +00:00
|
|
|
from homeassistant.helpers import dispatcher
|
2018-09-07 22:11:23 +00:00
|
|
|
from homeassistant.helpers.typing import ConfigType
|
|
|
|
from homeassistant.helpers.typing import HomeAssistantType
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2018-09-01 19:20:15 +00:00
|
|
|
from .const import (
|
|
|
|
CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS,
|
2018-09-07 22:11:23 +00:00
|
|
|
CONF_HASS, CONF_LOCAL_IP, CONF_PORTS,
|
|
|
|
CONF_UDN, CONF_SSDP_DESCRIPTION,
|
2018-10-03 09:07:25 +00:00
|
|
|
SIGNAL_REMOVE_SENSOR,
|
2018-09-01 19:20:15 +00:00
|
|
|
)
|
2018-08-17 19:28:29 +00:00
|
|
|
from .const import DOMAIN
|
|
|
|
from .const import LOGGER as _LOGGER
|
2018-10-29 07:10:01 +00:00
|
|
|
from .config_flow import async_ensure_domain_data
|
2018-09-07 22:11:23 +00:00
|
|
|
from .device import Device
|
2018-08-17 19:28:29 +00:00
|
|
|
|
|
|
|
|
2018-11-11 15:10:03 +00:00
|
|
|
REQUIREMENTS = ['async-upnp-client==0.13.2']
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2018-09-17 20:08:09 +00:00
|
|
|
NOTIFICATION_ID = 'upnp_notification'
|
2018-08-17 19:28:29 +00:00
|
|
|
NOTIFICATION_TITLE = 'UPnP/IGD Setup'
|
|
|
|
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
|
|
DOMAIN: vol.Schema({
|
2018-08-30 14:38:43 +00:00
|
|
|
vol.Optional(CONF_ENABLE_PORT_MAPPING, default=False): cv.boolean,
|
|
|
|
vol.Optional(CONF_ENABLE_SENSORS, default=True): cv.boolean,
|
2018-08-17 19:28:29 +00:00
|
|
|
vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string),
|
2018-09-07 22:11:23 +00:00
|
|
|
vol.Optional(CONF_PORTS):
|
2018-09-14 19:30:08 +00:00
|
|
|
vol.Schema({
|
2018-10-17 21:09:05 +00:00
|
|
|
vol.Any(CONF_HASS, cv.port):
|
|
|
|
vol.Any(CONF_HASS, cv.port)
|
2018-09-14 19:30:08 +00:00
|
|
|
})
|
2018-08-17 19:28:29 +00:00
|
|
|
}),
|
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
|
2018-10-23 19:52:01 +00:00
|
|
|
def _substitute_hass_ports(ports, hass_port=None):
|
|
|
|
"""
|
|
|
|
Substitute 'hass' for the hass_port.
|
|
|
|
|
|
|
|
This triggers a warning when hass_port is None.
|
|
|
|
"""
|
2018-10-03 09:07:25 +00:00
|
|
|
ports = ports.copy()
|
|
|
|
|
|
|
|
# substitute 'hass' for hass_port, both keys and values
|
2018-09-14 19:30:08 +00:00
|
|
|
if CONF_HASS in ports:
|
2018-10-23 19:52:01 +00:00
|
|
|
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]
|
2018-09-14 19:30:08 +00:00
|
|
|
del ports[CONF_HASS]
|
2018-10-03 09:07:25 +00:00
|
|
|
|
2018-09-14 19:30:08 +00:00
|
|
|
for port in ports:
|
|
|
|
if ports[port] == CONF_HASS:
|
2018-10-23 19:52:01 +00:00
|
|
|
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
|
2018-09-14 19:30:08 +00:00
|
|
|
|
2018-10-03 09:07:25 +00:00
|
|
|
return ports
|
|
|
|
|
2018-09-14 19:30:08 +00:00
|
|
|
|
2018-08-29 19:19:04 +00:00
|
|
|
# config
|
2018-08-30 14:38:43 +00:00
|
|
|
async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
2018-08-17 19:28:29 +00:00
|
|
|
"""Register a port mapping for Home Assistant via UPnP."""
|
2018-10-29 07:10:01 +00:00
|
|
|
await async_ensure_domain_data(hass)
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2018-08-30 14:38:43 +00:00
|
|
|
# ensure sane config
|
|
|
|
if DOMAIN not in config:
|
2018-09-01 08:14:21 +00:00
|
|
|
return True
|
2018-10-23 19:52:01 +00:00
|
|
|
upnp_config = config[DOMAIN]
|
2018-08-30 14:38:43 +00:00
|
|
|
|
2018-09-14 19:30:08 +00:00
|
|
|
# overridden local ip
|
2018-09-17 20:08:09 +00:00
|
|
|
if CONF_LOCAL_IP in upnp_config:
|
|
|
|
hass.data[DOMAIN]['local_ip'] = upnp_config[CONF_LOCAL_IP]
|
2018-08-30 14:38:43 +00:00
|
|
|
|
2018-09-07 22:11:23 +00:00
|
|
|
# determine ports
|
2018-10-01 16:25:54 +00:00
|
|
|
ports = {CONF_HASS: CONF_HASS} # default, port_mapping disabled by default
|
2018-09-17 20:08:09 +00:00
|
|
|
if CONF_PORTS in upnp_config:
|
2018-09-14 19:30:08 +00:00
|
|
|
# copy from config
|
2018-09-17 20:08:09 +00:00
|
|
|
ports = upnp_config[CONF_PORTS]
|
2018-09-07 22:11:23 +00:00
|
|
|
|
2018-08-30 14:38:43 +00:00
|
|
|
hass.data[DOMAIN]['auto_config'] = {
|
|
|
|
'active': True,
|
2018-10-01 16:25:54 +00:00
|
|
|
'enable_sensors': upnp_config[CONF_ENABLE_SENSORS],
|
|
|
|
'enable_port_mapping': upnp_config[CONF_ENABLE_PORT_MAPPING],
|
2018-09-07 22:11:23 +00:00
|
|
|
'ports': ports,
|
2018-08-30 14:38:43 +00:00
|
|
|
}
|
2018-08-17 19:28:29 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
2018-08-29 19:19:04 +00:00
|
|
|
|
|
|
|
# config flow
|
2018-08-30 14:38:43 +00:00
|
|
|
async def async_setup_entry(hass: HomeAssistantType,
|
|
|
|
config_entry: ConfigEntry):
|
2018-10-03 09:27:38 +00:00
|
|
|
"""Set up UPnP/IGD-device from a config entry."""
|
2018-10-29 07:10:01 +00:00
|
|
|
await async_ensure_domain_data(hass)
|
2018-08-29 19:19:04 +00:00
|
|
|
data = config_entry.data
|
|
|
|
|
2018-09-17 20:08:09 +00:00
|
|
|
# build UPnP/IGD device
|
2018-09-01 19:20:15 +00:00
|
|
|
ssdp_description = data[CONF_SSDP_DESCRIPTION]
|
2018-08-29 19:19:04 +00:00
|
|
|
try:
|
2018-09-07 22:11:23 +00:00
|
|
|
device = await Device.async_create_device(hass, ssdp_description)
|
2018-08-29 19:19:04 +00:00
|
|
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
2018-10-01 16:25:54 +00:00
|
|
|
_LOGGER.error('Unable to create upnp-device')
|
2018-10-03 09:07:25 +00:00
|
|
|
return False
|
2018-08-29 19:19:04 +00:00
|
|
|
|
2018-09-07 22:11:23 +00:00
|
|
|
hass.data[DOMAIN]['devices'][device.udn] = device
|
2018-08-29 19:19:04 +00:00
|
|
|
|
2018-08-30 14:38:43 +00:00
|
|
|
# port mapping
|
|
|
|
if data.get(CONF_ENABLE_PORT_MAPPING):
|
2018-10-23 19:52:01 +00:00
|
|
|
local_ip = hass.data[DOMAIN]['local_ip']
|
2018-09-07 22:11:23 +00:00
|
|
|
ports = hass.data[DOMAIN]['auto_config']['ports']
|
|
|
|
_LOGGER.debug('Enabling port mappings: %s', ports)
|
2018-09-14 19:30:08 +00:00
|
|
|
|
2018-10-23 19:52:01 +00:00
|
|
|
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)
|
2018-08-17 19:28:29 +00:00
|
|
|
|
|
|
|
# sensors
|
2018-08-30 14:38:43 +00:00
|
|
|
if data.get(CONF_ENABLE_SENSORS):
|
2018-09-07 22:11:23 +00:00
|
|
|
_LOGGER.debug('Enabling sensors')
|
2018-10-01 16:25:54 +00:00
|
|
|
|
|
|
|
# register sensor setup handlers
|
|
|
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
|
|
|
config_entry, 'sensor'))
|
2018-08-29 19:19:04 +00:00
|
|
|
|
2018-10-23 19:52:01 +00:00
|
|
|
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)
|
2018-08-17 19:28:29 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
2018-08-29 19:19:04 +00:00
|
|
|
|
2018-08-30 14:38:43 +00:00
|
|
|
async def async_unload_entry(hass: HomeAssistantType,
|
|
|
|
config_entry: ConfigEntry):
|
2018-08-17 19:28:29 +00:00
|
|
|
"""Unload a config entry."""
|
2018-08-29 19:19:04 +00:00
|
|
|
data = config_entry.data
|
2018-09-01 19:20:15 +00:00
|
|
|
udn = data[CONF_UDN]
|
2018-08-30 14:38:43 +00:00
|
|
|
|
2018-09-07 22:11:23 +00:00
|
|
|
if udn not in hass.data[DOMAIN]['devices']:
|
2018-08-30 14:38:43 +00:00
|
|
|
return True
|
2018-09-07 22:11:23 +00:00
|
|
|
device = hass.data[DOMAIN]['devices'][udn]
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2018-08-30 14:38:43 +00:00
|
|
|
# port mapping
|
|
|
|
if data.get(CONF_ENABLE_PORT_MAPPING):
|
2018-09-07 22:11:23 +00:00
|
|
|
_LOGGER.debug('Deleting port mappings')
|
|
|
|
await device.async_delete_port_mappings()
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2018-08-29 19:19:04 +00:00
|
|
|
# sensors
|
2018-10-01 16:25:54 +00:00
|
|
|
if data.get(CONF_ENABLE_SENSORS):
|
|
|
|
_LOGGER.debug('Deleting sensors')
|
2018-10-03 09:07:25 +00:00
|
|
|
dispatcher.async_dispatcher_send(hass, SIGNAL_REMOVE_SENSOR, device)
|
2018-08-29 19:19:04 +00:00
|
|
|
|
2018-09-01 16:13:45 +00:00
|
|
|
# clear stored device
|
2018-09-07 22:11:23 +00:00
|
|
|
del hass.data[DOMAIN]['devices'][udn]
|
2018-08-30 14:38:43 +00:00
|
|
|
|
2018-08-29 19:19:04 +00:00
|
|
|
return True
|