362 lines
14 KiB
Python
362 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""UPnP IGD module."""
|
|
|
|
from datetime import timedelta
|
|
from ipaddress import IPv4Address
|
|
import logging
|
|
from typing import List, NamedTuple, Optional
|
|
|
|
from async_upnp_client.profile import UpnpProfileDevice
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
CommonLinkProperties = NamedTuple(
|
|
'CommonLinkProperties', [
|
|
('wan_access_type', str),
|
|
('layer1_upstream_max_bit_rate', int),
|
|
('layer1_downstream_max_bit_rate', int),
|
|
('physical_link_status', str)])
|
|
|
|
ConnectionTypeInfo = NamedTuple(
|
|
'ConnectionTypeInfo', [
|
|
('connection_type', str),
|
|
('possible_connection_types', str)])
|
|
|
|
StatusInfo = NamedTuple(
|
|
'StatusInfo', [
|
|
('connection_status', str),
|
|
('last_connection_error', str),
|
|
('uptime', int)])
|
|
|
|
NatRsipStatusInfo = NamedTuple(
|
|
'NatRsipStatusInfo', [
|
|
('nat_enabled', bool),
|
|
('rsip_available', bool)])
|
|
|
|
PortMappingEntry = NamedTuple(
|
|
'PortMappingEntry', [
|
|
('remote_host', Optional[IPv4Address]),
|
|
('external_port', int),
|
|
('protocol', str),
|
|
('internal_port', int),
|
|
('internal_client', IPv4Address),
|
|
('enabled', bool),
|
|
('description', str),
|
|
('lease_duration', Optional[timedelta])])
|
|
|
|
|
|
class IgdDevice(UpnpProfileDevice):
|
|
"""Representation of a IGD device."""
|
|
|
|
# pylint: disable=too-many-public-methods
|
|
|
|
DEVICE_TYPES = [
|
|
'urn:schemas-upnp-org:device:InternetGatewayDevice:1',
|
|
'urn:schemas-upnp-org:device:InternetGatewayDevice:2',
|
|
]
|
|
|
|
_SERVICE_TYPES = {
|
|
'WANPPPC': {
|
|
'urn:schemas-upnp-org:service:WANPPPConnection:1',
|
|
},
|
|
'WANIPC': {
|
|
'urn:schemas-upnp-org:service:WANIPConnection:1',
|
|
},
|
|
'WANCIC': {
|
|
'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
|
|
},
|
|
'L3FWD': {
|
|
'urn:schemas-upnp-org:service:Layer3Forwarding:1',
|
|
},
|
|
}
|
|
|
|
def _any_action(self, service_names: List[str], action_name: str):
|
|
for service_name in service_names:
|
|
action = self._action(service_name, action_name)
|
|
if action is not None:
|
|
return action
|
|
return None
|
|
|
|
async def async_get_total_bytes_received(self) -> int:
|
|
"""Get total bytes received."""
|
|
action = self._action('WANCIC', 'GetTotalBytesReceived')
|
|
result = await action.async_call()
|
|
return result['NewTotalBytesReceived']
|
|
|
|
async def async_get_total_bytes_sent(self) -> int:
|
|
"""Get total bytes sent."""
|
|
action = self._action('WANCIC', 'GetTotalBytesSent')
|
|
result = await action.async_call()
|
|
return result['NewTotalBytesSent']
|
|
|
|
async def async_get_total_packets_received(self) -> int:
|
|
"""Get total packets received."""
|
|
# pylint: disable=invalid-name
|
|
action = self._action('WANCIC', 'GetTotalPacketsReceived')
|
|
result = await action.async_call()
|
|
return result['NewTotalPacketsReceived']
|
|
|
|
async def async_get_total_packets_sent(self) -> int:
|
|
"""Get total packets sent."""
|
|
action = self._action('WANCIC', 'GetTotalPacketsSent')
|
|
result = await action.async_call()
|
|
return result['NewTotalPacketsSent']
|
|
|
|
async def async_get_enabled_for_internet(self) -> bool:
|
|
"""Get internet access enabled state."""
|
|
action = self._action('WANCIC', 'GetEnabledForInternet')
|
|
result = await action.async_call()
|
|
return result['NewEnabledForInternet']
|
|
|
|
async def async_set_enabled_for_internet(self, enabled: bool) -> None:
|
|
"""
|
|
Set internet access enabled state.
|
|
|
|
:param enabled whether access should be enabled
|
|
"""
|
|
action = self._action('WANCIC', 'SetEnabledForInternet')
|
|
await action.async_call(NewEnabledForInternet=enabled)
|
|
|
|
async def async_get_common_link_properties(self) -> CommonLinkProperties:
|
|
"""Get common link properties."""
|
|
# pylint: disable=invalid-name
|
|
action = self._action('WANCIC', 'GetCommonLinkProperties')
|
|
result = await action.async_call()
|
|
return CommonLinkProperties(
|
|
result['NewWANAccessType'],
|
|
result['NewLayer1UpstreamMaxBitRate'],
|
|
result['NewLayer1DownstreamMaxBitRate'],
|
|
result['NewPhysicalLinkStatus'])
|
|
|
|
async def async_get_external_ip_address(self, services: List = None) -> str:
|
|
"""
|
|
Get the external IP address.
|
|
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'GetExternalIPAddress')
|
|
result = await action.async_call()
|
|
return result['NewExternalIPAddress']
|
|
|
|
async def async_get_generic_port_mapping_entry(self,
|
|
port_mapping_index: int,
|
|
services: List = None) -> PortMappingEntry:
|
|
"""
|
|
Get generic port mapping entry.
|
|
|
|
:param port_mapping_index Index of port mapping entry
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
# pylint: disable=invalid-name
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'GetGenericPortMappingEntry')
|
|
result = await action.async_call(
|
|
NewPortMappingIndex=port_mapping_index)
|
|
return PortMappingEntry(
|
|
IPv4Address(result['NewRemoteHost']) if result['NewRemoteHost'] else None,
|
|
result['NewExternalPort'],
|
|
result['NewProtocol'],
|
|
result['NewInternalPort'],
|
|
IPv4Address(result['NewInternalClient']),
|
|
result['NewEnabled'],
|
|
result['NewPortMappingDescription'],
|
|
timedelta(seconds=result['NewLeaseDuration']) if result['NewLeaseDuration'] else None)
|
|
|
|
async def async_get_specific_port_mapping_entry(self,
|
|
remote_host: Optional[IPv4Address],
|
|
external_port: int,
|
|
protocol: str,
|
|
services: List = None) -> PortMappingEntry:
|
|
"""
|
|
Get specific port mapping entry.
|
|
|
|
:param remote_host Address of remote host or None
|
|
:param external_port External port
|
|
:param protocol Protocol, 'TCP' or 'UDP'
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
# pylint: disable=invalid-name
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'GetSpecificPortMappingEntry')
|
|
result = await action.async_call(
|
|
NewRemoteHost=remote_host.exploded if remote_host else '',
|
|
NewExternalPort=external_port,
|
|
NewProtocol=protocol)
|
|
return PortMappingEntry(
|
|
remote_host,
|
|
external_port,
|
|
protocol,
|
|
result['NewInternalPort'],
|
|
IPv4Address(result['NewInternalClient']),
|
|
result['NewEnabled'],
|
|
result['NewPortMappingDescription'],
|
|
timedelta(seconds=result['NewLeaseDuration']) if result['NewLeaseDuration'] else None)
|
|
|
|
async def async_add_port_mapping(self,
|
|
remote_host: IPv4Address,
|
|
external_port: int,
|
|
protocol: str,
|
|
internal_port: int,
|
|
internal_client: IPv4Address,
|
|
enabled: bool,
|
|
description: str,
|
|
lease_duration: timedelta,
|
|
services: List = None):
|
|
"""
|
|
Add a port mapping.
|
|
|
|
:param remote_host Address of remote host or None
|
|
:param external_port External port
|
|
:param protocol Protocol, 'TCP' or 'UDP'
|
|
:param internal_port Internal port
|
|
:param internal_client Address of internal host
|
|
:param enabled Port mapping enabled
|
|
:param description Description for port mapping
|
|
:param lease_duration Lease duration
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
# pylint: disable=too-many-arguments
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'AddPortMapping')
|
|
await action.async_call(
|
|
NewRemoteHost=remote_host.exploded if remote_host else '',
|
|
NewExternalPort=external_port,
|
|
NewProtocol=protocol,
|
|
NewInternalPort=internal_port,
|
|
NewInternalClient=internal_client.exploded,
|
|
NewEnabled=enabled,
|
|
NewPortMappingDescription=description,
|
|
NewLeaseDuration=int(lease_duration.seconds) if lease_duration else 0)
|
|
|
|
async def async_delete_port_mapping(self,
|
|
remote_host: IPv4Address,
|
|
external_port: int,
|
|
protocol: str,
|
|
services: List = None):
|
|
"""
|
|
Delete an existing port mapping.
|
|
|
|
:param remote_host Address of remote host or None
|
|
:param external_port External port
|
|
:param protocol Protocol, 'TCP' or 'UDP'
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'DeletePortMapping')
|
|
await action.async_call(
|
|
NewRemoteHost=remote_host.exploded if remote_host else '',
|
|
NewExternalPort=external_port,
|
|
NewProtocol=protocol)
|
|
|
|
async def async_get_connection_type_info(self, services: List = None) -> ConnectionTypeInfo:
|
|
"""
|
|
Get connection type info.
|
|
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'GetConnectionTypeInfo')
|
|
result = await action.async_call()
|
|
return ConnectionTypeInfo(
|
|
result['NewConnectionType'],
|
|
result['NewPossibleConnectionTypes'])
|
|
|
|
async def async_set_connection_type(self, connection_type: str, services: List = None) -> None:
|
|
"""
|
|
Set connection type.
|
|
|
|
:param connection_type connection type
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'SetConnectionType')
|
|
await action.async_call(NewConnectionType=connection_type)
|
|
|
|
async def async_request_connection(self, services: List = None) -> None:
|
|
"""
|
|
Request connection.
|
|
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'RequestConnection')
|
|
await action.async_call()
|
|
|
|
async def async_request_termination(self, services: List = None) -> None:
|
|
"""
|
|
Request connection termination.
|
|
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'RequestTermination')
|
|
await action.async_call()
|
|
|
|
async def async_force_termination(self, services: List = None) -> None:
|
|
"""
|
|
Force connection termination.
|
|
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._action(services, 'ForceTermination')
|
|
await action.async_call()
|
|
|
|
async def async_get_status_info(self, services: List = None) -> StatusInfo:
|
|
"""
|
|
Get status info.
|
|
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'GetStatusInfo')
|
|
result = await action.async_call()
|
|
return StatusInfo(
|
|
result['NewConnectionStatus'],
|
|
result['NewLastConnectionError'],
|
|
result['NewUptime'])
|
|
|
|
async def async_get_port_mapping_number_of_entries(self, services: List = None) -> int:
|
|
"""
|
|
Get number of port mapping entries.
|
|
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
# pylint: disable=invalid-name
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'GetPortMappingNumberOfEntries')
|
|
result = await action.async_call()
|
|
return result['NewPortMappingNumberOfEntries']
|
|
|
|
async def async_get_nat_rsip_status(self, services: List = None) -> NatRsipStatusInfo:
|
|
"""
|
|
Get NAT enabled and RSIP availability statuses.
|
|
|
|
:param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
|
|
"""
|
|
services = services or ['WANIPC', 'WANPPP']
|
|
action = self._any_action(services, 'GetNATRSIPStatus')
|
|
result = await action.async_call()
|
|
return NatRsipStatusInfo(
|
|
result['NewNATEnabled'],
|
|
result['NewRSIPAvailable'])
|
|
|
|
async def async_get_default_connection_service(self) -> str:
|
|
"""Get default connection service."""
|
|
# pylint: disable=invalid-name
|
|
action = self._action('L3FWD', 'GetDefaultConnectionService')
|
|
result = await action.async_call()
|
|
return result['NewDefaultConnectionService']
|
|
|
|
async def async_set_default_connection_service(self, service: str) -> None:
|
|
"""
|
|
Set default connection service.
|
|
|
|
:param service default connection service
|
|
"""
|
|
# pylint: disable=invalid-name
|
|
action = self._action('L3FWD', 'SetDefaultConnectionService')
|
|
await action.async_call(NewDefaultConnectionService=service)
|