mirror of
https://github.com/CCOSTAN/Home-AssistantConfig.git
synced 2025-11-06 17:51:36 +00:00
Initial Configuration Push
This commit is contained in:
1
deps/netdisco/__init__.py
vendored
Normal file
1
deps/netdisco/__init__.py
vendored
Normal 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
31
deps/netdisco/__main__.py
vendored
Normal 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()
|
||||
BIN
deps/netdisco/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/__main__.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/__main__.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/const.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/const.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/discovery.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/discovery.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/gdm.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/gdm.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/lms.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/lms.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/mdns.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/mdns.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/service.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/service.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/ssdp.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/ssdp.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/tellstick.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/tellstick.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/__pycache__/util.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/__pycache__/util.cpython-34.pyc
vendored
Normal file
Binary file not shown.
15
deps/netdisco/const.py
vendored
Normal file
15
deps/netdisco/const.py
vendored
Normal 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
11
deps/netdisco/discoverables/DLNA.py
vendored
Normal 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
133
deps/netdisco/discoverables/__init__.py
vendored
Normal 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)
|
||||
BIN
deps/netdisco/discoverables/__pycache__/DLNA.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/DLNA.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/__init__.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/belkin_wemo.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/belkin_wemo.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/directv.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/directv.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/google_cast.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/google_cast.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/home_assistant.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/home_assistant.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/homekit.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/homekit.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/kodi.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/kodi.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/logitech_mediaserver.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/logitech_mediaserver.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/mystrom.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/mystrom.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/netgear_router.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/netgear_router.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/panasonic_viera.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/panasonic_viera.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/philips_hue.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/philips_hue.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/plex_mediaserver.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/plex_mediaserver.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/roku.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/roku.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/sabnzbd.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/sabnzbd.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/sonos.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/sonos.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/tellstick.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/tellstick.cpython-34.pyc
vendored
Normal file
Binary file not shown.
BIN
deps/netdisco/discoverables/__pycache__/webos_tv.cpython-34.pyc
vendored
Normal file
BIN
deps/netdisco/discoverables/__pycache__/webos_tv.cpython-34.pyc
vendored
Normal file
Binary file not shown.
19
deps/netdisco/discoverables/belkin_wemo.py
vendored
Normal file
19
deps/netdisco/discoverables/belkin_wemo.py
vendored
Normal 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
22
deps/netdisco/discoverables/directv.py
vendored
Normal 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"
|
||||
})
|
||||
11
deps/netdisco/discoverables/google_cast.py
vendored
Normal file
11
deps/netdisco/discoverables/google_cast.py
vendored
Normal 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.')
|
||||
20
deps/netdisco/discoverables/home_assistant.py
vendored
Normal file
20
deps/netdisco/discoverables/home_assistant.py
vendored
Normal 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
21
deps/netdisco/discoverables/homekit.py
vendored
Normal 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
20
deps/netdisco/discoverables/kodi.py
vendored
Normal 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 ')]
|
||||
14
deps/netdisco/discoverables/logitech_mediaserver.py
vendored
Normal file
14
deps/netdisco/discoverables/logitech_mediaserver.py
vendored
Normal 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
20
deps/netdisco/discoverables/mystrom.py
vendored
Normal 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()]
|
||||
20
deps/netdisco/discoverables/netgear_router.py
vendored
Normal file
20
deps/netdisco/discoverables/netgear_router.py
vendored
Normal 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"
|
||||
})
|
||||
17
deps/netdisco/discoverables/panasonic_viera.py
vendored
Normal file
17
deps/netdisco/discoverables/panasonic_viera.py
vendored
Normal 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")
|
||||
20
deps/netdisco/discoverables/philips_hue.py
vendored
Normal file
20
deps/netdisco/discoverables/philips_hue.py
vendored
Normal 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"]
|
||||
})
|
||||
15
deps/netdisco/discoverables/plex_mediaserver.py
vendored
Normal file
15
deps/netdisco/discoverables/plex_mediaserver.py
vendored
Normal 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
16
deps/netdisco/discoverables/roku.py
vendored
Normal 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
21
deps/netdisco/discoverables/sabnzbd.py
vendored
Normal 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
16
deps/netdisco/discoverables/sonos.py
vendored
Normal 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")
|
||||
14
deps/netdisco/discoverables/tellstick.py
vendored
Normal file
14
deps/netdisco/discoverables/tellstick.py
vendored
Normal 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
19
deps/netdisco/discoverables/webos_tv.py
vendored
Normal 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
126
deps/netdisco/discovery.py
vendored
Normal 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
110
deps/netdisco/gdm.py
vendored
Normal 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
70
deps/netdisco/lms.py
vendored
Normal 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
40
deps/netdisco/mdns.py
vendored
Normal 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
91
deps/netdisco/service.py
vendored
Normal 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
278
deps/netdisco/ssdp.py
vendored
Normal 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
68
deps/netdisco/tellstick.py
vendored
Normal 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
51
deps/netdisco/util.py
vendored
Normal 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]
|
||||
Reference in New Issue
Block a user