# -*- 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)