2018-09-17 20:08:09 +00:00
|
|
|
"""Hass representation of an UPnP/IGD."""
|
2018-09-07 22:11:23 +00:00
|
|
|
import asyncio
|
|
|
|
from ipaddress import IPv4Address
|
|
|
|
|
|
|
|
import aiohttp
|
2019-10-09 23:16:29 +00:00
|
|
|
from async_upnp_client.profiles.igd import IgdDevice
|
2018-09-07 22:11:23 +00:00
|
|
|
|
|
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
|
|
from homeassistant.helpers.typing import HomeAssistantType
|
|
|
|
|
|
|
|
from .const import LOGGER as _LOGGER
|
2019-07-31 19:25:30 +00:00
|
|
|
from .const import DOMAIN, CONF_LOCAL_IP
|
2018-09-07 22:11:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Device:
|
2018-09-17 20:08:09 +00:00
|
|
|
"""Hass representation of an UPnP/IGD."""
|
2018-09-07 22:11:23 +00:00
|
|
|
|
|
|
|
def __init__(self, igd_device):
|
|
|
|
"""Initializer."""
|
|
|
|
self._igd_device = igd_device
|
|
|
|
self._mapped_ports = []
|
|
|
|
|
2018-12-21 17:25:23 +00:00
|
|
|
@classmethod
|
|
|
|
async def async_discover(cls, hass: HomeAssistantType):
|
|
|
|
"""Discovery UPNP/IGD devices."""
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("Discovering UPnP/IGD devices")
|
2019-03-25 00:46:15 +00:00
|
|
|
local_ip = None
|
2019-07-31 19:25:30 +00:00
|
|
|
if DOMAIN in hass.data and "config" in hass.data[DOMAIN]:
|
|
|
|
local_ip = hass.data[DOMAIN]["config"].get(CONF_LOCAL_IP)
|
2019-01-19 17:08:53 +00:00
|
|
|
if local_ip:
|
|
|
|
local_ip = IPv4Address(local_ip)
|
2018-12-21 17:25:23 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
discovery_infos = await IgdDevice.async_search(source_ip=local_ip, timeout=10)
|
2018-12-21 17:25:23 +00:00
|
|
|
|
|
|
|
# add extra info and store devices
|
|
|
|
devices = []
|
|
|
|
for discovery_info in discovery_infos:
|
2019-07-31 19:25:30 +00:00
|
|
|
discovery_info["udn"] = discovery_info["_udn"]
|
|
|
|
discovery_info["ssdp_description"] = discovery_info["location"]
|
|
|
|
discovery_info["source"] = "async_upnp_client"
|
|
|
|
_LOGGER.debug("Discovered device: %s", discovery_info)
|
2018-12-21 17:25:23 +00:00
|
|
|
|
|
|
|
devices.append(discovery_info)
|
|
|
|
|
|
|
|
return devices
|
|
|
|
|
2018-09-07 22:11:23 +00:00
|
|
|
@classmethod
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_create_device(cls, hass: HomeAssistantType, ssdp_description: str):
|
2018-09-17 20:08:09 +00:00
|
|
|
"""Create UPnP/IGD device."""
|
2018-09-07 22:11:23 +00:00
|
|
|
# build async_upnp_client requester
|
|
|
|
from async_upnp_client.aiohttp import AiohttpSessionRequester
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-09-07 22:11:23 +00:00
|
|
|
session = async_get_clientsession(hass)
|
|
|
|
requester = AiohttpSessionRequester(session, True)
|
|
|
|
|
|
|
|
# create async_upnp_client device
|
|
|
|
from async_upnp_client import UpnpFactory
|
2019-07-31 19:25:30 +00:00
|
|
|
|
|
|
|
factory = UpnpFactory(requester, disable_state_variable_validation=True)
|
2018-09-07 22:11:23 +00:00
|
|
|
upnp_device = await factory.async_create_device(ssdp_description)
|
|
|
|
|
|
|
|
igd_device = IgdDevice(upnp_device, None)
|
|
|
|
|
2018-10-01 16:25:54 +00:00
|
|
|
return cls(igd_device)
|
2018-09-07 22:11:23 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def udn(self):
|
|
|
|
"""Get the UDN."""
|
|
|
|
return self._igd_device.udn
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Get the name."""
|
|
|
|
return self._igd_device.name
|
|
|
|
|
2018-12-21 17:25:23 +00:00
|
|
|
@property
|
|
|
|
def manufacturer(self):
|
|
|
|
"""Get the manufacturer."""
|
|
|
|
return self._igd_device.manufacturer
|
|
|
|
|
2019-10-31 19:51:35 +00:00
|
|
|
@property
|
|
|
|
def model_name(self):
|
|
|
|
"""Get the model name."""
|
|
|
|
return self._igd_device.model_name
|
|
|
|
|
2018-10-23 19:52:01 +00:00
|
|
|
async def async_add_port_mappings(self, ports, local_ip):
|
2018-09-07 22:11:23 +00:00
|
|
|
"""Add port mappings."""
|
2019-07-31 19:25:30 +00:00
|
|
|
if local_ip == "127.0.0.1":
|
|
|
|
_LOGGER.error("Could not create port mapping, our IP is 127.0.0.1")
|
2018-10-23 19:52:01 +00:00
|
|
|
|
|
|
|
# determine local ip, ensure sane IP
|
2018-09-07 22:11:23 +00:00
|
|
|
local_ip = IPv4Address(local_ip)
|
|
|
|
|
|
|
|
# create port mappings
|
|
|
|
for external_port, internal_port in ports.items():
|
2019-07-31 19:25:30 +00:00
|
|
|
await self._async_add_port_mapping(external_port, local_ip, internal_port)
|
2018-09-07 22:11:23 +00:00
|
|
|
self._mapped_ports.append(external_port)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def _async_add_port_mapping(self, external_port, local_ip, internal_port):
|
2018-09-07 22:11:23 +00:00
|
|
|
"""Add a port mapping."""
|
|
|
|
# create port mapping
|
|
|
|
from async_upnp_client import UpnpError
|
2019-07-31 19:25:30 +00:00
|
|
|
|
|
|
|
_LOGGER.info(
|
|
|
|
"Creating port mapping %s:%s:%s (TCP)",
|
|
|
|
external_port,
|
|
|
|
local_ip,
|
|
|
|
internal_port,
|
|
|
|
)
|
2018-09-07 22:11:23 +00:00
|
|
|
try:
|
|
|
|
await self._igd_device.async_add_port_mapping(
|
|
|
|
remote_host=None,
|
|
|
|
external_port=external_port,
|
2019-07-31 19:25:30 +00:00
|
|
|
protocol="TCP",
|
2018-09-07 22:11:23 +00:00
|
|
|
internal_port=internal_port,
|
|
|
|
internal_client=local_ip,
|
|
|
|
enabled=True,
|
|
|
|
description="Home Assistant",
|
2019-07-31 19:25:30 +00:00
|
|
|
lease_duration=None,
|
|
|
|
)
|
2018-09-07 22:11:23 +00:00
|
|
|
|
|
|
|
self._mapped_ports.append(external_port)
|
|
|
|
except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError):
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error(
|
|
|
|
"Could not add port mapping: %s:%s:%s",
|
|
|
|
external_port,
|
|
|
|
local_ip,
|
|
|
|
internal_port,
|
|
|
|
)
|
2018-09-07 22:11:23 +00:00
|
|
|
|
|
|
|
async def async_delete_port_mappings(self):
|
|
|
|
"""Remove a port mapping."""
|
|
|
|
for port in self._mapped_ports:
|
|
|
|
await self._async_delete_port_mapping(port)
|
|
|
|
|
|
|
|
async def _async_delete_port_mapping(self, external_port):
|
|
|
|
"""Remove a port mapping."""
|
|
|
|
from async_upnp_client import UpnpError
|
2019-07-31 19:25:30 +00:00
|
|
|
|
|
|
|
_LOGGER.info("Deleting port mapping %s (TCP)", external_port)
|
2018-09-07 22:11:23 +00:00
|
|
|
try:
|
|
|
|
await self._igd_device.async_delete_port_mapping(
|
2019-07-31 19:25:30 +00:00
|
|
|
remote_host=None, external_port=external_port, protocol="TCP"
|
|
|
|
)
|
2018-09-07 22:11:23 +00:00
|
|
|
|
|
|
|
self._mapped_ports.remove(external_port)
|
|
|
|
except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError):
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Could not delete port mapping")
|
2018-09-07 22:11:23 +00:00
|
|
|
|
|
|
|
async def async_get_total_bytes_received(self):
|
|
|
|
"""Get total bytes received."""
|
|
|
|
return await self._igd_device.async_get_total_bytes_received()
|
|
|
|
|
|
|
|
async def async_get_total_bytes_sent(self):
|
|
|
|
"""Get total bytes sent."""
|
|
|
|
return await self._igd_device.async_get_total_bytes_sent()
|
|
|
|
|
|
|
|
async def async_get_total_packets_received(self):
|
|
|
|
"""Get total packets received."""
|
|
|
|
# pylint: disable=invalid-name
|
|
|
|
return await self._igd_device.async_get_total_packets_received()
|
|
|
|
|
|
|
|
async def async_get_total_packets_sent(self):
|
|
|
|
"""Get total packets sent."""
|
|
|
|
return await self._igd_device.async_get_total_packets_sent()
|