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.
 
 
 
 

155 lines
4.7 KiB

# -*- 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."""