From 42202e038d291ed2e78886ab99c77e859a822f28 Mon Sep 17 00:00:00 2001 From: Ruben Undheim Date: Tue, 29 Jan 2019 14:58:16 +0000 Subject: New upstream version 2.3.0 --- .gitignore | 1 + .travis.yml | 2 ++ README.md | 2 ++ netdisco/daikin.py | 9 ++++---- netdisco/discoverables/__init__.py | 6 +++++- netdisco/discoverables/esphome.py | 9 ++++++++ netdisco/discoverables/openhome.py | 3 ++- netdisco/discoverables/wink.py | 5 ++++- netdisco/discovery.py | 3 ++- netdisco/gdm.py | 11 +++++----- netdisco/lms.py | 6 ++++-- netdisco/mdns.py | 6 ++++-- netdisco/service.py | 5 +++-- netdisco/smartglass.py | 5 ++++- netdisco/ssdp.py | 7 +++--- netdisco/tellstick.py | 7 +++--- netdisco/util.py | 14 ++++++++---- requirements.txt | 1 + setup.cfg | 16 +++++++++++--- setup.py | 44 ++++++++++++++++++++++++++++---------- tox.ini | 8 ++++++- 21 files changed, 125 insertions(+), 45 deletions(-) create mode 100644 netdisco/discoverables/esphome.py diff --git a/.gitignore b/.gitignore index edc491c..a496cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ lib64 pip-selfcheck.json .tox .pytest_cache/ +.mypy_cache/ diff --git a/.travis.yml b/.travis.yml index ac9af0a..80155d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ matrix: env: TOXENV=py34 - python: "3.4.2" env: TOXENV=lint + - python: "3.4.2" + env: TOXENV=typing - python: "3.5" env: TOXENV=py35 - python: "3.6" diff --git a/README.md b/README.md index 264c440..949a3ec 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ Current methods of scanning: It is the library that powers the device discovery within [Home Assistant](https://home-assistant.io/). +### We are no longer accepting PRs that implement custom discovery protocols. Only PRs that use mDNS or uPnP are supported. See [this issue](https://github.com/home-assistant/netdisco/issues/230) + ## Installation Netdisco is available on PyPi. Install using `pip3 install netdisco`. diff --git a/netdisco/daikin.py b/netdisco/daikin.py index a8e6e8f..014f1e2 100644 --- a/netdisco/daikin.py +++ b/netdisco/daikin.py @@ -2,6 +2,7 @@ import socket from datetime import timedelta +from typing import Dict, List # noqa: F401 from urllib.parse import unquote DISCOVERY_MSG = b"DAIKIN_UDP/common/basic_info" @@ -18,7 +19,7 @@ class Daikin: def __init__(self): """Initialize the Daikin discovery.""" - self.entries = [] + self.entries = [] # type: List[Dict[str, str]] def scan(self): """Scan the network.""" @@ -47,9 +48,9 @@ class Daikin: try: data, (address, _) = sock.recvfrom(1024) - # pylint: disable=consider-using-dict-comprehension - entry = dict([e.split('=') - for e in data.decode("UTF-8").split(',')]) + entry = {x[0]: x[1] for x in ( + e.split('=', 1) + for e in data.decode("UTF-8").split(','))} # expecting product, mac, activation code, version if 'ret' not in entry or entry['ret'] != 'OK': diff --git a/netdisco/discoverables/__init__.py b/netdisco/discoverables/__init__.py index dd39785..2cbf7be 100644 --- a/netdisco/discoverables/__init__.py +++ b/netdisco/discoverables/__init__.py @@ -1,6 +1,7 @@ """Provides helpful stuff for discoverables.""" # pylint: disable=abstract-method import ipaddress +from typing import Dict, TYPE_CHECKING # noqa: F401 from urllib.parse import urlparse from ..const import ( @@ -8,6 +9,9 @@ from ..const import ( ATTR_SERIAL, ATTR_MODEL_NUMBER, ATTR_HOSTNAME, ATTR_MAC_ADDRESS, ATTR_PROPERTIES, ATTR_MANUFACTURER, ATTR_UDN, ATTR_UPNP_DEVICE_TYPE) +if TYPE_CHECKING: + from zeroconf import ServiceInfo # noqa: F401 + class BaseDiscoverable: """Base class for discoverable services or device types.""" @@ -80,7 +84,7 @@ class MDNSDiscoverable(BaseDiscoverable): """Initialize MDNSDiscoverable.""" self.netdis = netdis self.typ = typ - self.services = {} + self.services = {} # type: Dict[str, ServiceInfo] netdis.mdns.register_service(self) diff --git a/netdisco/discoverables/esphome.py b/netdisco/discoverables/esphome.py new file mode 100644 index 0000000..faeac97 --- /dev/null +++ b/netdisco/discoverables/esphome.py @@ -0,0 +1,9 @@ +"""Discover ESPHome devices.""" +from . import MDNSDiscoverable + + +class Discoverable(MDNSDiscoverable): + """Add support for discovering ESPHome devices.""" + + def __init__(self, nd): + super().__init__(nd, '_esphomelib._tcp.local.') diff --git a/netdisco/discoverables/openhome.py b/netdisco/discoverables/openhome.py index 3cc4314..7adca6f 100644 --- a/netdisco/discoverables/openhome.py +++ b/netdisco/discoverables/openhome.py @@ -7,4 +7,5 @@ class Discoverable(SSDPDiscoverable): def get_entries(self): """Get all the Openhome compliant device uPnP entries.""" - return self.find_by_st("urn:av-openhome-org:service:Product:2") + return self.find_by_st("urn:av-openhome-org:service:Product:2") + \ + self.find_by_st("urn:av-openhome-org:service:Product:3") diff --git a/netdisco/discoverables/wink.py b/netdisco/discoverables/wink.py index 9e6833f..3678d60 100644 --- a/netdisco/discoverables/wink.py +++ b/netdisco/discoverables/wink.py @@ -1,5 +1,8 @@ """Discover Wink hub devices.""" +from typing import List # noqa: F401 + from . import SSDPDiscoverable +from ..ssdp import UPNPEntry # noqa: F401 class Discoverable(SSDPDiscoverable): @@ -7,7 +10,7 @@ class Discoverable(SSDPDiscoverable): def get_entries(self): """Return all Wink entries.""" - results = [] + results = [] # type: List[UPNPEntry] results.extend(self.find_by_st('urn:wink-com:device:hub2:2')) results.extend(self.find_by_st('urn:wink-com:device:hub:2')) results.extend(self.find_by_st('urn:wink-com:device:relay:2')) diff --git a/netdisco/discovery.py b/netdisco/discovery.py index 27bce19..39c4fda 100644 --- a/netdisco/discovery.py +++ b/netdisco/discovery.py @@ -123,7 +123,8 @@ class NetworkDiscovery: module = importlib.import_module( discoverables_format.format(module_name)) - self.discoverables[module_name] = module.Discoverable(self) + self.discoverables[module_name] = \ + getattr(module, 'Discoverable')(self) def print_raw_data(self): """Helper method to show what is discovered in your network.""" diff --git a/netdisco/gdm.py b/netdisco/gdm.py index ce81ff0..75428b2 100644 --- a/netdisco/gdm.py +++ b/netdisco/gdm.py @@ -8,13 +8,14 @@ Inspired by """ import socket import struct +from typing import Any, Dict, List # noqa: F401 class GDM: """Base class to discover GDM services.""" def __init__(self): - self.entries = [] + self.entries = [] # type: List[Dict[str, Any]] self.last_scan = None def scan(self): @@ -81,13 +82,13 @@ class GDM: # Look for responses from all recipients while True: try: - data, server = sock.recvfrom(1024) - data = data.decode('utf-8') + bdata, server = sock.recvfrom(1024) + data = bdata.decode('utf-8') if '200 OK' in data.splitlines()[0]: - data = {k: v.strip() for (k, v) in ( + ddata = {k: v.strip() for (k, v) in ( line.split(':') for line in data.splitlines() if ':' in line)} - self.entries.append({'data': data, + self.entries.append({'data': ddata, 'from': server}) except socket.timeout: break diff --git a/netdisco/lms.py b/netdisco/lms.py index 6026a86..71934c9 100644 --- a/netdisco/lms.py +++ b/netdisco/lms.py @@ -1,5 +1,6 @@ """Squeezebox/Logitech Media server discovery.""" import socket +from typing import Dict, List, Union # noqa: F401 from .const import ATTR_HOST, ATTR_PORT @@ -12,7 +13,7 @@ class LMS: def __init__(self): """Initialize the Logitech discovery.""" - self.entries = [] + self.entries = [] # type: List[Dict[str, Union[str, int]]] self.last_scan = None def scan(self): @@ -49,7 +50,8 @@ class LMS: # Where YY is length of port string (ie 4) # And XXXX is the web interface port (ie 9000) port = None - if data.startswith(b'JSON', 1): + # https://github.com/python/typeshed/pull/2696 + if data.startswith(b'JSON', 1): # type: ignore length = data[5:6][0] port = int(data[0-length:]) entries.append({ diff --git a/netdisco/mdns.py b/netdisco/mdns.py index 8826346..c2cf8b1 100644 --- a/netdisco/mdns.py +++ b/netdisco/mdns.py @@ -1,4 +1,6 @@ """Add support for discovering mDNS services.""" +from typing import List # noqa: F401 + import zeroconf @@ -8,8 +10,8 @@ class MDNS: def __init__(self): """Initialize the discovery.""" self.zeroconf = None - self.services = [] - self._browsers = [] + self.services = [] # type: List[zeroconf.ServiceInfo] + self._browsers = [] # type: List[zeroconf.ServiceBrowser] def register_service(self, service): """Register a mDNS service.""" diff --git a/netdisco/service.py b/netdisco/service.py index 3cfb6b1..27fc0f9 100644 --- a/netdisco/service.py +++ b/netdisco/service.py @@ -3,6 +3,7 @@ import logging import threading import time from collections import defaultdict +from typing import Any, Callable, Dict, List # noqa: F401 from .discovery import NetworkDiscovery @@ -25,7 +26,7 @@ class DiscoveryService(threading.Thread): self.interval = interval # Listeners for new services - self.listeners = [] + self.listeners = [] # type: List[Callable[[str, Any], None]] # To track when we have to stop self._stop = threading.Event() @@ -38,7 +39,7 @@ class DiscoveryService(threading.Thread): # Dict to keep track of found services. We do not want to # broadcast the same found service twice. - self._found = defaultdict(list) + self._found = defaultdict(list) # type: Dict[str, List] def add_listener(self, listener): """Add a listener for new services.""" diff --git a/netdisco/smartglass.py b/netdisco/smartglass.py index 4244a2b..0b534cf 100644 --- a/netdisco/smartglass.py +++ b/netdisco/smartglass.py @@ -3,6 +3,7 @@ import socket import struct import binascii from datetime import timedelta +from typing import Any, Dict, List, Optional, Tuple # noqa: F401 DISCOVERY_PORT = 5050 @@ -25,13 +26,15 @@ Android = 8 """ DISCOVERY_CLIENT_TYPE = 4 +_Response = Dict[str, Any] + class XboxSmartGlass: """Base class to discover Xbox SmartGlass devices.""" def __init__(self): """Initialize the Xbox SmartGlass discovery.""" - self.entries = [] + self.entries = [] # type: List[Tuple[str, Optional[_Response]]] self._discovery_payload = self.discovery_packet() @staticmethod diff --git a/netdisco/ssdp.py b/netdisco/ssdp.py index 55cb08b..7c4dd9c 100644 --- a/netdisco/ssdp.py +++ b/netdisco/ssdp.py @@ -4,6 +4,7 @@ import select import socket import logging from datetime import datetime, timedelta +from typing import Dict, List, Optional, Set # noqa: F401 from xml.etree import ElementTree import requests @@ -33,7 +34,7 @@ class SSDP: def __init__(self): """Initialize the discovery.""" - self.entries = [] + self.entries = [] # type: List[UPNPEntry] self.last_scan = None def scan(self): @@ -65,7 +66,7 @@ class SSDP: """ self.update() - seen = set() + seen = set() # type: Set[Optional[str]] results = [] # Make unique based on the location since we don't care about ST here @@ -100,7 +101,7 @@ class SSDP: class UPNPEntry: """Found uPnP entry.""" - DESCRIPTION_CACHE = {'_NO_LOCATION': {}} + DESCRIPTION_CACHE = {'_NO_LOCATION': {}} # type: Dict[str, Dict] def __init__(self, values): """Initialize the discovery.""" diff --git a/netdisco/tellstick.py b/netdisco/tellstick.py index 24cf243..f5bd7bd 100644 --- a/netdisco/tellstick.py +++ b/netdisco/tellstick.py @@ -2,6 +2,7 @@ import socket from datetime import timedelta import logging +from typing import List, Tuple # noqa: F401 DISCOVERY_PORT = 30303 @@ -15,7 +16,7 @@ class Tellstick: def __init__(self): """Initialize the Tellstick discovery.""" - self.entries = [] + self.entries = [] # type: List[Tuple[str]] def scan(self): """Scan the network.""" @@ -43,8 +44,8 @@ class Tellstick: # expecting product, mac, activation code, version if len(entry) != 4: continue - entry = (address,) + tuple(entry) - entries.append(entry) + entry.insert(0, address) + entries.append(tuple(entry)) except socket.timeout: break diff --git a/netdisco/util.py b/netdisco/util.py index 65475b4..445062d 100644 --- a/netdisco/util.py +++ b/netdisco/util.py @@ -1,5 +1,6 @@ """Util functions used by Netdisco.""" from collections import defaultdict +from typing import Any, Dict, List, Optional # noqa: F401 # Taken from http://stackoverflow.com/a/10077069 @@ -9,21 +10,26 @@ def etree_to_dict(t): # strip namespace tag_name = t.tag[t.tag.find("}")+1:] - d = {tag_name: {} if t.attrib else None} + d = { + tag_name: {} if t.attrib else None + } # type: Dict[str, Optional[Dict[str, Any]]] children = list(t) if children: - dd = defaultdict(list) + dd = defaultdict(list) # type: Dict[str, 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()}} + dt = d[tag_name] if t.attrib: - d[tag_name].update(('@' + k, v) for k, v in t.attrib.items()) + assert dt is not None + dt.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 + assert dt is not None + dt['#text'] = text else: d[tag_name] = text return d diff --git a/requirements.txt b/requirements.txt index 9ad458c..fbb8f3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ zeroconf>=0.21.0 requests>=2.0 +typing; python_version<'3.5' diff --git a/setup.cfg b/setup.cfg index e48ddf6..e443a40 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,16 @@ -[bdist_wheel] -universal = 1 - [tool:pytest] testpaths = tests norecursedirs = .git + +[mypy] +check_untyped_defs = true +# TODO disallow_untyped_calls = true +# TODO disallow_untyped_defs = true +follow_imports = silent +ignore_missing_imports = true +no_implicit_optional = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_return_any = true +warn_unused_configs = true +warn_unused_ignores = true diff --git a/setup.py b/setup.py index aaf08bb..7fe1e70 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,35 @@ +"""Setup file for netdisco.""" +import os from setuptools import setup, find_packages -setup(name='netdisco', - version='2.2.0', - description='Discover devices on your local network', - url='https://github.com/home-assistant/netdisco', - author='Paulus Schoutsen', - author_email='Paulus@PaulusSchoutsen.nl', - license='Apache License 2.0', - install_requires=['requests>=2.0', 'zeroconf>=0.21.0'], - python_requires='>=3', - packages=find_packages(exclude=['tests', 'tests.*']), - zip_safe=False) +here = os.path.abspath(os.path.dirname(__file__)) + +with open(os.path.join(here, 'README.md'), encoding='utf-8') as readme_file: + long_description = readme_file.read() + +setup( + name='netdisco', + version='2.3.0', + description='Discover devices on your local network', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/home-assistant/netdisco', + author='Paulus Schoutsen', + author_email='Paulus@PaulusSchoutsen.nl', + license='Apache License 2.0', + install_requires=['requests>=2.0', 'zeroconf>=0.21.0'], + python_requires='>=3', + packages=find_packages(exclude=['tests', 'tests.*']), + zip_safe=False, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX', + 'Programming Language :: Python :: 3', + 'Topic :: Utilities', + 'Topic :: Home Automation', + 'Topic :: System :: Networking', + ], +) diff --git a/tox.ini b/tox.ini index 03232c8..714a850 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34, py35, py36, py37, lint +envlist = py34, py35, py36, py37, lint, typing skip_missing_interpreters = True [testenv] @@ -18,3 +18,9 @@ deps = commands = flake8 netdisco tests setup.py example_service.py pylint netdisco tests + +[testenv:typing] +deps = + mypy>=0.650 +commands = + mypy netdisco tests setup.py example_service.py -- cgit v1.2.3