156 lines
4.7 KiB
Python
156 lines
4.7 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
"""UPnP discovery via Simple Service Discovery Protocol (SSDP)."""
|
||
|
import asyncio
|
||
|
import logging
|
||
|
import socket
|
||
|
|
||
|
from datetime import datetime
|
||
|
from ipaddress import IPv4Address
|
||
|
|
||
|
|
||
|
_LOGGER = logging.getLogger(__name__)
|
||
|
_LOGGER_TRAFFIC = logging.getLogger("async_upnp_client.traffic")
|
||
|
|
||
|
SSDP_TARGET = ('239.255.255.250', 1900)
|
||
|
SSDP_ST_ALL = 'ssdp:all'
|
||
|
SSDP_ST_ROOTDEVICE = 'upnp:rootdevice'
|
||
|
SSDP_MX = 4
|
||
|
RECV_SIZE = 32678
|
||
|
|
||
|
|
||
|
def _discovery_message(ssdp_target: str, ssdp_mx: int, ssdp_st: str):
|
||
|
"""Construct a SSDP packet."""
|
||
|
return 'M-SEARCH * HTTP/1.1\r\n' \
|
||
|
'HOST:{target}:{port}\r\n' \
|
||
|
'MAN:"ssdp:discover"\r\n' \
|
||
|
'MX:{mx}\r\n' \
|
||
|
'ST:{st}\r\n' \
|
||
|
'\r\n'.format(target=ssdp_target[0], port=ssdp_target[1],
|
||
|
mx=ssdp_mx, st=ssdp_st).encode()
|
||
|
|
||
|
|
||
|
def _parse_response(data):
|
||
|
headers = {}
|
||
|
for line in data.splitlines():
|
||
|
decoded = line.decode()
|
||
|
if ':' in decoded:
|
||
|
key, value = decoded.split(':', 1)
|
||
|
key = key.strip().lower()
|
||
|
headers[key] = value.strip()
|
||
|
|
||
|
# custom data
|
||
|
headers['_timestamp'] = datetime.now()
|
||
|
return headers
|
||
|
|
||
|
|
||
|
def discover(timeout: int = SSDP_MX,
|
||
|
service_type: str = SSDP_ST_ALL,
|
||
|
source_ip: IPv4Address = None):
|
||
|
"""Discover devices via SSDP."""
|
||
|
# create socket
|
||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, timeout)
|
||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
|
sock.settimeout(timeout)
|
||
|
if source_ip:
|
||
|
sock.bind((source_ip, 0))
|
||
|
|
||
|
# send discovery packet
|
||
|
message = _discovery_message(SSDP_TARGET, timeout, service_type)
|
||
|
_LOGGER.debug('Sending discovery message')
|
||
|
_LOGGER_TRAFFIC.debug('Sending message:\n%s', message)
|
||
|
sock.sendto(message, SSDP_TARGET)
|
||
|
|
||
|
# handle replies
|
||
|
responses = []
|
||
|
try:
|
||
|
while True:
|
||
|
# parse response
|
||
|
data, _ = sock.recvfrom(RECV_SIZE)
|
||
|
_LOGGER_TRAFFIC.debug('Received packet:\n%s', data)
|
||
|
|
||
|
response = _parse_response(data)
|
||
|
_LOGGER.debug('Received response: %s', response)
|
||
|
|
||
|
if response not in responses:
|
||
|
responses.append(response)
|
||
|
except socket.timeout:
|
||
|
pass
|
||
|
|
||
|
sock.close()
|
||
|
return responses
|
||
|
|
||
|
|
||
|
async def async_discover(timeout: int = SSDP_MX,
|
||
|
service_type: str = SSDP_ST_ALL,
|
||
|
source_ip: IPv4Address = None,
|
||
|
async_callback=None,
|
||
|
loop=None):
|
||
|
"""Discover devices via SSDP."""
|
||
|
loop = loop or asyncio.get_event_loop()
|
||
|
|
||
|
# create socket
|
||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, timeout)
|
||
|
# sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
|
if source_ip:
|
||
|
sock.bind((source_ip, 0))
|
||
|
|
||
|
# create protocol and send discovery packet
|
||
|
connect = loop.create_datagram_endpoint(
|
||
|
lambda: SsdpDiscoveryProtocol(loop, timeout, service_type, async_callback),
|
||
|
sock=sock,
|
||
|
)
|
||
|
transport, protocol = await connect
|
||
|
|
||
|
# wait for devices to respond
|
||
|
await asyncio.sleep(timeout)
|
||
|
|
||
|
transport.close()
|
||
|
|
||
|
return protocol.responses
|
||
|
|
||
|
|
||
|
class SsdpDiscoveryProtocol:
|
||
|
"""SSDP Discovery Protocol."""
|
||
|
|
||
|
def __init__(self, loop, timeout, service_type, async_callback):
|
||
|
"""Initializer."""
|
||
|
self.loop = loop
|
||
|
self.timeout = timeout
|
||
|
self.service_type = service_type
|
||
|
self.async_callback = async_callback
|
||
|
|
||
|
self.on_con_lost = loop.create_future()
|
||
|
self.transport = None
|
||
|
self.responses = []
|
||
|
|
||
|
def connection_made(self, transport):
|
||
|
"""Handle connection made."""
|
||
|
self.transport = transport
|
||
|
|
||
|
message = _discovery_message(SSDP_TARGET, self.timeout, self.service_type)
|
||
|
self.transport.sendto(message, SSDP_TARGET)
|
||
|
|
||
|
def datagram_received(self, data, addr):
|
||
|
"""Handle a discovery-response."""
|
||
|
_LOGGER_TRAFFIC.debug('Received packet from %s:\n%s', addr, data)
|
||
|
|
||
|
response = _parse_response(data)
|
||
|
_LOGGER.debug('Received response: %s', response)
|
||
|
|
||
|
if response not in self.responses:
|
||
|
self.responses.append(response)
|
||
|
|
||
|
if self.async_callback:
|
||
|
callback = self.async_callback(response)
|
||
|
self.loop.create_task(callback)
|
||
|
|
||
|
def error_received(self, exc):
|
||
|
"""Handle an error."""
|
||
|
# pylint: disable=no-self-use
|
||
|
_LOGGER.error('Received error: %s', exc)
|
||
|
|
||
|
def connection_lost(self, exc):
|
||
|
"""Handle connection lost."""
|