You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

362 lines
14 KiB

4 years ago
  1. # -*- coding: utf-8 -*-
  2. """UPnP IGD module."""
  3. from datetime import timedelta
  4. from ipaddress import IPv4Address
  5. import logging
  6. from typing import List, NamedTuple, Optional
  7. from async_upnp_client.profile import UpnpProfileDevice
  8. _LOGGER = logging.getLogger(__name__)
  9. CommonLinkProperties = NamedTuple(
  10. 'CommonLinkProperties', [
  11. ('wan_access_type', str),
  12. ('layer1_upstream_max_bit_rate', int),
  13. ('layer1_downstream_max_bit_rate', int),
  14. ('physical_link_status', str)])
  15. ConnectionTypeInfo = NamedTuple(
  16. 'ConnectionTypeInfo', [
  17. ('connection_type', str),
  18. ('possible_connection_types', str)])
  19. StatusInfo = NamedTuple(
  20. 'StatusInfo', [
  21. ('connection_status', str),
  22. ('last_connection_error', str),
  23. ('uptime', int)])
  24. NatRsipStatusInfo = NamedTuple(
  25. 'NatRsipStatusInfo', [
  26. ('nat_enabled', bool),
  27. ('rsip_available', bool)])
  28. PortMappingEntry = NamedTuple(
  29. 'PortMappingEntry', [
  30. ('remote_host', Optional[IPv4Address]),
  31. ('external_port', int),
  32. ('protocol', str),
  33. ('internal_port', int),
  34. ('internal_client', IPv4Address),
  35. ('enabled', bool),
  36. ('description', str),
  37. ('lease_duration', Optional[timedelta])])
  38. class IgdDevice(UpnpProfileDevice):
  39. """Representation of a IGD device."""
  40. # pylint: disable=too-many-public-methods
  41. DEVICE_TYPES = [
  42. 'urn:schemas-upnp-org:device:InternetGatewayDevice:1',
  43. 'urn:schemas-upnp-org:device:InternetGatewayDevice:2',
  44. ]
  45. _SERVICE_TYPES = {
  46. 'WANPPPC': {
  47. 'urn:schemas-upnp-org:service:WANPPPConnection:1',
  48. },
  49. 'WANIPC': {
  50. 'urn:schemas-upnp-org:service:WANIPConnection:1',
  51. },
  52. 'WANCIC': {
  53. 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1',
  54. },
  55. 'L3FWD': {
  56. 'urn:schemas-upnp-org:service:Layer3Forwarding:1',
  57. },
  58. }
  59. def _any_action(self, service_names: List[str], action_name: str):
  60. for service_name in service_names:
  61. action = self._action(service_name, action_name)
  62. if action is not None:
  63. return action
  64. return None
  65. async def async_get_total_bytes_received(self) -> int:
  66. """Get total bytes received."""
  67. action = self._action('WANCIC', 'GetTotalBytesReceived')
  68. result = await action.async_call()
  69. return result['NewTotalBytesReceived']
  70. async def async_get_total_bytes_sent(self) -> int:
  71. """Get total bytes sent."""
  72. action = self._action('WANCIC', 'GetTotalBytesSent')
  73. result = await action.async_call()
  74. return result['NewTotalBytesSent']
  75. async def async_get_total_packets_received(self) -> int:
  76. """Get total packets received."""
  77. # pylint: disable=invalid-name
  78. action = self._action('WANCIC', 'GetTotalPacketsReceived')
  79. result = await action.async_call()
  80. return result['NewTotalPacketsReceived']
  81. async def async_get_total_packets_sent(self) -> int:
  82. """Get total packets sent."""
  83. action = self._action('WANCIC', 'GetTotalPacketsSent')
  84. result = await action.async_call()
  85. return result['NewTotalPacketsSent']
  86. async def async_get_enabled_for_internet(self) -> bool:
  87. """Get internet access enabled state."""
  88. action = self._action('WANCIC', 'GetEnabledForInternet')
  89. result = await action.async_call()
  90. return result['NewEnabledForInternet']
  91. async def async_set_enabled_for_internet(self, enabled: bool) -> None:
  92. """
  93. Set internet access enabled state.
  94. :param enabled whether access should be enabled
  95. """
  96. action = self._action('WANCIC', 'SetEnabledForInternet')
  97. await action.async_call(NewEnabledForInternet=enabled)
  98. async def async_get_common_link_properties(self) -> CommonLinkProperties:
  99. """Get common link properties."""
  100. # pylint: disable=invalid-name
  101. action = self._action('WANCIC', 'GetCommonLinkProperties')
  102. result = await action.async_call()
  103. return CommonLinkProperties(
  104. result['NewWANAccessType'],
  105. result['NewLayer1UpstreamMaxBitRate'],
  106. result['NewLayer1DownstreamMaxBitRate'],
  107. result['NewPhysicalLinkStatus'])
  108. async def async_get_external_ip_address(self, services: List = None) -> str:
  109. """
  110. Get the external IP address.
  111. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  112. """
  113. services = services or ['WANIPC', 'WANPPP']
  114. action = self._any_action(services, 'GetExternalIPAddress')
  115. result = await action.async_call()
  116. return result['NewExternalIPAddress']
  117. async def async_get_generic_port_mapping_entry(self,
  118. port_mapping_index: int,
  119. services: List = None) -> PortMappingEntry:
  120. """
  121. Get generic port mapping entry.
  122. :param port_mapping_index Index of port mapping entry
  123. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  124. """
  125. # pylint: disable=invalid-name
  126. services = services or ['WANIPC', 'WANPPP']
  127. action = self._any_action(services, 'GetGenericPortMappingEntry')
  128. result = await action.async_call(
  129. NewPortMappingIndex=port_mapping_index)
  130. return PortMappingEntry(
  131. IPv4Address(result['NewRemoteHost']) if result['NewRemoteHost'] else None,
  132. result['NewExternalPort'],
  133. result['NewProtocol'],
  134. result['NewInternalPort'],
  135. IPv4Address(result['NewInternalClient']),
  136. result['NewEnabled'],
  137. result['NewPortMappingDescription'],
  138. timedelta(seconds=result['NewLeaseDuration']) if result['NewLeaseDuration'] else None)
  139. async def async_get_specific_port_mapping_entry(self,
  140. remote_host: Optional[IPv4Address],
  141. external_port: int,
  142. protocol: str,
  143. services: List = None) -> PortMappingEntry:
  144. """
  145. Get specific port mapping entry.
  146. :param remote_host Address of remote host or None
  147. :param external_port External port
  148. :param protocol Protocol, 'TCP' or 'UDP'
  149. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  150. """
  151. # pylint: disable=invalid-name
  152. services = services or ['WANIPC', 'WANPPP']
  153. action = self._any_action(services, 'GetSpecificPortMappingEntry')
  154. result = await action.async_call(
  155. NewRemoteHost=remote_host.exploded if remote_host else '',
  156. NewExternalPort=external_port,
  157. NewProtocol=protocol)
  158. return PortMappingEntry(
  159. remote_host,
  160. external_port,
  161. protocol,
  162. result['NewInternalPort'],
  163. IPv4Address(result['NewInternalClient']),
  164. result['NewEnabled'],
  165. result['NewPortMappingDescription'],
  166. timedelta(seconds=result['NewLeaseDuration']) if result['NewLeaseDuration'] else None)
  167. async def async_add_port_mapping(self,
  168. remote_host: IPv4Address,
  169. external_port: int,
  170. protocol: str,
  171. internal_port: int,
  172. internal_client: IPv4Address,
  173. enabled: bool,
  174. description: str,
  175. lease_duration: timedelta,
  176. services: List = None):
  177. """
  178. Add a port mapping.
  179. :param remote_host Address of remote host or None
  180. :param external_port External port
  181. :param protocol Protocol, 'TCP' or 'UDP'
  182. :param internal_port Internal port
  183. :param internal_client Address of internal host
  184. :param enabled Port mapping enabled
  185. :param description Description for port mapping
  186. :param lease_duration Lease duration
  187. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  188. """
  189. # pylint: disable=too-many-arguments
  190. services = services or ['WANIPC', 'WANPPP']
  191. action = self._any_action(services, 'AddPortMapping')
  192. await action.async_call(
  193. NewRemoteHost=remote_host.exploded if remote_host else '',
  194. NewExternalPort=external_port,
  195. NewProtocol=protocol,
  196. NewInternalPort=internal_port,
  197. NewInternalClient=internal_client.exploded,
  198. NewEnabled=enabled,
  199. NewPortMappingDescription=description,
  200. NewLeaseDuration=int(lease_duration.seconds) if lease_duration else 0)
  201. async def async_delete_port_mapping(self,
  202. remote_host: IPv4Address,
  203. external_port: int,
  204. protocol: str,
  205. services: List = None):
  206. """
  207. Delete an existing port mapping.
  208. :param remote_host Address of remote host or None
  209. :param external_port External port
  210. :param protocol Protocol, 'TCP' or 'UDP'
  211. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  212. """
  213. services = services or ['WANIPC', 'WANPPP']
  214. action = self._any_action(services, 'DeletePortMapping')
  215. await action.async_call(
  216. NewRemoteHost=remote_host.exploded if remote_host else '',
  217. NewExternalPort=external_port,
  218. NewProtocol=protocol)
  219. async def async_get_connection_type_info(self, services: List = None) -> ConnectionTypeInfo:
  220. """
  221. Get connection type info.
  222. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  223. """
  224. services = services or ['WANIPC', 'WANPPP']
  225. action = self._any_action(services, 'GetConnectionTypeInfo')
  226. result = await action.async_call()
  227. return ConnectionTypeInfo(
  228. result['NewConnectionType'],
  229. result['NewPossibleConnectionTypes'])
  230. async def async_set_connection_type(self, connection_type: str, services: List = None) -> None:
  231. """
  232. Set connection type.
  233. :param connection_type connection type
  234. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  235. """
  236. services = services or ['WANIPC', 'WANPPP']
  237. action = self._any_action(services, 'SetConnectionType')
  238. await action.async_call(NewConnectionType=connection_type)
  239. async def async_request_connection(self, services: List = None) -> None:
  240. """
  241. Request connection.
  242. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  243. """
  244. services = services or ['WANIPC', 'WANPPP']
  245. action = self._any_action(services, 'RequestConnection')
  246. await action.async_call()
  247. async def async_request_termination(self, services: List = None) -> None:
  248. """
  249. Request connection termination.
  250. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  251. """
  252. services = services or ['WANIPC', 'WANPPP']
  253. action = self._any_action(services, 'RequestTermination')
  254. await action.async_call()
  255. async def async_force_termination(self, services: List = None) -> None:
  256. """
  257. Force connection termination.
  258. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  259. """
  260. services = services or ['WANIPC', 'WANPPP']
  261. action = self._action(services, 'ForceTermination')
  262. await action.async_call()
  263. async def async_get_status_info(self, services: List = None) -> StatusInfo:
  264. """
  265. Get status info.
  266. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  267. """
  268. services = services or ['WANIPC', 'WANPPP']
  269. action = self._any_action(services, 'GetStatusInfo')
  270. result = await action.async_call()
  271. return StatusInfo(
  272. result['NewConnectionStatus'],
  273. result['NewLastConnectionError'],
  274. result['NewUptime'])
  275. async def async_get_port_mapping_number_of_entries(self, services: List = None) -> int:
  276. """
  277. Get number of port mapping entries.
  278. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  279. """
  280. # pylint: disable=invalid-name
  281. services = services or ['WANIPC', 'WANPPP']
  282. action = self._any_action(services, 'GetPortMappingNumberOfEntries')
  283. result = await action.async_call()
  284. return result['NewPortMappingNumberOfEntries']
  285. async def async_get_nat_rsip_status(self, services: List = None) -> NatRsipStatusInfo:
  286. """
  287. Get NAT enabled and RSIP availability statuses.
  288. :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP]
  289. """
  290. services = services or ['WANIPC', 'WANPPP']
  291. action = self._any_action(services, 'GetNATRSIPStatus')
  292. result = await action.async_call()
  293. return NatRsipStatusInfo(
  294. result['NewNATEnabled'],
  295. result['NewRSIPAvailable'])
  296. async def async_get_default_connection_service(self) -> str:
  297. """Get default connection service."""
  298. # pylint: disable=invalid-name
  299. action = self._action('L3FWD', 'GetDefaultConnectionService')
  300. result = await action.async_call()
  301. return result['NewDefaultConnectionService']
  302. async def async_set_default_connection_service(self, service: str) -> None:
  303. """
  304. Set default connection service.
  305. :param service default connection service
  306. """
  307. # pylint: disable=invalid-name
  308. action = self._action('L3FWD', 'SetDefaultConnectionService')
  309. await action.async_call(NewDefaultConnectionService=service)