Initial Configuration Push

This commit is contained in:
CCOSTAN
2016-10-11 16:42:06 +00:00
parent b83eeadfcb
commit 5127bc2109
2145 changed files with 298464 additions and 0 deletions

1
deps/netdisco/__init__.py vendored Normal file
View File

@@ -0,0 +1 @@
"""Module to scan the network using uPnP and mDNS for devices and services."""

31
deps/netdisco/__main__.py vendored Normal file
View File

@@ -0,0 +1,31 @@
"""Command line tool to print discocvered devices or dump raw data."""
from __future__ import print_function
import sys
from netdisco.discovery import NetworkDiscovery
def main():
"""Handle command line execution."""
netdisco = NetworkDiscovery()
netdisco.scan()
# Pass in command line argument dump to get the raw data
if sys.argv[-1] == 'dump':
netdisco.print_raw_data()
print()
print()
print("Discovered devices:")
count = 0
for dev in netdisco.discover():
count += 1
print(dev, netdisco.get_info(dev))
print()
print("Discovered {} devices".format(count))
netdisco.stop()
if __name__ == '__main__':
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

15
deps/netdisco/const.py vendored Normal file
View File

@@ -0,0 +1,15 @@
"""Constants of services that can be discovered."""
BELKIN_WEMO = "belkin_wemo"
DLNA = "DLNA"
GOOGLE_CAST = "google_cast"
PHILIPS_HUE = "philips_hue"
PMS = 'plex_mediaserver'
LMS = 'logitech_mediaserver'
NETGEAR_ROUTER = "netgear_router"
SONOS = "sonos"
PANASONIC_VIERA = "panasonic_viera"
SABNZBD = 'sabnzbd'
KODI = 'kodi'
HOME_ASSISTANT = "home_assistant"
MYSTROM = 'mystrom'

11
deps/netdisco/discoverables/DLNA.py vendored Normal file
View File

@@ -0,0 +1,11 @@
"""Discover DLNA services."""
from . import SSDPDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(SSDPDiscoverable):
"""Add support for discovering DLNA services."""
def get_entries(self):
"""Get all the DLNA service uPnP entries."""
return self.find_by_st("urn:schemas-upnp-org:device:MediaServer:1")

133
deps/netdisco/discoverables/__init__.py vendored Normal file
View File

@@ -0,0 +1,133 @@
"""Provides helpful stuff for discoverables."""
# pylint: disable=abstract-method
class BaseDiscoverable(object):
"""Base class for discoverable services or device types."""
def is_discovered(self):
"""Return True if it is discovered."""
return len(self.get_entries()) > 0
def get_info(self):
"""Return a list with the important info for each item.
Uses self.info_from_entry internally.
"""
return [self.info_from_entry(entry) for entry in self.get_entries()]
# pylint: disable=no-self-use
def info_from_entry(self, entry):
"""Return an object with important info from the entry."""
return entry
# pylint: disable=no-self-use
def get_entries(self):
"""Return all the discovered entries."""
raise NotImplementedError()
class SSDPDiscoverable(BaseDiscoverable):
"""uPnP discoverable base class."""
def __init__(self, netdis):
"""Initialize SSDPDiscoverable."""
self.netdis = netdis
def get_info(self):
"""Get most important info, by default the description location."""
return list(set(
self.info_from_entry(entry) for entry in self.get_entries()))
def info_from_entry(self, entry):
"""Get most important info, by default the description location."""
return entry.values['location']
# Helper functions
# pylint: disable=invalid-name
def find_by_st(self, st):
"""Find entries by ST (the device identifier)."""
return self.netdis.ssdp.find_by_st(st)
def find_by_device_description(self, values):
"""Find entries based on values from their description."""
return self.netdis.ssdp.find_by_device_description(values)
class MDNSDiscoverable(BaseDiscoverable):
"""mDNS Discoverable base class."""
def __init__(self, netdis, typ):
"""Initialize MDNSDiscoverable."""
self.netdis = netdis
self.typ = typ
self.services = {}
netdis.mdns.register_service(self)
def reset(self):
"""Reset found services."""
self.services.clear()
def is_discovered(self):
"""Return True if any device has been discovered."""
return len(self.services) > 0
# pylint: disable=unused-argument
def remove_service(self, zconf, typ, name):
"""Callback when a service is removed."""
self.services.pop(name, None)
def add_service(self, zconf, typ, name):
"""Callback when a service is found."""
service = None
tries = 0
while service is None and tries < 3:
service = zconf.get_service_info(typ, name)
tries += 1
if service is not None:
self.services[name] = service
def get_entries(self):
"""Return all found services."""
return self.services.values()
def info_from_entry(self, entry):
"""Return most important info from mDNS entries."""
return (self.ip_from_host(entry.server), entry.port)
def ip_from_host(self, host):
"""Attempt to return the ip address from an mDNS host.
Return host if failed.
"""
ips = self.netdis.mdns.zeroconf.cache.entries_with_name(host.lower())
return repr(ips[0]) if ips else host
class GDMDiscoverable(BaseDiscoverable):
"""GDM discoverable base class."""
def __init__(self, netdis):
"""Initialize GDMDiscoverable."""
self.netdis = netdis
def get_info(self):
"""Get most important info, by default the description location."""
return [self.info_from_entry(entry) for entry in self.get_entries()]
def info_from_entry(self, entry):
"""Get most important info, by default the description location."""
return 'https://%s:%s/' % (entry.values['location'],
entry.values['port'])
def find_by_content_type(self, value):
"""Find entries based on values from their content_type."""
return self.netdis.gdm.find_by_content_type(value)
def find_by_data(self, values):
"""Find entries based on values from any returned field."""
return self.netdis.gdm.find_by_data(values)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,19 @@
"""Discover Belkin Wemo devices."""
from . import SSDPDiscoverable
class Discoverable(SSDPDiscoverable):
"""Add support for discovering Belkin WeMo platform devices."""
def info_from_entry(self, entry):
"""Return most important info from a uPnP entry."""
device = entry.description['device']
return (device['friendlyName'], device['modelName'],
entry.values['location'], device.get('macAddress', ''),
device['serialNumber'])
def get_entries(self):
"""Return all Belkin Wemo entries."""
return self.find_by_device_description(
{'manufacturer': 'Belkin International Inc.'})

22
deps/netdisco/discoverables/directv.py vendored Normal file
View File

@@ -0,0 +1,22 @@
"""Discover DirecTV Receivers."""
from netdisco.util import urlparse
from . import SSDPDiscoverable
class Discoverable(SSDPDiscoverable):
"""Add support for discovering DirecTV Receivers."""
def info_from_entry(self, entry):
"""Return the most important info from a uPnP entry."""
url = urlparse(entry.values['location'])
device = entry.description['device']
return url.hostname, device['serialNumber']
def get_entries(self):
"""Get all the DirecTV uPnP entries."""
return self.find_by_device_description({
"manufacturer": "DIRECTV",
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1"
})

View File

@@ -0,0 +1,11 @@
"""Discover devices that implement the Google Cast platform."""
from . import MDNSDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(MDNSDiscoverable):
"""Add support for discovering Google Cast platform devices."""
def __init__(self, nd):
"""Initialize the Cast discovery."""
super(Discoverable, self).__init__(nd, '_googlecast._tcp.local.')

View File

@@ -0,0 +1,20 @@
"""Discover Home Assistant servers."""
from . import MDNSDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(MDNSDiscoverable):
"""Add support for discovering Home Assistant instances."""
def __init__(self, nd):
super(Discoverable, self).__init__(nd, '_home-assistant._tcp.local.')
def info_from_entry(self, entry):
"""Returns most important info from mDNS entries."""
return (entry.properties.get(b'base_url').decode('utf-8'),
entry.properties.get(b'version').decode('utf-8'),
entry.properties.get(b'requires_api_password'))
def get_info(self):
"""Get details from Home Assistant instances."""
return [self.info_from_entry(entry) for entry in self.get_entries()]

21
deps/netdisco/discoverables/homekit.py vendored Normal file
View File

@@ -0,0 +1,21 @@
"""Discover myStrom devices."""
from . import MDNSDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(MDNSDiscoverable):
"""Add support for discovering myStrom switches."""
def __init__(self, nd):
super(Discoverable, self).__init__(nd, '_hap._tcp.local.')
def info_from_entry(self, entry):
"""Return the most important info from mDNS entries."""
info = {key.decode('utf-8'): value.decode('utf-8')
for key, value in entry.properties.items()}
info['host'] = 'http://{}'.format(self.ip_from_host(entry.server))
return info
def get_info(self):
"""Get details from myStrom devices."""
return [self.info_from_entry(entry) for entry in self.get_entries()]

20
deps/netdisco/discoverables/kodi.py vendored Normal file
View File

@@ -0,0 +1,20 @@
"""Discover Kodi servers."""
from . import MDNSDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(MDNSDiscoverable):
"""Add support for discovering Kodi."""
def __init__(self, nd):
"""Initialize the Kodi discovery."""
super(Discoverable, self).__init__(nd, '_http._tcp.local.')
def info_from_entry(self, entry):
"""Return most important info from mDNS entries."""
return (self.ip_from_host(entry.server), entry.port)
def get_info(self):
"""Get all the Kodi details."""
return [self.info_from_entry(entry) for entry in self.get_entries()
if entry.name.startswith('Kodi ')]

View File

@@ -0,0 +1,14 @@
"""Discover Logitech Media Server."""
from . import BaseDiscoverable
class Discoverable(BaseDiscoverable):
"""Add support for discovering Logitech Media Server."""
def __init__(self, netdis):
"""Initialize Logitech Media Server discovery."""
self.netdis = netdis
def get_entries(self):
"""Get all the Logitech Media Server details."""
return [entry['from'] for entry in self.netdis.lms.entries]

20
deps/netdisco/discoverables/mystrom.py vendored Normal file
View File

@@ -0,0 +1,20 @@
"""Discover myStrom devices."""
from . import MDNSDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(MDNSDiscoverable):
"""Add support for discovering myStrom switches."""
def __init__(self, nd):
super(Discoverable, self).__init__(nd, '_hap._tcp.local.')
def info_from_entry(self, entry):
"""Return the most important info from mDNS entries."""
return (entry.properties.get(b'md').decode('utf-8'),
'http://{}'.format(self.ip_from_host(entry.server)),
entry.properties.get(b'id').decode('utf-8'))
def get_info(self):
"""Get details from myStrom devices."""
return [self.info_from_entry(entry) for entry in self.get_entries()]

View File

@@ -0,0 +1,20 @@
"""Discover Netgear routers."""
from netdisco.util import urlparse
from . import SSDPDiscoverable
class Discoverable(SSDPDiscoverable):
"""Add support for discovering Netgear routers."""
def info_from_entry(self, entry):
"""Return the most important info from a uPnP entry."""
url = urlparse(entry.values['location'])
return (entry.description['device']['modelNumber'], url.hostname)
def get_entries(self):
"""Get all the Netgear uPnP entries."""
return self.find_by_device_description({
"manufacturer": "NETGEAR, Inc.",
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
})

View File

@@ -0,0 +1,17 @@
"""Discover Panasonic Viera TV devices."""
from netdisco.util import urlparse
from . import SSDPDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(SSDPDiscoverable):
"""Add support for discovering Viera TV devices."""
def info_from_entry(self, entry):
"""Return the most important info from a uPnP entry."""
parsed = urlparse(entry.values['location'])
return '{}:{}'.format(parsed.hostname, parsed.port)
def get_entries(self):
"""Get all the Viera TV device uPnP entries."""
return self.find_by_st("urn:panasonic-com:service:p00NetworkControl:1")

View File

@@ -0,0 +1,20 @@
"""Discover Philips Hue bridges."""
from . import SSDPDiscoverable
class Discoverable(SSDPDiscoverable):
"""Add support for discovering Philips Hue bridges."""
def info_from_entry(self, entry):
"""Return the most important info from a uPnP entry."""
desc = entry.description
return desc['device']['friendlyName'], desc['URLBase']
def get_entries(self):
"""Get all the Hue bridge uPnP entries."""
# Hub models for year 2012 and 2015
return self.find_by_device_description({
"manufacturer": "Royal Philips Electronics",
"modelNumber": ["929000226503", "BSB002"]
})

View File

@@ -0,0 +1,15 @@
"""Discover PlexMediaServer."""
from . import GDMDiscoverable
class Discoverable(GDMDiscoverable):
"""Add support for discovering Plex Media Server."""
def info_from_entry(self, entry):
"""Return most important info from a GDM entry."""
return (entry['data']['Name'],
'https://%s:%s' % (entry['from'][0], entry['data']['Port']))
def get_entries(self):
"""Return all PMS entries."""
return self.find_by_data({'Content-Type': 'plex/media-server'})

16
deps/netdisco/discoverables/roku.py vendored Normal file
View File

@@ -0,0 +1,16 @@
"""Discover Roku players."""
from netdisco.util import urlparse
from . import SSDPDiscoverable
class Discoverable(SSDPDiscoverable):
"""Add support for discovering Roku media players."""
def info_from_entry(self, entry):
"""Return the most important info from a uPnP entry."""
info = urlparse(entry.location)
return info.hostname, info.port
def get_entries(self):
"""Get all the Roku entries."""
return self.find_by_st("roku:ecp")

21
deps/netdisco/discoverables/sabnzbd.py vendored Normal file
View File

@@ -0,0 +1,21 @@
"""Discover SABnzbd servers."""
from . import MDNSDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(MDNSDiscoverable):
"""Add support for discovering SABnzbd."""
def __init__(self, nd):
"""Initialize the SABnzbd discovery."""
super(Discoverable, self).__init__(nd, '_http._tcp.local.')
def info_from_entry(self, entry):
"""Return most important info from mDNS entries."""
return (self.ip_from_host(entry.server), entry.port,
entry.properties.get('path', '/sabnzbd/'))
def get_info(self):
"""Get details of SABnzbd."""
return [self.info_from_entry(entry) for entry in self.get_entries()
if entry.name.startswith('SABnzbd on')]

16
deps/netdisco/discoverables/sonos.py vendored Normal file
View File

@@ -0,0 +1,16 @@
"""Discover Sonos devices."""
from netdisco.util import urlparse
from . import SSDPDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(SSDPDiscoverable):
"""Add support for discovering Sonos devices."""
def info_from_entry(self, entry):
"""Return the most important info from a uPnP entry."""
return urlparse(entry.values['location']).hostname
def get_entries(self):
"""Get all the Sonos device uPnP entries."""
return self.find_by_st("urn:schemas-upnp-org:device:ZonePlayer:1")

View File

@@ -0,0 +1,14 @@
"""Discover Tellstick devices."""
from . import BaseDiscoverable
class Discoverable(BaseDiscoverable):
"""Add support for discovering a Tellstick device."""
def __init__(self, netdis):
"""Initialize the Tellstick discovery."""
self._netdis = netdis
def get_entries(self):
"""Get all the Tellstick details."""
return self._netdis.tellstick.entries

19
deps/netdisco/discoverables/webos_tv.py vendored Normal file
View File

@@ -0,0 +1,19 @@
"""Discover LG WebOS TV devices."""
from netdisco.util import urlparse
from . import SSDPDiscoverable
# pylint: disable=too-few-public-methods
class Discoverable(SSDPDiscoverable):
"""Add support for discovering LG WebOS TV devices."""
def info_from_entry(self, entry):
"""Return the most important info from a uPnP entry."""
return urlparse(entry.values['location']).hostname
def get_entries(self):
"""Get all the LG WebOS TV device uPnP entries."""
return self.find_by_device_description({
"deviceType": "urn:dial-multiscreen-org:device:dial:1",
"friendlyName": "[LG] webOS TV"
})

126
deps/netdisco/discovery.py vendored Normal file
View File

@@ -0,0 +1,126 @@
"""Combine all the different protocols into a simple interface."""
from __future__ import print_function
import logging
import os
import importlib
from .ssdp import SSDP
from .mdns import MDNS
from .gdm import GDM
from .lms import LMS
from .tellstick import Tellstick
_LOGGER = logging.getLogger(__name__)
class NetworkDiscovery(object):
"""Scan the network for devices.
mDNS scans in a background thread.
SSDP scans in the foreground.
GDM scans in the foreground.
LMS scans in the foreground.
Tellstick scans in the foreground
start: is ready to scan
scan: scan the network
discover: parse scanned data
get_in
"""
# pylint: disable=too-many-instance-attributes
def __init__(self, limit_discovery=None):
"""Initialize the discovery."""
self.limit_discovery = limit_discovery
self.mdns = MDNS()
self.ssdp = SSDP()
self.gdm = GDM()
self.lms = LMS()
self.tellstick = Tellstick()
self.discoverables = {}
self._load_device_support()
self.is_discovering = False
def scan(self):
"""Start and tells scanners to scan."""
if not self.is_discovering:
self.mdns.start()
self.is_discovering = True
self.ssdp.scan()
self.gdm.scan()
self.lms.scan()
self.tellstick.scan()
def stop(self):
"""Turn discovery off."""
if not self.is_discovering:
return
self.mdns.stop()
self.is_discovering = False
def discover(self):
"""Return a list of discovered devices and services."""
self._check_enabled()
return [dis for dis, checker in self.discoverables.items()
if checker.is_discovered()]
def get_info(self, dis):
"""Get a list with the most important info about discovered type."""
return self.discoverables[dis].get_info()
def get_entries(self, dis):
"""Get a list with all info about a discovered type."""
return self.discoverables[dis].get_entries()
def _check_enabled(self):
"""Raise RuntimeError if discovery is disabled."""
if not self.is_discovering:
raise RuntimeError("NetworkDiscovery is disabled")
def _load_device_support(self):
"""Load the devices and services that can be discovered."""
self.discoverables = {}
discoverables_format = __name__.rsplit('.', 1)[0] + '.discoverables.{}'
for module_name in os.listdir(os.path.join(os.path.dirname(__file__),
'discoverables')):
if module_name[-3:] != '.py' or module_name == '__init__.py':
continue
module_name = module_name[:-3]
if self.limit_discovery is not None and \
module_name not in self.limit_discovery:
continue
module = importlib.import_module(
discoverables_format.format(module_name))
self.discoverables[module_name] = module.Discoverable(self)
def print_raw_data(self):
"""Helper method to show what is discovered in your network."""
from pprint import pprint
print("Zeroconf")
pprint(self.mdns.entries)
print("")
print("SSDP")
pprint(self.ssdp.entries)
print("")
print("GDM")
pprint(self.gdm.entries)
print("")
print("LMS")
pprint(self.lms.entries)
print("")
print("Tellstick")
pprint(self.tellstick.entries)

110
deps/netdisco/gdm.py vendored Normal file
View File

@@ -0,0 +1,110 @@
"""
Support for discovery using GDM (Good Day Mate), multicast protocol by Plex.
Inspired by
hippojay's plexGDM:
https://github.com/hippojay/script.plexbmc.helper/resources/lib/plexgdm.py
iBaa's PlexConnect: https://github.com/iBaa/PlexConnect/PlexAPI.py
"""
import threading
import socket
class GDM(object):
"""Base class to discover GDM services."""
def __init__(self):
self.entries = []
self.last_scan = None
self._lock = threading.RLock()
def scan(self):
"""Scan the network."""
with self._lock:
self.update()
def all(self):
"""Return all found entries.
Will scan for entries if not scanned recently.
"""
self.scan()
return list(self.entries)
def find_by_content_type(self, value):
"""Return a list of entries that match the content_type."""
self.scan()
return [entry for entry in self.entries
if value in entry['data']['Content_Type']]
def find_by_data(self, values):
"""Return a list of entries that match the search parameters."""
self.scan()
return [entry for entry in self.entries
if all(item in entry['data'].items()
for item in values.items())]
def update(self):
"""Scan for new GDM services.
Example of the dict list returned by this function:
[{'data': 'Content-Type: plex/media-server'
'Host: 53f4b5b6023d41182fe88a99b0e714ba.plex.direct'
'Name: myfirstplexserver'
'Port: 32400'
'Resource-Identifier: 646ab0aa8a01c543e94ba975f6fd6efadc36b7'
'Updated-At: 1444852697'
'Version: 0.9.12.13.1464-4ccd2ca'
'from': ('10.10.10.100', 32414)}]
"""
gdm_ip = '239.0.0.250' # multicast to PMS
gdm_port = 32414
gdm_msg = 'M-SEARCH * HTTP/1.0'.encode('ascii')
gdm_timeout = 1
self.entries = []
# setup socket for discovery -> multicast message
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(gdm_timeout)
# Set the time-to-live for messages for local network
sock.setsockopt(socket.IPPROTO_IP,
socket.IP_MULTICAST_TTL,
gdm_timeout)
try:
# Send data to the multicast group
sock.sendto(gdm_msg, (gdm_ip, gdm_port))
# Look for responses from all recipients
while True:
try:
data, server = sock.recvfrom(1024)
data = data.decode('utf-8')
if '200 OK' in data.splitlines()[0]:
data = {k: v.strip() for (k, v) in (
line.split(':') for line in
data.splitlines() if ':' in line)}
self.entries.append({'data': data,
'from': server})
except socket.timeout:
break
finally:
sock.close()
def main():
"""Test GDM discovery."""
# pylint: disable=invalid-name
from pprint import pprint
gdm = GDM()
pprint("Scanning GDM...")
gdm.update()
pprint(gdm.entries)
if __name__ == "__main__":
main()

70
deps/netdisco/lms.py vendored Normal file
View File

@@ -0,0 +1,70 @@
"""Squeezebox/Logitech Media server discovery."""
import socket
import threading
DISCOVERY_PORT = 3483
DEFAULT_DISCOVERY_TIMEOUT = 5
class LMS(object):
"""Base class to discover Logitech Media servers."""
def __init__(self):
"""Initialize the Logitech discovery."""
self.entries = []
self.last_scan = None
self._lock = threading.RLock()
def scan(self):
"""Scan the network."""
with self._lock:
self.update()
def all(self):
"""Scan and return all found entries."""
self.scan()
return list(self.entries)
def update(self):
"""Scan network for Logitech Media Servers."""
lms_ip = '<broadcast>'
lms_port = DISCOVERY_PORT
lms_msg = b"d................."
lms_timeout = DEFAULT_DISCOVERY_TIMEOUT
entries = []
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.settimeout(lms_timeout)
sock.bind(('', 0))
try:
sock.sendto(lms_msg, (lms_ip, lms_port))
while True:
try:
data, server = sock.recvfrom(1024)
if data.startswith(b'D'):
entries.append({'data': data,
'from': server})
except socket.timeout:
break
finally:
sock.close()
self.entries = entries
def main():
"""Test LMS discovery."""
from pprint import pprint
# pylint: disable=invalid-name
lms = LMS()
pprint("Scanning for Logitech Media Servers...")
lms.update()
pprint(lms.entries)
if __name__ == "__main__":
main()

40
deps/netdisco/mdns.py vendored Normal file
View File

@@ -0,0 +1,40 @@
"""Add support for discovering mDNS services."""
import zeroconf
class MDNS(object):
"""Base class to discover mDNS services."""
def __init__(self):
"""Initialize the discovery."""
self.zeroconf = None
self.services = []
self._browsers = []
def register_service(self, service):
"""Register a mDNS service."""
self.services.append(service)
def start(self):
"""Start discovery."""
self.zeroconf = zeroconf.Zeroconf()
for service in self.services:
self._browsers.append(
zeroconf.ServiceBrowser(self.zeroconf, service.typ, service))
def stop(self):
"""Stop discovering."""
while self._browsers:
self._browsers.pop().cancel()
for service in self.services:
service.reset()
self.zeroconf.close()
self.zeroconf = None
@property
def entries(self):
"""Return all entries in the cache."""
return self.zeroconf.cache.entries()

91
deps/netdisco/service.py vendored Normal file
View File

@@ -0,0 +1,91 @@
"""Provide service that scans the network in intervals."""
import logging
import threading
import time
from collections import defaultdict
from .discovery import NetworkDiscovery
DEFAULT_INTERVAL = 300 # seconds
_LOGGER = logging.getLogger(__name__)
class DiscoveryService(threading.Thread):
"""Service that will scan the network for devices each `interval` seconds.
Add listeners to the service to be notified of new services found.
"""
def __init__(self, interval=DEFAULT_INTERVAL, limit_discovery=None):
"""Initialize the discovery."""
super(DiscoveryService, self).__init__()
# Scanning interval
self.interval = interval
# Limit discovery to the following types
self.limit_discovery = limit_discovery
# Listeners for new services
self.listeners = []
# To track when we have to stop
self._stop = threading.Event()
# Tell Python not to wait till this thread exits
self.daemon = True
# The discovery object
self.discovery = None
# Dict to keep track of found services. We do not want to
# broadcast the same found service twice.
self._found = defaultdict(list)
def add_listener(self, listener):
"""Add a listener for new services."""
self.listeners.append(listener)
def stop(self):
"""Stop the service."""
self._stop.set()
def run(self):
"""Start the discovery service."""
self.discovery = NetworkDiscovery(self.limit_discovery)
while True:
self._scan()
seconds_since_scan = 0
while seconds_since_scan < self.interval:
if self._stop.is_set():
return
time.sleep(1)
seconds_since_scan += 1
def _scan(self):
"""Scan for new devices."""
_LOGGER.info("Scanning")
self.discovery.scan()
for disc in self.discovery.discover():
for service in self.discovery.get_info(disc):
self._service_found(disc, service)
self.discovery.stop()
def _service_found(self, disc, service):
"""Tell listeners a service was found."""
if service not in self._found[disc]:
self._found[disc].append(service)
for listener in self.listeners:
try:
listener(disc, service)
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Error calling listener")

278
deps/netdisco/ssdp.py vendored Normal file
View File

@@ -0,0 +1,278 @@
"""Module that implements SSDP protocol."""
import re
import select
import socket
import logging
from datetime import datetime, timedelta
import threading
import xml.etree.ElementTree as ElementTree
import requests
from netdisco.util import etree_to_dict, interface_addresses
DISCOVER_TIMEOUT = 5
SSDP_MX = 1
SSDP_TARGET = ("239.255.255.250", 1900)
RESPONSE_REGEX = re.compile(r'\n(.*)\: (.*)\r')
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=59)
# Devices and services
ST_ALL = "ssdp:all"
# Devices only, some devices will only respond to this query
ST_ROOTDEVICE = "upnp:rootdevice"
class SSDP(object):
"""Control the scanning of uPnP devices and services and caches output."""
def __init__(self):
"""Initialize the discovery."""
self.entries = []
self.last_scan = None
self._lock = threading.RLock()
def scan(self):
"""Scan the network."""
with self._lock:
self.update()
def all(self):
"""Return all found entries.
Will scan for entries if not scanned recently.
"""
with self._lock:
self.update()
return list(self.entries)
# pylint: disable=invalid-name
def find_by_st(self, st):
"""Return a list of entries that match the ST."""
with self._lock:
self.update()
return [entry for entry in self.entries
if entry.st == st]
def find_by_device_description(self, values):
"""Return a list of entries that match the description.
Pass in a dict with values to match against the device tag in the
description.
"""
with self._lock:
self.update()
return [entry for entry in self.entries
if entry.match_device_description(values)]
def update(self, force_update=False):
"""Scan for new uPnP devices and services."""
with self._lock:
if self.last_scan is None or force_update or \
datetime.now()-self.last_scan > MIN_TIME_BETWEEN_SCANS:
self.remove_expired()
# Wemo does not respond to a query for all devices+services
# but only to a query for just root devices.
self.entries.extend(
entry for entry in scan() + scan(ST_ROOTDEVICE)
if entry not in self.entries)
self.last_scan = datetime.now()
def remove_expired(self):
"""Filter out expired entries."""
with self._lock:
self.entries = [entry for entry in self.entries
if not entry.is_expired]
class UPNPEntry(object):
"""Found uPnP entry."""
DESCRIPTION_CACHE = {'_NO_LOCATION': {}}
def __init__(self, values):
"""Initialize the discovery."""
self.values = values
self.created = datetime.now()
if 'cache-control' in self.values:
cache_seconds = int(self.values['cache-control'].split('=')[1])
self.expires = self.created + timedelta(seconds=cache_seconds)
else:
self.expires = None
@property
def is_expired(self):
"""Return if the entry is expired or not."""
return self.expires is not None and datetime.now() > self.expires
# pylint: disable=invalid-name
@property
def st(self):
"""Return ST value."""
return self.values.get('st')
@property
def location(self):
"""Return Location value."""
return self.values.get('location')
@property
def description(self):
"""Return the description from the uPnP entry."""
url = self.values.get('location', '_NO_LOCATION')
if url not in UPNPEntry.DESCRIPTION_CACHE:
try:
xml = requests.get(url).text
tree = ElementTree.fromstring(xml)
UPNPEntry.DESCRIPTION_CACHE[url] = \
etree_to_dict(tree).get('root', {})
except requests.RequestException:
logging.getLogger(__name__).error(
"Error fetching description at %s", url)
UPNPEntry.DESCRIPTION_CACHE[url] = {}
except ElementTree.ParseError:
logging.getLogger(__name__).error(
"Found malformed XML at %s: %s", url, xml)
UPNPEntry.DESCRIPTION_CACHE[url] = {}
return UPNPEntry.DESCRIPTION_CACHE[url]
def match_device_description(self, values):
"""Fetch description and matches against it.
Values should only contain lowercase keys.
"""
device = self.description.get('device')
if device is None:
return False
return all(device.get(key) in val
if isinstance(val, list)
else val == device.get(key)
for key, val in values.items())
@classmethod
def from_response(cls, response):
"""Create a uPnP entry from a response."""
return UPNPEntry({key.lower(): item for key, item
in RESPONSE_REGEX.findall(response)})
def __eq__(self, other):
"""Return the comparison."""
return (self.__class__ == other.__class__ and
self.values == other.values)
def __repr__(self):
"""Return the entry."""
return "<UPNPEntry {} - {}>".format(
self.values.get('st', ''), self.values.get('location', ''))
# pylint: disable=invalid-name,too-many-locals
def scan(st=None, timeout=DISCOVER_TIMEOUT, max_entries=None):
"""Send a message over the network to discover uPnP devices.
Inspired by Crimsdings
https://github.com/crimsdings/ChromeCast/blob/master/cc_discovery.py
Protocol explanation:
https://embeddedinn.wordpress.com/tutorials/upnp-device-architecture/
"""
# pylint: disable=too-many-nested-blocks,too-many-branches
ssdp_st = st or ST_ALL
ssdp_request = "\r\n".join([
'M-SEARCH * HTTP/1.1',
'HOST: 239.255.255.250:1900',
'MAN: "ssdp:discover"',
'MX: {:d}'.format(SSDP_MX),
'ST: {}'.format(ssdp_st),
'', '']).encode('utf-8')
stop_wait = datetime.now() + timedelta(0, timeout)
sockets = []
for addr in interface_addresses():
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Set the time-to-live for messages for local network
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)
sock.bind((addr, 0))
sockets.append(sock)
except socket.error:
pass
entries = []
for sock in [s for s in sockets]:
try:
sock.sendto(ssdp_request, SSDP_TARGET)
sock.setblocking(False)
except socket.error:
sockets.remove(sock)
sock.close()
try:
while sockets:
time_diff = stop_wait - datetime.now()
seconds_left = time_diff.total_seconds()
if seconds_left <= 0:
break
ready = select.select(sockets, [], [], seconds_left)[0]
for sock in ready:
try:
response = sock.recv(1024).decode("utf-8")
except socket.error:
logging.getLogger(__name__).exception(
"Socket error while discovering SSDP devices")
sockets.remove(sock)
sock.close()
continue
entry = UPNPEntry.from_response(response)
if (st is None or entry.st == st) and entry not in entries:
entries.append(entry)
if max_entries and len(entries) == max_entries:
raise StopIteration
except StopIteration:
pass
finally:
for s in sockets:
s.close()
return entries
def main():
"""Test SSDP discovery."""
from pprint import pprint
pprint("Scanning SSDP..")
pprint(scan())
if __name__ == "__main__":
main()

68
deps/netdisco/tellstick.py vendored Normal file
View File

@@ -0,0 +1,68 @@
"""Tellstick device discovery."""
import socket
import threading
from datetime import timedelta
DISCOVERY_PORT = 30303
DISCOVERY_ADDRESS = '<broadcast>'
DISCOVERY_PAYLOAD = b"D"
DISCOVERY_TIMEOUT = timedelta(seconds=5)
class Tellstick(object):
"""Base class to discover Tellstick devices."""
def __init__(self):
"""Initialize the TEllstick discovery."""
self.entries = []
self._lock = threading.RLock()
def scan(self):
"""Scan the network."""
with self._lock:
self.update()
def all(self):
"""Scan and return all found entries."""
self.scan()
return self.entries
def update(self):
"""Scan network for Tellstick devices."""
entries = []
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.settimeout(DISCOVERY_TIMEOUT.seconds)
sock.sendto(DISCOVERY_PAYLOAD, (DISCOVERY_ADDRESS, DISCOVERY_PORT))
while True:
try:
data, (address, _) = sock.recvfrom(1024)
entry = data.decode("ascii").split(":")
# expecting product, mac, activation code, version
if len(entry) != 4:
continue
entry = (address,) + tuple(entry)
entries.append(entry)
except socket.timeout:
break
self.entries = entries
sock.close()
def main():
"""Test Tellstick discovery."""
from pprint import pprint
tellstick = Tellstick()
pprint("Scanning for Tellstick devices..")
tellstick.update()
pprint(tellstick.entries)
if __name__ == "__main__":
main()

51
deps/netdisco/util.py vendored Normal file
View File

@@ -0,0 +1,51 @@
"""Util functions used by Netdisco."""
from collections import defaultdict
# pylint: disable=unused-import, import-error, no-name-in-module
try:
# Py2
from urlparse import urlparse # noqa
except ImportError:
# Py3
from urllib.parse import urlparse # noqa
import netifaces
# Taken from http://stackoverflow.com/a/10077069
# pylint: disable=invalid-name
def etree_to_dict(t):
"""Convert an ETree object to a dict."""
# strip namespace
tag_name = t.tag[t.tag.find("}")+1:]
d = {tag_name: {} if t.attrib else None}
children = list(t)
if children:
dd = defaultdict(list)
for dc in map(etree_to_dict, children):
for k, v in dc.items():
dd[k].append(v)
d = {tag_name: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}}
if t.attrib:
d[tag_name].update(('@' + k, v) for k, v in t.attrib.items())
if t.text:
text = t.text.strip()
if children or t.attrib:
if text:
d[tag_name]['#text'] = text
else:
d[tag_name] = text
return d
def interface_addresses(family=netifaces.AF_INET):
"""Return local addresses of any associated network.
Gathering of addresses which are bound to a local interface that has
broadcast (and probably multicast) capability.
"""
# pylint: disable=no-member
return [addr['addr']
for i in netifaces.interfaces()
for addr in netifaces.ifaddresses(i).get(family) or []
if 'broadcast' in addr]