pax_global_header00006660000000000000000000000064137164301070014514gustar00rootroot0000000000000052 comment=3e9519cfd9ed3d00a416f840d3ef9247bd30a19e netdisco-2.8.2/000077500000000000000000000000001371643010700133355ustar00rootroot00000000000000netdisco-2.8.2/.github/000077500000000000000000000000001371643010700146755ustar00rootroot00000000000000netdisco-2.8.2/.github/release-drafter.yml000066400000000000000000000000541371643010700204640ustar00rootroot00000000000000template: | ## What's Changed $CHANGES netdisco-2.8.2/.gitignore000066400000000000000000000002051371643010700153220ustar00rootroot00000000000000__pycache__ **/*.pyc netdisco.egg-info/ dist build pyvenv.cfg bin lib lib64 pip-selfcheck.json .tox .pytest_cache/ .mypy_cache/ venv netdisco-2.8.2/.travis.yml000066400000000000000000000007071371643010700154520ustar00rootroot00000000000000sudo: false matrix: fast_finish: true include: - python: "3.4.2" 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" env: TOXENV=py36 - python: "3.7" env: TOXENV=py37 sudo: true dist: xenial cache: directories: - $HOME/.cache/pip install: pip install -U tox language: python script: tox netdisco-2.8.2/CLA.md000066400000000000000000000032661371643010700142650ustar00rootroot00000000000000# Contributor License Agreement ``` By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the Apache 2.0 license; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the Apache 2.0 license; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` ## Attribution The text of this license is available under the [Creative Commons Attribution-ShareAlike 3.0 Unported License](http://creativecommons.org/licenses/by-sa/3.0/). It is based on the Linux [Developer Certificate Of Origin](http://elinux.org/Developer_Certificate_Of_Origin), but is modified to explicitly use the Apache 2.0 license and not mention sign-off. ## Signing To sign this CLA you must first submit a pull request to a repository under the Home Assistant organization. ## Adoption This Contributor License Agreement (CLA) was first announced on January 21st, 2017 in [this][cla-blog] blog post and adopted January 28th, 2017. [cla-blog]: https://home-assistant.io/blog/2017/01/21/home-assistant-governance/ netdisco-2.8.2/CODE_OF_CONDUCT.md000066400000000000000000000065571371643010700161510ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [safety@home-assistant.io][email]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available [here][version]. ## Adoption This Code of Conduct was first adopted January 21st, 2017 and announced in [this][coc-blog] blog post. [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ [email]: mailto:safety@home-assistant.io [coc-blog]: https://home-assistant.io/blog/2017/01/21/home-assistant-governance/ netdisco-2.8.2/LICENSE.md000066400000000000000000000243601371643010700147460ustar00rootroot00000000000000Apache License ============== _Version 2.0, January 2004_ _<>_ ### Terms and Conditions for use, reproduction, and distribution #### 1. Definitions “License” shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. “Licensor” shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. “Legal Entity” shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, “control” means **(i)** the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the outstanding shares, or **(iii)** beneficial ownership of such entity. “You” (or “Your”) shall mean an individual or Legal Entity exercising permissions granted by this License. “Source” form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. “Object” form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. “Work” shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). “Derivative Works” shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. “Contribution” shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as “Not a Contribution.” “Contributor” shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. #### 2. Grant of Copyright License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. #### 3. Grant of Patent License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. #### 4. Redistribution You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: * **(a)** You must give any other recipients of the Work or Derivative Works a copy of this License; and * **(b)** You must cause any modified files to carry prominent notices stating that You changed the files; and * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. #### 5. Submission of Contributions Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. #### 6. Trademarks This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. #### 7. Disclaimer of Warranty Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. #### 8. Limitation of Liability In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. #### 9. Accepting Warranty or Additional Liability While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. _END OF TERMS AND CONDITIONS_ ### APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets `[]` replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same “printed page” as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. netdisco-2.8.2/MANIFEST.in000066400000000000000000000000611371643010700150700ustar00rootroot00000000000000include README.md include LICENSE.md graft tests netdisco-2.8.2/README.md000066400000000000000000000034011371643010700146120ustar00rootroot00000000000000## This library is deprecated. We will no longer release new versions, fix bugs or accept pull requests. If you are looking to make your Home Assistant integration discoverable, use [the zeroconf and SSDP manifest options](https://developers.home-assistant.io/docs/en/next/creating_integration_manifest.html#zeroconf). # NetDisco NetDisco is a Python 3 library to discover local devices and services. It allows to scan on demand or offer a service that will scan the network in the background in a set interval. Current methods of scanning: - mDNS (includes Chromecast, Homekit) - uPnP - Plex Media Server using Good Day Mate protocol - Logitech Media Server discovery protocol - Daikin discovery protocol - Web OS discovery protocol 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`. ## Example From command-line: ```bash python3 -m netdisco # To see all raw data: python3 -m netdisco dump ``` In your script: ```python from netdisco.discovery import NetworkDiscovery netdis = NetworkDiscovery() netdis.scan() for dev in netdis.discover(): print(dev, netdis.get_info(dev)) netdis.stop() ``` Will result in a list of discovered devices and their most important information: ``` DLNA ['http://192.168.1.1:8200/rootDesc.xml', 'http://192.168.1.150:32469/DeviceDescription.xml'] google_cast [('Living Room.local.', 8009)] philips_hue ['http://192.168.1.2:80/description.xml'] belkin_wemo ['http://192.168.1.10:49153/setup.xml'] ``` netdisco-2.8.2/example_service.py000066400000000000000000000011361371643010700170630ustar00rootroot00000000000000""" Example use of DiscoveryService. Will scan every 10 seconds and print out new found entries. Will quit after 2 minutes. """ import logging from datetime import datetime import time from netdisco.service import DiscoveryService logging.basicConfig(level=logging.INFO) # Scan every 10 seconds nd = DiscoveryService(10) def new_service_listener(discoverable, service): """ Print out a new service found message. """ print("{} - Found new service: {} {}".format( datetime.now(), discoverable, service)) nd.add_listener(new_service_listener) nd.start() time.sleep(120) nd.stop() netdisco-2.8.2/netdisco/000077500000000000000000000000001371643010700151455ustar00rootroot00000000000000netdisco-2.8.2/netdisco/__init__.py000066400000000000000000000001171371643010700172550ustar00rootroot00000000000000"""Module to scan the network using uPnP and mDNS for devices and services.""" netdisco-2.8.2/netdisco/__main__.py000066400000000000000000000014011371643010700172330ustar00rootroot00000000000000"""Command line tool to print discocvered devices or dump raw data.""" from pprint import pprint import sys from netdisco.discovery import NetworkDiscovery def main(): """Handle command line execution.""" netdisco = NetworkDiscovery() netdisco.scan() print("Discovered devices:") count = 0 for dev in netdisco.discover(): count += 1 print('{}:'.format(dev)) pprint(netdisco.get_info(dev)) print() print("Discovered {} devices".format(count)) # Pass in command line argument dump to get the raw data if sys.argv[-1] == 'dump': print() print() print("Raw Data") print() netdisco.print_raw_data() netdisco.stop() if __name__ == '__main__': main() netdisco-2.8.2/netdisco/const.py000066400000000000000000000023131371643010700166440ustar00rootroot00000000000000"""Constants of services that can be discovered.""" BELKIN_WEMO = "belkin_wemo" DLNA_DMS = "DLNA_DMS" DLNA_DMR = "DLNA_DMR" GOOGLE_CAST = "google_cast" PHILIPS_HUE = "philips_hue" PMS = 'plex_mediaserver' LMS = 'logitech_mediaserver' ASUS_ROUTER = "asus_router" HUAWEI_ROUTER = "huawei_router" NETGEAR_ROUTER = "netgear_router" SONOS = "sonos" PANASONIC_VIERA = "panasonic_viera" SABNZBD = 'sabnzbd' KODI = 'kodi' HOME_ASSISTANT = "home_assistant" MYSTROM = 'mystrom' HASS_IOS = "hass_ios" BOSE_SOUNDTOUCH = 'bose_soundtouch' SAMSUNG_TV = "samsung_tv" FRONTIER_SILICON = "frontier_silicon" APPLE_TV = "apple_tv" HARMONY = "harmony" BLUESOUND = "bluesound" ZIGGO_MEDIABOX_XL = "ziggo_mediabox_xl" DECONZ = "deconz" TIVO_DVR = "tivo_dvr" FREEBOX = "freebox" XBOX_SMARTGLASS = "xbox_smartglass" ATTR_NAME = 'name' ATTR_HOST = 'host' ATTR_PORT = 'port' ATTR_HOSTNAME = 'hostname' ATTR_URLBASE = 'urlbase' ATTR_DEVICE_TYPE = 'device_type' ATTR_MODEL_NAME = 'model_name' ATTR_MODEL_NUMBER = 'model_number' ATTR_MANUFACTURER = 'manufacturer' ATTR_UDN = 'udn' ATTR_PROPERTIES = 'properties' ATTR_SSDP_DESCRIPTION = 'ssdp_description' ATTR_UPNP_DEVICE_TYPE = 'upnp_device_type' ATTR_SERIAL = 'serial' ATTR_MAC_ADDRESS = 'mac_address' netdisco-2.8.2/netdisco/daikin.py000066400000000000000000000055731371643010700167700ustar00rootroot00000000000000"""Daikin device discovery.""" 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" UDP_SRC_PORT = 30000 UDP_DST_PORT = 30050 DISCOVERY_ADDRESS = '' DISCOVERY_TIMEOUT = timedelta(seconds=2) class Daikin: """Base class to discover Daikin devices.""" def __init__(self): """Initialize the Daikin discovery.""" self.entries = [] # type: List[Dict[str, str]] def scan(self): """Scan the network.""" self.update() def all(self): """Scan and return all found entries.""" self.scan() return self.entries def update(self): """Scan network for Daikin 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.bind(("", UDP_SRC_PORT)) try: sock.sendto(DISCOVERY_MSG, (DISCOVERY_ADDRESS, UDP_DST_PORT)) while True: try: data, (address, _) = sock.recvfrom(1024) 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': # non-OK return on response continue if 'mac' not in entry: # no mac found for device" continue if 'type' not in entry or entry['type'] != 'aircon': # no mac found for device" continue if 'name' in entry: entry['name'] = unquote(entry['name']) # in case the device was not configured to have an id # then use the mac address if 'id' in entry and entry['id'] == '': entry['id'] = entry['mac'] entries.append({ 'id': entry['id'], 'name': entry['name'], 'ip': address, 'mac': entry['mac'], 'ver': entry['ver'], }) except socket.timeout: break finally: sock.close() self.entries = entries def main(): """Test Daikin discovery.""" from pprint import pprint daikin = Daikin() pprint("Scanning for Daikin devices..") daikin.update() pprint(daikin.entries) if __name__ == "__main__": main() netdisco-2.8.2/netdisco/discoverables/000077500000000000000000000000001371643010700177725ustar00rootroot00000000000000netdisco-2.8.2/netdisco/discoverables/__init__.py000066400000000000000000000117161371643010700221110ustar00rootroot00000000000000"""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 ( ATTR_NAME, ATTR_MODEL_NAME, ATTR_HOST, ATTR_PORT, ATTR_SSDP_DESCRIPTION, 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.""" 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 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 info_from_entry(self, entry): """Get most important info.""" url = urlparse(entry.location) info = { ATTR_HOST: url.hostname, ATTR_PORT: url.port, ATTR_SSDP_DESCRIPTION: entry.location } device = entry.description.get('device') if device: info[ATTR_NAME] = device.get('friendlyName') info[ATTR_MODEL_NAME] = device.get('modelName') info[ATTR_MODEL_NUMBER] = device.get('modelNumber') info[ATTR_SERIAL] = device.get('serialNumber') info[ATTR_MANUFACTURER] = device.get('manufacturer') info[ATTR_UDN] = device.get('UDN') info[ATTR_UPNP_DEVICE_TYPE] = device.get('deviceType') return info # 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 = {} # type: Dict[str, ServiceInfo] netdis.mdns.register_service(self) def reset(self): """Reset found services.""" self.services.clear() # pylint: disable=unused-argument def remove_service(self, zconf, typ, name): """Callback when a service is removed.""" self.services.pop(name, None) def update_service(self, zconf, typ, name): """Callback when a service is updated.""" pass 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.""" properties = {} for key, value in entry.properties.items(): if isinstance(value, bytes): value = value.decode('utf-8') properties[key.decode('utf-8')] = value info = { ATTR_HOST: str(ipaddress.ip_address(entry.addresses[0])), ATTR_PORT: entry.port, ATTR_HOSTNAME: entry.server, ATTR_PROPERTIES: properties, } if "mac" in properties: info[ATTR_MAC_ADDRESS] = properties["mac"] return info def find_by_device_name(self, name): """Find entries based on the beginning of their entry names.""" return [entry for entry in self.services.values() if entry.name.startswith(name)] class GDMDiscoverable(BaseDiscoverable): """GDM discoverable base class.""" def __init__(self, netdis): """Initialize GDMDiscoverable.""" self.netdis = netdis 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) netdisco-2.8.2/netdisco/discoverables/apple_tv.py000066400000000000000000000010431371643010700221540ustar00rootroot00000000000000"""Discover Apple TV media players.""" from . import MDNSDiscoverable from ..const import ATTR_NAME, ATTR_PROPERTIES class Discoverable(MDNSDiscoverable): """Add support for Apple TV devices.""" def __init__(self, nd): super(Discoverable, self).__init__(nd, '_appletv-v2._tcp.local.') def info_from_entry(self, entry): """Returns most important info from mDNS entries.""" info = super().info_from_entry(entry) info[ATTR_NAME] = info[ATTR_PROPERTIES]['Name'].replace('\xa0', ' ') return info netdisco-2.8.2/netdisco/discoverables/arduino.py000066400000000000000000000004021371643010700220010ustar00rootroot00000000000000"""Discover Arduino devices.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Arduino devices.""" def __init__(self, nd): super(Discoverable, self).__init__(nd, '_arduino._tcp.local.') netdisco-2.8.2/netdisco/discoverables/asus_router.py000066400000000000000000000006421371643010700227210ustar00rootroot00000000000000"""Discover ASUS routers.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering ASUS routers.""" def get_entries(self): """Get all the ASUS uPnP entries.""" return self.find_by_device_description({ "manufacturer": "ASUSTeK Computer Inc.", "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" }) netdisco-2.8.2/netdisco/discoverables/axis.py000066400000000000000000000023161371643010700213120ustar00rootroot00000000000000"""Discover Axis devices.""" from . import MDNSDiscoverable from ..const import ( ATTR_HOST, ATTR_PORT, ATTR_HOSTNAME, ATTR_PROPERTIES) class Discoverable(MDNSDiscoverable): """Add support for discovering Axis devices.""" def info_from_entry(self, entry): """Return most important info from mDNS entries.""" properties = {} for key, value in entry.properties.items(): if isinstance(value, bytes): value = value.decode('utf-8') properties[key.decode('utf-8')] = value return { ATTR_HOST: self.ip_from_host(entry.server), ATTR_PORT: entry.port, ATTR_HOSTNAME: entry.server, ATTR_PROPERTIES: properties, } def __init__(self, nd): """Initialize the Axis discovery.""" super(Discoverable, self).__init__(nd, '_axis-video._tcp.local.') 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()) try: return repr(ips[0]) if ips else host except TypeError: return host netdisco-2.8.2/netdisco/discoverables/belkin_wemo.py000066400000000000000000000012311371643010700226340ustar00rootroot00000000000000"""Discover Belkin Wemo devices.""" from . import SSDPDiscoverable from ..const import ATTR_MAC_ADDRESS 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.""" info = super().info_from_entry(entry) device = entry.description['device'] info[ATTR_MAC_ADDRESS] = device.get('macAddress', '') return info def get_entries(self): """Return all Belkin Wemo entries.""" return self.find_by_device_description( {'manufacturer': 'Belkin International Inc.'}) netdisco-2.8.2/netdisco/discoverables/bluesound.py000066400000000000000000000005211371643010700223420ustar00rootroot00000000000000"""Discover devices that implement the Bluesound platform.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Bluesound service.""" def __init__(self, nd): """Initialize the Bluesound discovery.""" super(Discoverable, self).__init__(nd, '_musc._tcp.local.') netdisco-2.8.2/netdisco/discoverables/bose_soundtouch.py000066400000000000000000000005151371643010700235500ustar00rootroot00000000000000"""Discover Bose SoundTouch devices.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Bose SoundTouch devices.""" def __init__(self, nd): """Initialize the Bose SoundTouch discovery.""" super(Discoverable, self).__init__(nd, '_soundtouch._tcp.local.') netdisco-2.8.2/netdisco/discoverables/cambridgeaudio.py000066400000000000000000000007311371643010700233040ustar00rootroot00000000000000""" Discover Cambridge Audio StreamMagic devices. """ from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Cambridge Audio StreamMagic devices.""" def get_entries(self): """Get all Cambridge Audio MediaRenderer uPnP entries.""" return self.find_by_device_description({ "manufacturer": "Cambridge Audio", "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1" }) netdisco-2.8.2/netdisco/discoverables/canon_printer.py000066400000000000000000000006271371643010700232120ustar00rootroot00000000000000"""Discover Canon Printers""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Support for the discovery of Canon Printers""" def get_entries(self): """Get all the Canon Printer uPnP entries.""" return self.find_by_device_description({ "manufacturer": "CANON INC.", "deviceType": "urn:schemas-cipa-jp:device:DPSPrinter:1" }) netdisco-2.8.2/netdisco/discoverables/daikin.py000066400000000000000000000005741371643010700216110ustar00rootroot00000000000000"""Discover Daikin devices.""" from . import BaseDiscoverable class Discoverable(BaseDiscoverable): """Add support for discovering a Daikin device.""" def __init__(self, netdis): """Initialize the Daikin discovery.""" self._netdis = netdis def get_entries(self): """Get all the Daikin details.""" return self._netdis.daikin.entries netdisco-2.8.2/netdisco/discoverables/deconz.py000066400000000000000000000007131371643010700216270ustar00rootroot00000000000000"""Discover deCONZ gateways.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering deCONZ Wireless Light Control gateways.""" def get_entries(self): """Get all the deCONZ uPnP entries.""" return self.find_by_device_description({ "manufacturerURL": "http://www.dresden-elektronik.de", "modelDescription": "dresden elektronik Wireless Light Control" }) netdisco-2.8.2/netdisco/discoverables/denonavr.py000066400000000000000000000013561371643010700221650ustar00rootroot00000000000000"""Discover Denon AVR devices.""" from urllib.parse import urlparse from . import SSDPDiscoverable from ..const import ATTR_HOST class Discoverable(SSDPDiscoverable): """Add support for discovering Denon AVR devices.""" def get_entries(self): """Get all Denon AVR uPnP entries.""" return self.find_by_device_description({ "manufacturer": "Denon", "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1" }) def info_from_entry(self, entry): """Get most important info, which is name, model and host.""" info = super().info_from_entry(entry) info[ATTR_HOST] = urlparse( entry.description['device']['presentationURL']).hostname return info netdisco-2.8.2/netdisco/discoverables/digitalstrom.py000066400000000000000000000005761371643010700230560ustar00rootroot00000000000000"""Discover digitalSTROM server IP (dss-ip) devices.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering digitalSTROM server IP (dss-ip) devices.""" def __init__(self, nd): super(Discoverable, self).__init__(nd, '_http._tcp.local.') def get_entries(self): return self.find_by_device_name('dss-ip') netdisco-2.8.2/netdisco/discoverables/directv.py000066400000000000000000000006271371643010700220110ustar00rootroot00000000000000"""Discover DirecTV Receivers.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering DirecTV Receivers.""" 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" }) netdisco-2.8.2/netdisco/discoverables/dlna_dmr.py000066400000000000000000000007451371643010700221320ustar00rootroot00000000000000"""Discover DLNA services.""" from . import SSDPDiscoverable 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:MediaRenderer:1") + \ self.find_by_st("urn:schemas-upnp-org:device:MediaRenderer:2") + \ self.find_by_st("urn:schemas-upnp-org:device:MediaRenderer:3") netdisco-2.8.2/netdisco/discoverables/dlna_dms.py000066400000000000000000000010361371643010700221250ustar00rootroot00000000000000"""Discover DLNA services.""" from . import SSDPDiscoverable 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") + \ self.find_by_st("urn:schemas-upnp-org:device:MediaServer:2") + \ self.find_by_st("urn:schemas-upnp-org:device:MediaServer:3") + \ self.find_by_st("urn:schemas-upnp-org:device:MediaServer:4") netdisco-2.8.2/netdisco/discoverables/enigma2.py000066400000000000000000000004611371643010700216670ustar00rootroot00000000000000"""Discover Enigma2 servers.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Enigma2 boxes.""" def __init__(self, nd): """Initialize the Enigma2 discovery.""" super(Discoverable, self).__init__(nd, '_e2stream._tcp.local.') netdisco-2.8.2/netdisco/discoverables/esphome.py000066400000000000000000000003631371643010700220060ustar00rootroot00000000000000"""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.') netdisco-2.8.2/netdisco/discoverables/freebox.py000066400000000000000000000004621371643010700220000ustar00rootroot00000000000000"""Discover Freebox routers.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Freebox routers.""" def __init__(self, nd): """Initialize the Freebox discovery.""" super(Discoverable, self).__init__(nd, '_fbx-api._tcp.local.') netdisco-2.8.2/netdisco/discoverables/fritzbox.py000066400000000000000000000004601371643010700222130ustar00rootroot00000000000000"""Discover AVM FRITZ devices.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering AVM FRITZ devices.""" def get_entries(self): """Get all AVM FRITZ entries.""" return self.find_by_st("urn:schemas-upnp-org:device:fritzbox:1") netdisco-2.8.2/netdisco/discoverables/frontier_silicon.py000066400000000000000000000006671371643010700237250ustar00rootroot00000000000000"""Discover frontier silicon devices.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering frontier silicon devices.""" def get_entries(self): """Get all the frontier silicon uPnP entries.""" return [entry for entry in self.netdis.ssdp.all() if entry.st and 'fsapi' in entry.st and 'urn:schemas-frontier-silicon-com' in entry.st] netdisco-2.8.2/netdisco/discoverables/google_cast.py000066400000000000000000000005371371643010700226370ustar00rootroot00000000000000"""Discover devices that implement the Google Cast platform.""" from . import MDNSDiscoverable 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.') netdisco-2.8.2/netdisco/discoverables/harmony.py000066400000000000000000000006241371643010700220230ustar00rootroot00000000000000"""Discover Harmony Hub remotes.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Harmony Hub remotes""" def get_entries(self): """Get all the Harmony uPnP entries.""" return self.find_by_device_description({ "manufacturer": "Logitech", "deviceType": "urn:myharmony-com:device:harmony:1" }) netdisco-2.8.2/netdisco/discoverables/hass_ios.py000066400000000000000000000004251371643010700221550ustar00rootroot00000000000000"""Discover Home Assistant iOS app.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering the Home Assistant iOS app.""" def __init__(self, nd): super(Discoverable, self).__init__(nd, '_hass-ios._tcp.local.') netdisco-2.8.2/netdisco/discoverables/hass_mobile_app.py000066400000000000000000000004511371643010700234710ustar00rootroot00000000000000"""Discover Home Assistant servers.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering mobile apps that support Home Assistant.""" def __init__(self, nd): super(Discoverable, self).__init__(nd, '_hass-mobile-app._tcp.local.') netdisco-2.8.2/netdisco/discoverables/heos.py000066400000000000000000000004501371643010700213010ustar00rootroot00000000000000"""Discover Heos devices.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering DLNA services.""" def get_entries(self): """Get all the HEOS devices.""" return self.find_by_st("urn:schemas-denon-com:device:ACT-Denon:1") netdisco-2.8.2/netdisco/discoverables/hikvision.py000066400000000000000000000006111371643010700223450ustar00rootroot00000000000000"""Discover Hikvision cameras.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Hikvision cameras.""" def __init__(self, nd): """Initialize Hikvision camera discovery.""" super(Discoverable, self).__init__(nd, '_http._tcp.local.') def get_entries(self): return self.find_by_device_name('HIKVISION') netdisco-2.8.2/netdisco/discoverables/home_assistant.py000066400000000000000000000004311371643010700233630ustar00rootroot00000000000000"""Discover Home Assistant servers.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Home Assistant instances.""" def __init__(self, nd): super(Discoverable, self).__init__(nd, '_home-assistant._tcp.local.') netdisco-2.8.2/netdisco/discoverables/homekit.py000066400000000000000000000010061371643010700220010ustar00rootroot00000000000000"""Discover Homekit devices.""" from . import MDNSDiscoverable from ..const import ATTR_NAME class Discoverable(MDNSDiscoverable): """Add support for discovering HomeKit devices.""" def __init__(self, nd): super(Discoverable, self).__init__(nd, '_hap._tcp.local.') def info_from_entry(self, entry): info = super(Discoverable, self).info_from_entry(entry) name = entry.name name = name.replace('._hap._tcp.local.', '') info[ATTR_NAME] = name return info netdisco-2.8.2/netdisco/discoverables/hp_printer.py000066400000000000000000000005661371643010700225250ustar00rootroot00000000000000"""Discover HP Printers""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Support for the discovery of HP Printers""" def __init__(self, nd): """Initialize the HP Printer discovery""" super(Discoverable, self).__init__(nd, '_printer._tcp.local.') def get_entries(self): return self.find_by_device_name('HP ') netdisco-2.8.2/netdisco/discoverables/huawei_router.py000066400000000000000000000006601371643010700232300ustar00rootroot00000000000000"""Discover Huawei routers.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Huawei routers.""" def get_entries(self): """Get all the Huawei uPnP entries.""" return self.find_by_device_description({ "manufacturer": "Huawei Technologies Co., Ltd.", "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" }) netdisco-2.8.2/netdisco/discoverables/igd.py000066400000000000000000000007051371643010700211110ustar00rootroot00000000000000"""Discover IGD services.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering IGD services.""" def get_entries(self): """Get all the IGD service uPnP entries.""" return \ self.find_by_st( "urn:schemas-upnp-org:device:InternetGatewayDevice:1") + \ self.find_by_st( "urn:schemas-upnp-org:device:InternetGatewayDevice:2") netdisco-2.8.2/netdisco/discoverables/ikea_tradfri.py000066400000000000000000000005221371643010700227670ustar00rootroot00000000000000"""Discover devices that implement the Ikea Tradfri platform.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Ikea Tradfri devices.""" def __init__(self, nd): """Initialize the Cast discovery.""" super(Discoverable, self).__init__(nd, '_coap._udp.local.') netdisco-2.8.2/netdisco/discoverables/kodi.py000066400000000000000000000005531371643010700212750ustar00rootroot00000000000000"""Discover Kodi servers.""" from . import MDNSDiscoverable 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 get_entries(self): return self.find_by_device_name('Kodi ') netdisco-2.8.2/netdisco/discoverables/konnected.py000066400000000000000000000005111371643010700223130ustar00rootroot00000000000000"""Discover Konnected Security devices.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Konnected Security devices.""" def get_entries(self): """Return all Konnected entries.""" return self.find_by_st('urn:schemas-konnected-io:device:Security:1') netdisco-2.8.2/netdisco/discoverables/lg_smart_device.py000066400000000000000000000004651371643010700235000ustar00rootroot00000000000000"""Discover LG smart devices.""" from . import MDNSDiscoverable # pylint: disable=too-few-public-methods class Discoverable(MDNSDiscoverable): """Add support for discovering LG smart devices.""" def __init__(self, nd): super(Discoverable, self).__init__(nd, '_lg-smart-device._tcp.local.') netdisco-2.8.2/netdisco/discoverables/logitech_mediaserver.py000066400000000000000000000006361371643010700245350ustar00rootroot00000000000000"""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 self.netdis.lms.entries netdisco-2.8.2/netdisco/discoverables/lutron.py000066400000000000000000000006211371643010700216660ustar00rootroot00000000000000"""Discover Lutron Caseta Smart Bridge and Smart Bridge Pro devices.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Lutron Caseta Smart Bridge and Smart Bridge Pro devices.""" def __init__(self, nd): """Initialize the Lutron Smart Bridge discovery.""" super(Discoverable, self).__init__(nd, '_lutron._tcp.local.') netdisco-2.8.2/netdisco/discoverables/nanoleaf_aurora.py000066400000000000000000000004261371643010700235020ustar00rootroot00000000000000"""Discover Nanoleaf Aurora devices.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Nanoleaf Aurora devices.""" def __init__(self, nd): super(Discoverable, self).__init__(nd, '_nanoleafapi._tcp.local.') netdisco-2.8.2/netdisco/discoverables/netgear_router.py000066400000000000000000000006431371643010700233740ustar00rootroot00000000000000"""Discover Netgear routers.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Netgear routers.""" 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" }) netdisco-2.8.2/netdisco/discoverables/octoprint.py000066400000000000000000000005401371643010700223640ustar00rootroot00000000000000"""Discover OctoPrint Servers.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering OctoPrint servers.""" def get_entries(self): """Get all the OctoPrint uPnP entries.""" return self.find_by_device_description({ "manufacturer": "The OctoPrint Project" }) netdisco-2.8.2/netdisco/discoverables/openhome.py000066400000000000000000000007421371643010700221610ustar00rootroot00000000000000"""Discover Openhome devices.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Openhome compliant devices.""" def get_entries(self): """Get all the Openhome compliant device uPnP entries.""" return self.find_by_st("urn:av-openhome-org:service:Product:1") + \ self.find_by_st("urn:av-openhome-org:service:Product:2") + \ self.find_by_st("urn:av-openhome-org:service:Product:3") netdisco-2.8.2/netdisco/discoverables/panasonic_viera.py000066400000000000000000000005161371643010700235070ustar00rootroot00000000000000"""Discover Panasonic Viera TV devices.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Viera TV devices.""" def get_entries(self): """Get all the Viera TV device uPnP entries.""" return self.find_by_st("urn:panasonic-com:service:p00NetworkControl:1") netdisco-2.8.2/netdisco/discoverables/philips_hue.py000066400000000000000000000007311371643010700226560ustar00rootroot00000000000000"""Discover Philips Hue bridges.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Philips Hue bridges.""" def get_entries(self): """Get all the Hue bridge uPnP entries.""" return self.find_by_device_description({ "manufacturer": "Royal Philips Electronics", "manufacturerURL": "http://www.philips.com", "modelNumber": ["929000226503", "BSB002"] }) netdisco-2.8.2/netdisco/discoverables/plex_mediaserver.py000066400000000000000000000013571371643010700237100ustar00rootroot00000000000000"""Discover PlexMediaServer.""" from . import GDMDiscoverable from ..const import ATTR_NAME, ATTR_HOST, ATTR_PORT, ATTR_URLBASE 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 { ATTR_NAME: entry['data']['Name'], ATTR_HOST: entry['from'][0], ATTR_PORT: entry['data']['Port'], ATTR_URLBASE: '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'}) netdisco-2.8.2/netdisco/discoverables/roku.py000066400000000000000000000004151371643010700213240ustar00rootroot00000000000000"""Discover Roku players.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Roku media players.""" def get_entries(self): """Get all the Roku entries.""" return self.find_by_st("roku:ecp") netdisco-2.8.2/netdisco/discoverables/sabnzbd.py000066400000000000000000000005711371643010700217720ustar00rootroot00000000000000"""Discover SABnzbd servers.""" from . import MDNSDiscoverable 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 get_entries(self): return self.find_by_device_name('SABnzbd on') netdisco-2.8.2/netdisco/discoverables/samsung_printer.py000066400000000000000000000006441371643010700235700ustar00rootroot00000000000000"""Discover Samsung Printers""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Support for the discovery of Samsung Printers""" def get_entries(self): """Get all the Samsung Printer uPnP entries.""" return self.find_by_device_description({ "manufacturer": "Samsung Electronics", "deviceType": "urn:schemas-upnp-org:device:Printer:1" }) netdisco-2.8.2/netdisco/discoverables/samsung_tv.py000066400000000000000000000015441371643010700225360ustar00rootroot00000000000000"""Discover Samsung Smart TV services.""" from . import SSDPDiscoverable from ..const import ATTR_NAME # For some models, Samsung forces a [TV] prefix to the user-specified name. FORCED_NAME_PREFIX = '[TV]' class Discoverable(SSDPDiscoverable): """Add support for discovering Samsung Smart TV services.""" def get_entries(self): """Get all the Samsung RemoteControlReceiver entries.""" return self.find_by_st( "urn:samsung.com:device:RemoteControlReceiver:1") def info_from_entry(self, entry): """Get most important info, by default the description location.""" info = super().info_from_entry(entry) # Strip the forced prefix, if present if info[ATTR_NAME].startswith(FORCED_NAME_PREFIX): info[ATTR_NAME] = info[ATTR_NAME][len(FORCED_NAME_PREFIX):].strip() return info netdisco-2.8.2/netdisco/discoverables/sercomm.py000066400000000000000000000007541371643010700220170ustar00rootroot00000000000000""" Discover Sercomm network cameras. These are rebranded as iControl and many others, and are usually distributed as part of an ADT or Comcast/Xfinity monitoring package. https://github.com/edent/Sercomm-API """ from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering camera services.""" def get_entries(self): """Get all Sercomm iControl devices.""" return self.find_by_device_description({'manufacturer': 'iControl'}) netdisco-2.8.2/netdisco/discoverables/songpal.py000066400000000000000000000041531371643010700220120ustar00rootroot00000000000000"""Discover Songpal devices.""" import logging from . import SSDPDiscoverable from . import ATTR_PROPERTIES class Discoverable(SSDPDiscoverable): """Support for Songpal devices. Supported devices: http://vssupport.sony.net/en_ww/device.html.""" def get_entries(self): """Get all the Songpal devices.""" devs = self.find_by_st( "urn:schemas-sony-com:service:ScalarWebAPI:1") # At least some Bravia televisions use the same API for communication, # but are handled by another platforms, so we filter them out here. supported = [] for dev in devs: if 'device' in dev.description: device = dev.description['device'] scalarweb_info = device.get("X_ScalarWebAPI_DeviceInfo", None) if scalarweb_info: services = scalarweb_info["X_ScalarWebAPI_ServiceList"] service_types = services["X_ScalarWebAPI_ServiceType"] # Sony Bravias offer videoScreen service, soundbars do not if 'videoScreen' in service_types: continue supported.append(dev) return supported def info_from_entry(self, entry): """Get information for a device..""" info = super().info_from_entry(entry) cached_descs = entry.DESCRIPTION_CACHE[entry.location] device_info_element = "X_ScalarWebAPI_DeviceInfo" baseurl_element = "X_ScalarWebAPI_BaseURL" device_element = "device" if device_element in cached_descs and \ device_info_element in cached_descs[device_element]: scalarweb = cached_descs[device_element][device_info_element] properties = {"scalarwebapi": scalarweb} if baseurl_element in scalarweb: properties["endpoint"] = scalarweb[baseurl_element] else: logging.warning("Unable to find %s", baseurl_element) info[ATTR_PROPERTIES] = properties else: logging.warning("Unable to find ScalarWeb element from desc.") return info netdisco-2.8.2/netdisco/discoverables/sonos.py000066400000000000000000000004661371643010700215130ustar00rootroot00000000000000"""Discover Sonos devices.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Sonos devices.""" def get_entries(self): """Get all the Sonos device uPnP entries.""" return self.find_by_st("urn:schemas-upnp-org:device:ZonePlayer:1") netdisco-2.8.2/netdisco/discoverables/spotify_connect.py000066400000000000000000000005431371643010700235540ustar00rootroot00000000000000"""Discover devices that implement the Spotify Connect platform.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Spotify Connect service.""" def __init__(self, nd): """Initialize the Cast discovery.""" super(Discoverable, self).__init__(nd, '_spotify-connect._tcp.local.') netdisco-2.8.2/netdisco/discoverables/tellstick.py000066400000000000000000000006131371643010700223420ustar00rootroot00000000000000"""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 netdisco-2.8.2/netdisco/discoverables/tivo_dvr.py000066400000000000000000000007571371643010700222110ustar00rootroot00000000000000"""Discover TiVo DVR devices providing the TCP Remote Protocol.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering TiVo Remote Protocol service.""" def __init__(self, nd): """Initialize the discovery. Yields a dictionary with hostname, host and port along with a properties sub-dictionary with some device specific ids. """ super(Discoverable, self).__init__(nd, '_tivo-remote._tcp.local.') netdisco-2.8.2/netdisco/discoverables/volumio.py000066400000000000000000000004521371643010700220370ustar00rootroot00000000000000"""Discover Volumio servers.""" from . import MDNSDiscoverable class Discoverable(MDNSDiscoverable): """Add support for discovering Volumio.""" def __init__(self, nd): """Initialize the Volumio discovery.""" super(Discoverable, self).__init__(nd, '_Volumio._tcp.local.') netdisco-2.8.2/netdisco/discoverables/webos_tv.py000066400000000000000000000007031371643010700221740ustar00rootroot00000000000000"""Discover LG WebOS TV devices.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering LG WebOS TV devices.""" def get_entries(self): """Get all the LG WebOS TV device uPnP entries.""" return self.find_by_device_description( { "deviceType": "urn:schemas-upnp-org:device:Basic:1", "modelName": "LG Smart TV" } ) netdisco-2.8.2/netdisco/discoverables/wink.py000066400000000000000000000011141371643010700213110ustar00rootroot00000000000000"""Discover Wink hub devices.""" from typing import List # noqa: F401 from . import SSDPDiscoverable from ..ssdp import UPNPEntry # noqa: F401 class Discoverable(SSDPDiscoverable): """Add support for discovering Wink hub devices.""" def get_entries(self): """Return all Wink entries.""" 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')) return results netdisco-2.8.2/netdisco/discoverables/xbox_smartglass.py000066400000000000000000000006511371643010700235660ustar00rootroot00000000000000"""Discover Xbox SmartGlass devices.""" from . import BaseDiscoverable class Discoverable(BaseDiscoverable): """Add support for discovering a Xbox SmartGlass device.""" def __init__(self, netdis): """Initialize the Xbox SmartGlass discovery.""" self._netdis = netdis def get_entries(self): """Get all the Xbox SmartGlass details.""" return self._netdis.xbox_smartglass.entries netdisco-2.8.2/netdisco/discoverables/xiaomi_gw.py000066400000000000000000000023501371643010700223270ustar00rootroot00000000000000"""Discover Xiaomi Mi Home (aka Lumi) Gateways.""" from . import MDNSDiscoverable from ..const import ATTR_MAC_ADDRESS, ATTR_PROPERTIES class Discoverable(MDNSDiscoverable): """Add support for discovering Xiaomi Gateway""" def __init__(self, nd): """Initialize the discovery.""" super(Discoverable, self).__init__(nd, '_miio._udp.local.') def info_from_entry(self, entry): """Return most important info from mDNS entries.""" info = super().info_from_entry(entry) # Workaround of misparsing of mDNS properties. It's unclear # whether it's bug in zeroconf module or in the Gateway, but # returned properties look like: # {b'poch': b'0:mac=286c07aaaaaa\x00'} instead of expected: # {b'epoch': b'0', b'mac': '286c07aaaaaa'} if "poch" in info[ATTR_PROPERTIES]: misparsed = info[ATTR_PROPERTIES]["poch"] misparsed = misparsed.rstrip("\0") for val in misparsed.split(":"): if val.startswith("mac="): info[ATTR_MAC_ADDRESS] = val[len("mac="):] return info def get_entries(self): """Return Xiaomi Gateway devices.""" return self.find_by_device_name('lumi-gateway-') netdisco-2.8.2/netdisco/discoverables/yamaha.py000066400000000000000000000027341371643010700216120ustar00rootroot00000000000000"""Discover Yamaha Receivers.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Yamaha Receivers.""" COMPATIBLE_MODELS = "RX-V" REMOTE_CONTROL_SPEC_TYPE =\ 'urn:schemas-yamaha-com:service:X_YamahaRemoteControl:1' def info_from_entry(self, entry): """Return the most important info from a uPnP entry.""" info = super().info_from_entry(entry) yam = entry.description['X_device'] services = yam['X_serviceList']['X_service'] if isinstance(services, list): service = next( (s for s in services if s['X_specType'] == self.REMOTE_CONTROL_SPEC_TYPE), services[0]) else: service = services # do a slice of the second element so we don't have double / info['control_url'] = yam['X_URLBase'] + service['X_controlURL'][1:] info['description_url'] = (yam['X_URLBase'] + service['X_unitDescURL'][1:]) return info def get_entries(self): """Get all the Yamaha uPnP entries.""" devices = self.find_by_device_description({ "manufacturer": "Yamaha Corporation", "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1" }) return [device for device in devices if device.description['device'].get('modelName', '') .startswith(self.COMPATIBLE_MODELS)] netdisco-2.8.2/netdisco/discoverables/yeelight.py000066400000000000000000000015611371643010700221610ustar00rootroot00000000000000"""Discover Yeelight bulbs, based on Kodi discoverable.""" from . import MDNSDiscoverable from ..const import ATTR_DEVICE_TYPE DEVICE_NAME_PREFIX = 'yeelink-light-' class Discoverable(MDNSDiscoverable): """Add support for discovering Yeelight.""" def __init__(self, nd): """Initialize the Yeelight discovery.""" super(Discoverable, self).__init__(nd, '_miio._udp.local.') def info_from_entry(self, entry): """Return most important info from mDNS entries.""" info = super().info_from_entry(entry) # Example name: yeelink-light-ceiling4_mibt72799069._miio._udp.local. info[ATTR_DEVICE_TYPE] = \ entry.name.replace(DEVICE_NAME_PREFIX, '').split('_', 1)[0] return info def get_entries(self): """ Return yeelight devices. """ return self.find_by_device_name(DEVICE_NAME_PREFIX) netdisco-2.8.2/netdisco/discoverables/ziggo_mediabox_xl.py000066400000000000000000000006701371643010700240410ustar00rootroot00000000000000"""Discover Ziggo Mediabox XL devices.""" from . import SSDPDiscoverable class Discoverable(SSDPDiscoverable): """Add support for discovering Ziggo Mediabox XL devices.""" def get_entries(self): """Return all Ziggo (UPC) Mediabox XL entries.""" return self.find_by_device_description( {'modelDescription': 'UPC Hzn Gateway', 'deviceType': 'urn:schemas-upnp-org:device:RemoteUIServer:2'}) netdisco-2.8.2/netdisco/discovery.py000066400000000000000000000100061371643010700175230ustar00rootroot00000000000000"""Combine all the different protocols into a simple interface.""" 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 from .daikin import Daikin from .smartglass import XboxSmartGlass _LOGGER = logging.getLogger(__name__) class NetworkDiscovery: """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 Xbox One 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): """Initialize the discovery.""" self.mdns = None self.ssdp = None self.gdm = None self.lms = None self.tellstick = None self.daikin = None self.xbox_smartglass = None self.is_discovering = False self.discoverables = None def scan(self, zeroconf_instance=None): """Start and tells scanners to scan.""" self.is_discovering = True self.mdns = MDNS(zeroconf_instance) # Needs to be after MDNS init self._load_device_support() self.mdns.start() self.ssdp = SSDP() self.ssdp.scan() self.gdm = GDM() self.gdm.scan() self.lms = LMS() self.lms.scan() self.tellstick = Tellstick() self.tellstick.scan() self.daikin = Daikin() self.daikin.scan() self.xbox_smartglass = XboxSmartGlass() self.xbox_smartglass.scan() def stop(self): """Turn discovery off.""" if not self.is_discovering: return self.mdns.stop() # Not removing SSDP because it tracks state self.mdns = None self.gdm = None self.lms = None self.tellstick = None self.daikin = None self.xbox_smartglass = None self.discoverables = None self.is_discovering = False def discover(self): """Return a list of discovered devices and services.""" if not self.is_discovering: raise RuntimeError("Needs to be called after start, before stop") 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 _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] module = importlib.import_module( discoverables_format.format(module_name)) self.discoverables[module_name] = \ getattr(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) print("") print("Xbox SmartGlass") pprint(self.xbox_smartglass.entries) netdisco-2.8.2/netdisco/gdm.py000066400000000000000000000065331371643010700162750ustar00rootroot00000000000000""" 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 socket import struct from typing import Any, Dict, List # noqa: F401 class GDM: """Base class to discover GDM services.""" def __init__(self): self.entries = [] # type: List[Dict[str, Any]] self.last_scan = None def scan(self): """Scan the network.""" 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 assigned to self.entries 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, struct.pack("B", 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: bdata, server = sock.recvfrom(1024) data = bdata.decode('utf-8') if '200 OK' in data.splitlines()[0]: ddata = {k: v.strip() for (k, v) in ( line.split(':') for line in data.splitlines() if ':' in line)} self.entries.append({'data': ddata, 'from': server}) except socket.timeout: break finally: sock.close() def main(): """Test GDM discovery.""" from pprint import pprint gdm = GDM() pprint("Scanning GDM...") gdm.update() pprint(gdm.entries) if __name__ == "__main__": main() netdisco-2.8.2/netdisco/lms.py000066400000000000000000000044041371643010700163140ustar00rootroot00000000000000"""Squeezebox/Logitech Media server discovery.""" import socket from typing import Dict, List, Union # noqa: F401 from .const import ATTR_HOST, ATTR_PORT DISCOVERY_PORT = 3483 DEFAULT_DISCOVERY_TIMEOUT = 2 class LMS: """Base class to discover Logitech Media servers.""" def __init__(self): """Initialize the Logitech discovery.""" self.entries = [] # type: List[Dict[str, Union[str, int]]] self.last_scan = None def scan(self): """Scan the network.""" 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 = '' lms_port = DISCOVERY_PORT lms_msg = b"eJSON\0" 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'E'): # Full response is EJSON\xYYXXXX # Where YY is length of port string (ie 4) # And XXXX is the web interface port (ie 9000) port = None # 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({ ATTR_HOST: server[0], ATTR_PORT: port, }) except socket.timeout: break finally: sock.close() self.entries = entries def main(): """Test LMS discovery.""" from pprint import pprint lms = LMS() pprint("Scanning for Logitech Media Servers...") lms.update() pprint(lms.entries) if __name__ == "__main__": main() netdisco-2.8.2/netdisco/mdns.py000066400000000000000000000055601371643010700164660ustar00rootroot00000000000000"""Add support for discovering mDNS services.""" import logging from typing import List # noqa: F401 from zeroconf import DNSPointer, DNSRecord from zeroconf import Error as ZeroconfError from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf _LOGGER = logging.getLogger(__name__) class FastServiceBrowser(ServiceBrowser): """ServiceBrowser that does not process record updates.""" def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: """Ignore record updates for non-ptrs.""" if record.name not in self.types or not isinstance(record, DNSPointer): return super().update_record(zc, now, record) class MDNS: """Base class to discover mDNS services.""" def __init__(self, zeroconf_instance=None): """Initialize the discovery.""" self.zeroconf = zeroconf_instance self._created_zeroconf = False self.services = [] # type: List[ServiceInfo] self._browser = None # type: ServiceBrowser def register_service(self, service): """Register a mDNS service.""" self.services.append(service) def start(self): """Start discovery.""" try: if not self.zeroconf: self.zeroconf = Zeroconf() self._created_zeroconf = True services_by_type = {} for service in self.services: services_by_type.setdefault(service.typ, []) services_by_type[service.typ].append(service) def _service_update(zeroconf, service_type, name, state_change): if state_change == ServiceStateChange.Added: for service in services_by_type[service_type]: try: service.add_service(zeroconf, service_type, name) except ZeroconfError: _LOGGER.exception("Failed to add service %s", name) elif state_change == ServiceStateChange.Removed: for service in services_by_type[service_type]: service.remove_service(zeroconf, service_type, name) types = [service.typ for service in self.services] self._browser = FastServiceBrowser( self.zeroconf, types, handlers=[_service_update] ) except Exception: # pylint: disable=broad-except self.stop() raise def stop(self): """Stop discovering.""" if self._browser: self._browser.cancel() self._browser = None for service in self.services: service.reset() if self._created_zeroconf: self.zeroconf.close() self.zeroconf = None @property def entries(self): """Return all entries in the cache.""" return self.zeroconf.cache.entries() netdisco-2.8.2/netdisco/service.py000066400000000000000000000050271371643010700171630ustar00rootroot00000000000000"""Provide service that scans the network in intervals.""" import logging import threading import time from collections import defaultdict from typing import Any, Callable, Dict, List # noqa: F401 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): """Initialize the discovery.""" super(DiscoveryService, self).__init__() # Scanning interval self.interval = interval # Listeners for new services self.listeners = [] # type: List[Callable[[str, Any], None]] # 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) # type: Dict[str, 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() 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") netdisco-2.8.2/netdisco/smartglass.py000066400000000000000000000101541371643010700177000ustar00rootroot00000000000000"""Xbox One SmartGlass device discovery.""" import socket import struct import binascii from datetime import timedelta from typing import Any, Dict, List, Optional, Tuple # noqa: F401 DISCOVERY_PORT = 5050 DISCOVERY_ADDRESS_BCAST = '' DISCOVERY_ADDRESS_MCAST = '239.255.255.250' DISCOVERY_REQUEST = 0xDD00 DISCOVERY_RESPONSE = 0xDD01 DISCOVERY_TIMEOUT = timedelta(seconds=2) """ SmartGlass Client type XboxOne = 1 Xbox360 = 2 WindowsDesktop = 3 WindowsStore = 4 WindowsPhone = 5 iPhone = 6 iPad = 7 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 = [] # type: List[Tuple[str, Optional[_Response]]] self._discovery_payload = self.discovery_packet() @staticmethod def discovery_packet(): """Assemble discovery payload.""" version = 0 flags = 0 min_version = 0 max_version = 2 payload = struct.pack( '>IHHH', flags, DISCOVERY_CLIENT_TYPE, min_version, max_version ) header = struct.pack( '>HHH', DISCOVERY_REQUEST, len(payload), version ) return header + payload @staticmethod def parse_discovery_response(data): """Parse console's discovery response.""" pos = 0 # Header # pkt_type, payload_len, version = struct.unpack_from( # '>HHH', # data, pos # ) pos += 6 # Payload flags, type_, name_len = struct.unpack_from( '>IHH', data, pos ) pos += 8 name = data[pos:pos + name_len] pos += name_len + 1 # including null terminator uuid_len = struct.unpack_from( '>H', data, pos )[0] pos += 2 uuid = data[pos:pos + uuid_len] pos += uuid_len + 1 # including null terminator last_error, cert_len = struct.unpack_from( '>IH', data, pos ) pos += 6 cert = data[pos:pos + cert_len] return { 'device_type': type_, 'flags': flags, 'name': name.decode('utf-8'), 'uuid': uuid.decode('utf-8'), 'last_error': last_error, 'certificate': binascii.hexlify(cert).decode('utf-8') } def scan(self): """Scan the network.""" self.update() def all(self): """Scan and return all found entries.""" self.scan() return self.entries @staticmethod def verify_packet(data): """Parse packet if it has correct magic""" if len(data) < 2: return None pkt_type = struct.unpack_from('>H', data)[0] if pkt_type != DISCOVERY_RESPONSE: return None return XboxSmartGlass.parse_discovery_response(data) def update(self): """Scan network for Xbox SmartGlass 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(self._discovery_payload, (DISCOVERY_ADDRESS_BCAST, DISCOVERY_PORT)) sock.sendto(self._discovery_payload, (DISCOVERY_ADDRESS_MCAST, DISCOVERY_PORT)) while True: try: data, (address, _) = sock.recvfrom(1024) response = self.verify_packet(data) if response: entries.append((address, response)) except socket.timeout: break self.entries = entries sock.close() def main(): """Test XboxOne discovery.""" from pprint import pprint xbsmartglass = XboxSmartGlass() pprint("Scanning for Xbox One SmartGlass consoles devices..") xbsmartglass.update() pprint(xbsmartglass.entries) if __name__ == "__main__": main() netdisco-2.8.2/netdisco/ssdp.py000066400000000000000000000211621371643010700164720ustar00rootroot00000000000000"""Module that implements SSDP protocol.""" import re 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 import zeroconf from netdisco.util import etree_to_dict DISCOVER_TIMEOUT = 2 # MX is a suggested random wait time for a device to reply, so should be # bound by our discovery timeout. SSDP_MX = DISCOVER_TIMEOUT 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: """Control the scanning of uPnP devices and services and caches output.""" def __init__(self): """Initialize the discovery.""" self.entries = [] # type: List[UPNPEntry] self.last_scan = None def scan(self): """Scan the network.""" self.update() def all(self): """Return all found entries. Will scan for entries if not scanned recently. """ 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.""" 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. """ self.update() seen = set() # type: Set[Optional[str]] results = [] # Make unique based on the location since we don't care about ST here for entry in self.entries: location = entry.location if location not in seen and entry.match_device_description(values): results.append(entry) seen.add(location) return results def update(self, force_update=False): """Scan for new uPnP devices and services.""" if self.last_scan is None or force_update or \ datetime.now()-self.last_scan > MIN_TIME_BETWEEN_SCANS: self.remove_expired() self.entries.extend( entry for entry in scan() if entry not in self.entries) self.last_scan = datetime.now() def remove_expired(self): """Filter out expired entries.""" self.entries = [entry for entry in self.entries if not entry.is_expired] class UPNPEntry: """Found uPnP entry.""" DESCRIPTION_CACHE = {'_NO_LOCATION': {}} # type: Dict[str, Dict] def __init__(self, values): """Initialize the discovery.""" self.values = values self.created = datetime.now() if 'cache-control' in self.values: cache_directive = self.values['cache-control'] max_age = re.findall(r'max-age *= *\d+', cache_directive) if max_age: cache_seconds = int(max_age[0].split('=')[1]) self.expires = self.created + timedelta(seconds=cache_seconds) else: self.expires = None 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, timeout=5).text if not xml: # Samsung Smart TV sometimes returns an empty document the # first time. Retry once. xml = requests.get(url, timeout=5).text tree = ElementTree.fromstring(xml) UPNPEntry.DESCRIPTION_CACHE[url] = \ etree_to_dict(tree).get('root', {}) except requests.RequestException: logging.getLogger(__name__).debug( "Error fetching description at %s", url) UPNPEntry.DESCRIPTION_CACHE[url] = {} except ElementTree.ParseError: logging.getLogger(__name__).debug( "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 "".format(self.location or '', self.st or '') def ssdp_request(ssdp_st, ssdp_mx=SSDP_MX): """Return request bytes for given st and mx.""" return "\r\n".join([ 'M-SEARCH * HTTP/1.1', 'ST: {}'.format(ssdp_st), 'MX: {:d}'.format(ssdp_mx), 'MAN: "ssdp:discover"', 'HOST: {}:{}'.format(*SSDP_TARGET), '', '']).encode('utf-8') # pylint: disable=invalid-name,too-many-locals,too-many-branches def scan(timeout=DISCOVER_TIMEOUT): """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/ """ ssdp_requests = ssdp_request(ST_ALL), ssdp_request(ST_ROOTDEVICE) stop_wait = datetime.now() + timedelta(seconds=timeout) sockets = [] for addr in zeroconf.get_all_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, SSDP_MX) sock.bind((addr, 0)) sockets.append(sock) except socket.error: pass entries = {} for sock in [s for s in sockets]: try: for req in ssdp_requests: sock.sendto(req, 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: data, address = sock.recvfrom(1024) response = data.decode("utf-8") except UnicodeDecodeError: logging.getLogger(__name__).debug( 'Ignoring invalid unicode response from %s', address) continue except socket.error: logging.getLogger(__name__).exception( "Socket error while discovering SSDP devices") sockets.remove(sock) sock.close() continue entry = UPNPEntry.from_response(response) entries[(entry.st, entry.location)] = entry finally: for s in sockets: s.close() return sorted(entries.values(), key=lambda entry: entry.location or '') def main(): """Test SSDP discovery.""" from pprint import pprint print("Scanning SSDP..") pprint(scan()) if __name__ == "__main__": main() netdisco-2.8.2/netdisco/tellstick.py000066400000000000000000000037661371643010700175310ustar00rootroot00000000000000"""Tellstick device discovery.""" import socket from datetime import timedelta import logging from typing import List, Tuple # noqa: F401 DISCOVERY_PORT = 30303 DISCOVERY_ADDRESS = '' DISCOVERY_PAYLOAD = b"D" DISCOVERY_TIMEOUT = timedelta(seconds=2) class Tellstick: """Base class to discover Tellstick devices.""" def __init__(self): """Initialize the Tellstick discovery.""" self.entries = [] # type: List[Tuple[str]] def scan(self): """Scan the network.""" 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.insert(0, address) entries.append(tuple(entry)) except socket.timeout: break except UnicodeDecodeError: # Catch invalid responses logging.getLogger(__name__).debug( 'Ignoring invalid unicode response from %s', address) continue 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() netdisco-2.8.2/netdisco/util.py000066400000000000000000000021151371643010700164730ustar00rootroot00000000000000"""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 # 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 } # type: Dict[str, Optional[Dict[str, Any]]] children = list(t) if children: 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: 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: assert dt is not None dt['#text'] = text else: d[tag_name] = text return d netdisco-2.8.2/pylintrc000066400000000000000000000000401371643010700151160ustar00rootroot00000000000000[MASTER] disable=duplicate-code netdisco-2.8.2/requirements.txt000066400000000000000000000000741371643010700166220ustar00rootroot00000000000000zeroconf>=0.21.0 requests>=2.0 typing; python_version<'3.5' netdisco-2.8.2/script/000077500000000000000000000000001371643010700146415ustar00rootroot00000000000000netdisco-2.8.2/script/release000077500000000000000000000004751371643010700162150ustar00rootroot00000000000000#!/bin/sh # Pushes a new version to PyPi. cd "$(dirname "$0")/.." CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD` if [ "$CURRENT_BRANCH" != "master" ] then echo "You have to be on the master to release." exit 1 fi rm -rf dist python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* --skip-existing netdisco-2.8.2/setup.cfg000066400000000000000000000005671371643010700151660ustar00rootroot00000000000000[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 netdisco-2.8.2/setup.py000066400000000000000000000022541371643010700150520ustar00rootroot00000000000000"""Setup file for netdisco.""" import os from setuptools import setup, find_packages 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.8.2", 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.27.1"], 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", ], ) netdisco-2.8.2/tests/000077500000000000000000000000001371643010700144775ustar00rootroot00000000000000netdisco-2.8.2/tests/__init__.py000066400000000000000000000000321371643010700166030ustar00rootroot00000000000000"""Tests for NetDisco.""" netdisco-2.8.2/tests/discoverables/000077500000000000000000000000001371643010700173245ustar00rootroot00000000000000netdisco-2.8.2/tests/discoverables/test_yamaha.py000066400000000000000000000113711371643010700222000ustar00rootroot00000000000000"""The tests for discovering Yamaha Receivers.""" import unittest from unittest.mock import MagicMock import xml.etree.ElementTree as ElementTree from netdisco.discoverables.yamaha import Discoverable from netdisco.util import etree_to_dict LOCATION = 'http://192.168.XXX.XXX:80/desc.xml' class MockUPNPEntry(object): """UPNPEntry backed by a description file.""" location = LOCATION def __init__(self, name): """Read and parse a MockUPNPEntry from a file.""" with open('tests/discoverables/yamaha_files/%s' % name, encoding='utf-8') as content: self.description = etree_to_dict( ElementTree.fromstring(content.read())).get('root', {}) class TestYamaha(unittest.TestCase): """Test the Yamaha Discoverable.""" def test_info_from_entry_rx_v481(self): self.assertEqual( Discoverable(None).info_from_entry( MockUPNPEntry("desc_RX-V481.xml")), { 'control_url': 'http://192.168.XXX.XXX:80/YamahaRemoteControl/ctrl', 'description_url': 'http://192.168.XXX.XXX:80/YamahaRemoteControl/desc.xml', 'host': '192.168.xxx.xxx', 'model_name': 'RX-V481', 'model_number': 'V481', 'manufacturer': 'Yamaha Corporation', 'name': 'RX-V481 XXXXXX', 'port': 80, 'serial': 'XXXXXXXX', 'ssdp_description': 'http://192.168.XXX.XXX:80/desc.xml', 'udn': 'uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', 'upnp_device_type': 'urn:schemas-upnp-org:device:MediaRenderer:1', }) def test_info_from_entry_single_service(self): self.assertEqual( Discoverable(None).info_from_entry( MockUPNPEntry("desc_single_service.xml")), { 'control_url': 'http://192.168.1.2:80/YamahaRemoteControl/single_ctrl', 'description_url': 'http://192.168.1.2:80/YamahaRemoteControl/single_desc.xml', 'host': '192.168.xxx.xxx', 'model_name': 'RX-V single service model name', 'model_number': None, 'manufacturer': None, 'name': 'single service friendly name', 'port': 80, 'serial': None, 'ssdp_description': 'http://192.168.XXX.XXX:80/desc.xml', 'udn': None, 'upnp_device_type': None, }) def test_info_from_entry_multiple_services_remote_control_last(self): self.assertEqual( Discoverable(None).info_from_entry(MockUPNPEntry( "desc_multiple_services_remote_control_last.xml")), { 'control_url': 'http://192.168.1.2:80/YamahaRemoteControl/multi_ctrl', 'description_url': 'http://192.168.1.2:80/YamahaRemoteControl/multi_desc.xml', 'host': '192.168.xxx.xxx', 'model_name': 'RX-V multi service model name', 'model_number': None, 'manufacturer': None, 'name': 'multi service friendly name', 'port': 80, 'serial': None, 'ssdp_description': 'http://192.168.XXX.XXX:80/desc.xml', 'udn': None, 'upnp_device_type': None, }) def test_info_from_entry_multiple_services_no_remote_control(self): self.assertEqual( Discoverable(None).info_from_entry(MockUPNPEntry( "desc_multiple_services_no_remote_control.xml")), { 'control_url': 'http://192.168.1.2:80/YamahaNewControl/ctrl', 'description_url': 'http://192.168.1.2:80/YamahaNewControl/desc.xml', 'host': '192.168.xxx.xxx', 'model_name': 'RX-V multi service model name', 'model_number': None, 'manufacturer': None, 'name': 'multi service friendly name', 'port': 80, 'serial': None, 'ssdp_description': 'http://192.168.XXX.XXX:80/desc.xml', 'udn': None, 'upnp_device_type': None, }) def test_get_entries_incompatible_models(self): supported_model = MockUPNPEntry( "desc_RX-V481.xml") devices = [ supported_model, MockUPNPEntry("desc_R-N602.xml") ] discoverable = Discoverable(None) discoverable.find_by_device_description = MagicMock( return_value=devices) self.assertEqual(discoverable.get_entries(), [supported_model]) netdisco-2.8.2/tests/discoverables/yamaha_files/000077500000000000000000000000001371643010700217465ustar00rootroot00000000000000netdisco-2.8.2/tests/discoverables/yamaha_files/desc_R-N602.xml000066400000000000000000000056071371643010700243220ustar00rootroot00000000000000 1 0 DMR-1.50 urn:schemas-upnp-org:device:MediaRenderer:1 Yamaha R-N602 Yamaha Corporation http://www.yamaha.com/ MusicCast R-N602 N602 http://www.yamaha.com/ XXXXXXXX uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX image/jpeg 48 48 24 /Icons/48x48.jpg image/jpeg 120 120 24 /Icons/120x120.jpg image/png 48 48 24 /Icons/48x48.png image/png 120 120 24 /Icons/120x120.png urn:schemas-upnp-org:service:AVTransport:1 urn:upnp-org:serviceId:AVTransport /AVTransport/desc.xml /AVTransport/ctrl /AVTransport/event urn:schemas-upnp-org:service:RenderingControl:1 urn:upnp-org:serviceId:RenderingControl /RenderingControl/desc.xml /RenderingControl/ctrl /RenderingControl/event urn:schemas-upnp-org:service:ConnectionManager:1 urn:upnp-org:serviceId:ConnectionManager /ConnectionManager/desc.xml /ConnectionManager/ctrl /ConnectionManager/event http://192.168.XXX.XXX/ http://192.168.XXX.XXX:80/ urn:schemas-yamaha-com:service:X_YamahaRemoteControl:1 /YamahaRemoteControl/ctrl /YamahaRemoteControl/desc.xml urn:schemas-yamaha-com:service:X_YamahaExtendedControl:1 /YamahaExtendedControl/v1/ 1812 netdisco-2.8.2/tests/discoverables/yamaha_files/desc_RX-V481.xml000066400000000000000000000056171371643010700244700ustar00rootroot00000000000000 1 0 DMR-1.50 urn:schemas-upnp-org:device:MediaRenderer:1 RX-V481 XXXXXX Yamaha Corporation http://www.yamaha.com/ AV Receiver RX-V481 V481 http://www.yamaha.com/ XXXXXXXX uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX image/jpeg 48 48 24 /Icons/48x48.jpg image/jpeg 120 120 24 /Icons/120x120.jpg image/png 48 48 24 /Icons/48x48.png image/png 120 120 24 /Icons/120x120.png urn:schemas-upnp-org:service:AVTransport:1 urn:upnp-org:serviceId:AVTransport /AVTransport/desc.xml /AVTransport/ctrl /AVTransport/event urn:schemas-upnp-org:service:RenderingControl:1 urn:upnp-org:serviceId:RenderingControl /RenderingControl/desc.xml /RenderingControl/ctrl /RenderingControl/event urn:schemas-upnp-org:service:ConnectionManager:1 urn:upnp-org:serviceId:ConnectionManager /ConnectionManager/desc.xml /ConnectionManager/ctrl /ConnectionManager/event http://192.168.XXX.XXX/ http://192.168.XXX.XXX:80/ urn:schemas-yamaha-com:service:X_YamahaRemoteControl:1 /YamahaRemoteControl/ctrl /YamahaRemoteControl/desc.xml urn:schemas-yamaha-com:service:X_YamahaExtendedControl:1 /YamahaExtendedControl/v1/ 1131 netdisco-2.8.2/tests/discoverables/yamaha_files/desc_multiple_services_no_remote_control.xml000066400000000000000000000016131371643010700330340ustar00rootroot00000000000000 multi service friendly name RX-V multi service model name http://192.168.1.2:80/ urn:schemas-yamaha-com:service:X_YamahaNewControl:1 /YamahaNewControl/ctrl /YamahaNewControl/desc.xml urn:schemas-yamaha-com:service:X_YamahaExtendedControl:1 /YamahaExtendedControl/v1/ 1131 netdisco-2.8.2/tests/discoverables/yamaha_files/desc_multiple_services_remote_control_last.xml000066400000000000000000000016401371643010700333630ustar00rootroot00000000000000 multi service friendly name RX-V multi service model name http://192.168.1.2:80/ urn:schemas-yamaha-com:service:X_YamahaExtendedControl:1 /YamahaExtendedControl/v1/ 1131 urn:schemas-yamaha-com:service:X_YamahaRemoteControl:1 /YamahaRemoteControl/multi_ctrl /YamahaRemoteControl/multi_desc.xml netdisco-2.8.2/tests/discoverables/yamaha_files/desc_single_service.xml000066400000000000000000000013361371643010700264720ustar00rootroot00000000000000 1 0 single service friendly name RX-V single service model name http://192.168.1.2:80/ urn:schemas-yamaha-com:service:X_YamahaRemoteControl:1 /YamahaRemoteControl/single_ctrl /YamahaRemoteControl/single_desc.xml netdisco-2.8.2/tests/test_xboxone.py000066400000000000000000000036431371643010700176000ustar00rootroot00000000000000"""The tests for discovering Xbox gaming consoles via SmartGlass protocol.""" import unittest from binascii import unhexlify from netdisco.smartglass import XboxSmartGlass class TestXboxOne(unittest.TestCase): """Test the Xbox One Discoverable.""" def setUp(self): """ Setup test class """ with open('tests/xboxone_files/discovery_response', 'rb') as content: packet = content.read() if not packet: raise Exception('Failed to read test data') self.discovery_response = packet def test_assemble_request(self): """ Test discovery request assembly """ packet = XboxSmartGlass.discovery_packet() self.assertEqual( packet, unhexlify(b'dd00000a000000000000000400000002') ) def test_parse_response(self): """ Test discovery response parsing """ response = XboxSmartGlass.parse_discovery_response( self.discovery_response) self.assertEqual(response['device_type'], 1) self.assertEqual(response['flags'], 2) self.assertEqual(response['name'], 'XboxOne') self.assertEqual(response['uuid'], 'DE305D54-75B4-431B-ADB2-EB6B9E546014') self.assertEqual(response['last_error'], 0) self.assertEqual(response['certificate'][:8], '30820203') self.assertEqual(len(unhexlify(response['certificate'])), 519) def test_verify_response(self): """ Test discovery response verification """ valid_parse = XboxSmartGlass.verify_packet(self.discovery_response) invalid_length = XboxSmartGlass.verify_packet(unhexlify(b'41')) invalid_magic = XboxSmartGlass.verify_packet( unhexlify(b'aabbccddeeff00')) self.assertIsNotNone(valid_parse) self.assertIsNone(invalid_length) self.assertIsNone(invalid_magic) netdisco-2.8.2/tests/xboxone_files/000077500000000000000000000000001371643010700173435ustar00rootroot00000000000000netdisco-2.8.2/tests/xboxone_files/discovery_response000066400000000000000000000011121371643010700232060ustar00rootroot00000000000000DXboxOne$DE305D54-75B4-431B-ADB2-EB6B9E546014000  *H  01 0 U Rust0 170930152542Z 180930152542Z010U FFFFFFFFFFF0Y0*H=*H=B8-ג4/q~j%wyF5skLj one\C#z#1kВ/0-0 U0U% 0 +0 U#00  *H  X|z7{Tvǔf0iب 4]-/.3KXxOO7їj_5J[6 Iҽ*YʕWaao8-B i}ύ Jچ,N[vw?ZoMXU>k4yU҄񞐴n AHu`wLeFSqX Oz2mab+LC)ZGE) 8Gq14a^;LDnetdisco-2.8.2/tox.ini000066400000000000000000000007301371643010700146500ustar00rootroot00000000000000[tox] envlist = py34, py35, py36, py37, lint, typing skip_missing_interpreters = True [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/netdisco deps = -r{toxinidir}/requirements.txt pytest commands = py.test [testenv:lint] deps = flake8 pylint 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