platypush/platypush/plugins/zeroconf.py

158 lines
5.9 KiB
Python

import queue
import socket
import time
from typing import List, Dict, Any, Optional, Union
from zeroconf import Zeroconf, ServiceInfo, ServiceBrowser, ServiceListener, ZeroconfServiceTypes
from platypush.context import get_bus
from platypush.message.event.zeroconf import ZeroconfServiceAddedEvent, ZeroconfServiceRemovedEvent, \
ZeroconfServiceUpdatedEvent
from platypush.plugins import Plugin, action
class ZeroconfListener(ServiceListener):
def __init__(self, evt_queue: queue.Queue):
super().__init__()
self.evt_queue = evt_queue
@classmethod
def get_service_info(cls, zc: Zeroconf, type_: str, name: str) -> dict:
info = zc.get_service_info(type_, name)
if not info:
return {}
return cls.parse_service_info(info)
@staticmethod
def parse_service_info(info: ServiceInfo) -> dict:
return {
'addresses': [socket.inet_ntoa(addr) for addr in info.addresses if info.addresses],
'port': info.port,
'host_ttl': info.host_ttl,
'other_ttl': info.other_ttl,
'priority': info.priority,
'properties': {k.decode() if isinstance(k, bytes) else k: v.decode() if isinstance(v, bytes) else v
for k, v in info.properties.items()},
'server': info.server,
'weight': info.weight,
}
def add_service(self, zc: Zeroconf, type_: str, name: str):
info = self.get_service_info(zc, type_, name)
self.evt_queue.put(ZeroconfServiceAddedEvent(service_type=type_, service_name=name, service_info=info))
def remove_service(self, zc: Zeroconf, type_: str, name: str):
info = self.get_service_info(zc, type_, name)
self.evt_queue.put(ZeroconfServiceRemovedEvent(service_type=type_, service_name=name, service_info=info))
def update_service(self, zc: Zeroconf, type_: str, name: str):
info = self.get_service_info(zc, type_, name)
self.evt_queue.put(ZeroconfServiceUpdatedEvent(service_type=type_, service_name=name, service_info=info))
class ZeroconfPlugin(Plugin):
"""
Plugin for Zeroconf services discovery.
Triggers:
* :class:`platypush.message.event.zeroconf.ZeroconfServiceAddedEvent` when a new service is discovered.
* :class:`platypush.message.event.zeroconf.ZeroconfServiceUpdatedEvent` when a service is updated.
* :class:`platypush.message.event.zeroconf.ZeroconfServiceRemovedEvent` when a service is removed.
Requires:
* **zeroconf** (``pip install zeroconf``)
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._discovery_in_progress = False
@action
def get_services(self, timeout: int = 5) -> List[str]:
"""
Get the full list of services found on the network.
:param timeout: Discovery timeout in seconds (default: 5).
:return: List of the services as strings.
"""
return list(ZeroconfServiceTypes.find(timeout=timeout))
@action
def discover_service(self, service: Union[str, list], timeout: Optional[int] = 5) -> Dict[str, Any]:
"""
Find all the services matching the specified type.
:param service: Service type (e.g. ``_http._tcp.local.``) or list of service types.
:param timeout: Browser timeout in seconds (default: 5). Specify None for no timeout - in such case the
discovery will loop forever and generate events upon service changes.
:return: A ``service_type -> [service_names]`` mapping. Example:
.. code-block:: json
{
"host1._platypush-http._tcp.local.": {
"type": "_platypush-http._tcp.local.",
"name": "host1._platypush-http._tcp.local.",
"info": {
"addresses": ["192.168.1.11"],
"port": 8008,
"host_ttl": 120,
"other_ttl": 4500,
"priority": 0,
"properties": {
"name": "Platypush",
"vendor": "Platypush",
"version": "0.13.2"
},
"server": "host1._platypush-http._tcp.local.",
"weight": 0
}
}
}
"""
assert not self._discovery_in_progress, 'A discovery process is already running'
self._discovery_in_progress = True
evt_queue = queue.Queue()
zc = Zeroconf()
listener = ZeroconfListener(evt_queue=evt_queue)
discovery_start = time.time()
services = {}
browser = None
try:
browser = ServiceBrowser(zc, service, listener)
while timeout and time.time() - discovery_start < timeout:
to = discovery_start + timeout - time.time() if timeout else None
try:
evt = evt_queue.get(block=True, timeout=to)
if isinstance(evt, ZeroconfServiceAddedEvent) or isinstance(evt, ZeroconfServiceUpdatedEvent):
services[evt.service_name] = {
'type': evt.service_type,
'name': evt.service_name,
'info': evt.service_info,
}
elif isinstance(evt, ZeroconfServiceRemovedEvent):
if evt.service_name in services:
del services[evt.service_name]
get_bus().post(evt)
except queue.Empty:
if not services:
self.logger.warning('No such service discovered: {}'.format(service))
finally:
if browser:
browser.cancel()
zc.close()
self._discovery_in_progress = False
return services
# vim:sw=4:ts=4:et: