pax_global_header 0000666 0000000 0000000 00000000064 14064530656 0014523 g ustar 00root root 0000000 0000000 52 comment=96029c59049b63c24c008e4f5e1c48fb5699e94c
netdisco-2.9.0/ 0000775 0000000 0000000 00000000000 14064530656 0013343 5 ustar 00root root 0000000 0000000 netdisco-2.9.0/.github/ 0000775 0000000 0000000 00000000000 14064530656 0014703 5 ustar 00root root 0000000 0000000 netdisco-2.9.0/.github/release-drafter.yml 0000664 0000000 0000000 00000000054 14064530656 0020472 0 ustar 00root root 0000000 0000000 template: |
## What's Changed
$CHANGES
netdisco-2.9.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14064530656 0016740 5 ustar 00root root 0000000 0000000 netdisco-2.9.0/.github/workflows/pythonpublish.yml 0000664 0000000 0000000 00000001515 14064530656 0022375 0 ustar 00root root 0000000 0000000 # This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Upload Python Package
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
netdisco-2.9.0/.gitignore 0000664 0000000 0000000 00000000205 14064530656 0015330 0 ustar 00root root 0000000 0000000 __pycache__
**/*.pyc
netdisco.egg-info/
dist
build
pyvenv.cfg
bin
lib
lib64
pip-selfcheck.json
.tox
.pytest_cache/
.mypy_cache/
venv
netdisco-2.9.0/.travis.yml 0000664 0000000 0000000 00000000707 14064530656 0015460 0 ustar 00root root 0000000 0000000 sudo: 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.9.0/CLA.md 0000664 0000000 0000000 00000003266 14064530656 0014273 0 ustar 00root root 0000000 0000000 # 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.9.0/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000006557 14064530656 0016157 0 ustar 00root root 0000000 0000000 # 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.9.0/LICENSE.md 0000664 0000000 0000000 00000024360 14064530656 0014754 0 ustar 00root root 0000000 0000000 Apache 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.9.0/MANIFEST.in 0000664 0000000 0000000 00000000061 14064530656 0015076 0 ustar 00root root 0000000 0000000 include README.md
include LICENSE.md
graft tests
netdisco-2.9.0/README.md 0000664 0000000 0000000 00000003401 14064530656 0014620 0 ustar 00root root 0000000 0000000 ## 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.9.0/example_service.py 0000664 0000000 0000000 00000001136 14064530656 0017071 0 ustar 00root root 0000000 0000000 """
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.9.0/netdisco/ 0000775 0000000 0000000 00000000000 14064530656 0015153 5 ustar 00root root 0000000 0000000 netdisco-2.9.0/netdisco/__init__.py 0000664 0000000 0000000 00000000117 14064530656 0017263 0 ustar 00root root 0000000 0000000 """Module to scan the network using uPnP and mDNS for devices and services."""
netdisco-2.9.0/netdisco/__main__.py 0000664 0000000 0000000 00000001401 14064530656 0017241 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/const.py 0000664 0000000 0000000 00000002313 14064530656 0016652 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/daikin.py 0000664 0000000 0000000 00000005573 14064530656 0016776 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/ 0000775 0000000 0000000 00000000000 14064530656 0020000 5 ustar 00root root 0000000 0000000 netdisco-2.9.0/netdisco/discoverables/__init__.py 0000664 0000000 0000000 00000011716 14064530656 0022117 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/apple_tv.py 0000664 0000000 0000000 00000001043 14064530656 0022162 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/arduino.py 0000664 0000000 0000000 00000000402 14064530656 0022007 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/asus_router.py 0000664 0000000 0000000 00000000642 14064530656 0022727 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/axis.py 0000664 0000000 0000000 00000002316 14064530656 0021320 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/belkin_wemo.py 0000664 0000000 0000000 00000001231 14064530656 0022642 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/bluesound.py 0000664 0000000 0000000 00000000521 14064530656 0022350 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/bose_soundtouch.py 0000664 0000000 0000000 00000000515 14064530656 0023556 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/cambridgeaudio.py 0000664 0000000 0000000 00000000731 14064530656 0023312 0 ustar 00root root 0000000 0000000 """ 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.9.0/netdisco/discoverables/canon_printer.py 0000664 0000000 0000000 00000000627 14064530656 0023220 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/daikin.py 0000664 0000000 0000000 00000000574 14064530656 0021617 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/deconz.py 0000664 0000000 0000000 00000000713 14064530656 0021635 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/denonavr.py 0000664 0000000 0000000 00000001356 14064530656 0022173 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/digitalstrom.py 0000664 0000000 0000000 00000000576 14064530656 0023064 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/directv.py 0000664 0000000 0000000 00000000627 14064530656 0022017 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/dlna_dmr.py 0000664 0000000 0000000 00000000745 14064530656 0022140 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/dlna_dms.py 0000664 0000000 0000000 00000001036 14064530656 0022133 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/enigma2.py 0000664 0000000 0000000 00000000461 14064530656 0021675 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/esphome.py 0000664 0000000 0000000 00000000363 14064530656 0022014 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/freebox.py 0000664 0000000 0000000 00000000462 14064530656 0022006 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/fritzbox.py 0000664 0000000 0000000 00000000460 14064530656 0022221 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/frontier_silicon.py 0000664 0000000 0000000 00000000667 14064530656 0023733 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/google_cast.py 0000664 0000000 0000000 00000000537 14064530656 0022645 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/harmony.py 0000664 0000000 0000000 00000000624 14064530656 0022031 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/hass_ios.py 0000664 0000000 0000000 00000000425 14064530656 0022163 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/hass_mobile_app.py 0000664 0000000 0000000 00000000451 14064530656 0023477 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/heos.py 0000664 0000000 0000000 00000000450 14064530656 0021307 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/hikvision.py 0000664 0000000 0000000 00000000611 14064530656 0022353 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/home_assistant.py 0000664 0000000 0000000 00000000431 14064530656 0023371 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/homekit.py 0000664 0000000 0000000 00000001006 14064530656 0022007 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/hp_printer.py 0000664 0000000 0000000 00000000566 14064530656 0022533 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/huawei_router.py 0000664 0000000 0000000 00000000660 14064530656 0023236 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/igd.py 0000664 0000000 0000000 00000000705 14064530656 0021117 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/ikea_tradfri.py 0000664 0000000 0000000 00000000522 14064530656 0022775 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/kodi.py 0000664 0000000 0000000 00000000553 14064530656 0021303 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/konnected.py 0000664 0000000 0000000 00000000511 14064530656 0022321 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/lg_smart_device.py 0000664 0000000 0000000 00000000465 14064530656 0023506 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/logitech_mediaserver.py 0000664 0000000 0000000 00000000636 14064530656 0024543 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/lutron.py 0000664 0000000 0000000 00000000621 14064530656 0021674 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/nanoleaf_aurora.py 0000664 0000000 0000000 00000000426 14064530656 0023510 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/netgear_router.py 0000664 0000000 0000000 00000000643 14064530656 0023402 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/octoprint.py 0000664 0000000 0000000 00000000540 14064530656 0022372 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/openhome.py 0000664 0000000 0000000 00000000742 14064530656 0022167 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/panasonic_viera.py 0000664 0000000 0000000 00000000516 14064530656 0023515 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/philips_hue.py 0000664 0000000 0000000 00000000731 14064530656 0022664 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/plex_mediaserver.py 0000664 0000000 0000000 00000001357 14064530656 0023716 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/roku.py 0000664 0000000 0000000 00000000415 14064530656 0021332 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/sabnzbd.py 0000664 0000000 0000000 00000000571 14064530656 0022000 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/samsung_printer.py 0000664 0000000 0000000 00000000644 14064530656 0023576 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/samsung_tv.py 0000664 0000000 0000000 00000001544 14064530656 0022544 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/sercomm.py 0000664 0000000 0000000 00000000754 14064530656 0022025 0 ustar 00root root 0000000 0000000 """
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.9.0/netdisco/discoverables/songpal.py 0000664 0000000 0000000 00000004153 14064530656 0022020 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/sonos.py 0000664 0000000 0000000 00000000466 14064530656 0021521 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/spotify_connect.py 0000664 0000000 0000000 00000000543 14064530656 0023562 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/tellstick.py 0000664 0000000 0000000 00000000613 14064530656 0022350 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/tivo_dvr.py 0000664 0000000 0000000 00000000757 14064530656 0022217 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/volumio.py 0000664 0000000 0000000 00000000452 14064530656 0022045 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/webos_tv.py 0000664 0000000 0000000 00000000703 14064530656 0022202 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/wink.py 0000664 0000000 0000000 00000001114 14064530656 0021317 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/xbox_smartglass.py 0000664 0000000 0000000 00000000651 14064530656 0023574 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/xiaomi_gw.py 0000664 0000000 0000000 00000002350 14064530656 0022335 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/yamaha.py 0000664 0000000 0000000 00000002734 14064530656 0021620 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/yeelight.py 0000664 0000000 0000000 00000001561 14064530656 0022167 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discoverables/ziggo_mediabox_xl.py 0000664 0000000 0000000 00000000670 14064530656 0024047 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/discovery.py 0000664 0000000 0000000 00000010240 14064530656 0017531 0 ustar 00root root 0000000 0000000 """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, suppress_mdns_types=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()
if suppress_mdns_types:
for type_ in suppress_mdns_types:
self.mdns.unregister_type(type_)
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.9.0/netdisco/gdm.py 0000664 0000000 0000000 00000006533 14064530656 0016303 0 ustar 00root root 0000000 0000000 """
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.9.0/netdisco/lms.py 0000664 0000000 0000000 00000004404 14064530656 0016322 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/mdns.py 0000664 0000000 0000000 00000006536 14064530656 0016500 0 ustar 00root root 0000000 0000000 """Add support for discovering mDNS services."""
import itertools
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 unregister_type(self, type_):
"""Unregister a mDNS type."""
removes = []
for service in self.services:
if service.typ == type_:
removes.append(service)
for service in removes:
self.services.remove(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 list(
itertools.chain(
*[
self.zeroconf.cache.entries_with_name(name)
for name in self.zeroconf.cache.names()
]
)
)
netdisco-2.9.0/netdisco/service.py 0000664 0000000 0000000 00000005027 14064530656 0017171 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/smartglass.py 0000664 0000000 0000000 00000010154 14064530656 0017706 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/ssdp.py 0000664 0000000 0000000 00000021162 14064530656 0016500 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/tellstick.py 0000664 0000000 0000000 00000003766 14064530656 0017537 0 ustar 00root root 0000000 0000000 """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.9.0/netdisco/util.py 0000664 0000000 0000000 00000002115 14064530656 0016501 0 ustar 00root root 0000000 0000000 """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.9.0/pylintrc 0000664 0000000 0000000 00000000040 14064530656 0015124 0 ustar 00root root 0000000 0000000 [MASTER]
disable=duplicate-code
netdisco-2.9.0/requirements.txt 0000664 0000000 0000000 00000000074 14064530656 0016630 0 ustar 00root root 0000000 0000000 zeroconf>=0.21.0
requests>=2.0
typing; python_version<'3.5'
netdisco-2.9.0/script/ 0000775 0000000 0000000 00000000000 14064530656 0014647 5 ustar 00root root 0000000 0000000 netdisco-2.9.0/script/release 0000775 0000000 0000000 00000000475 14064530656 0016223 0 ustar 00root root 0000000 0000000 #!/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.9.0/setup.cfg 0000664 0000000 0000000 00000000567 14064530656 0015174 0 ustar 00root root 0000000 0000000 [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.9.0/setup.py 0000664 0000000 0000000 00000002254 14064530656 0015060 0 ustar 00root root 0000000 0000000 """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.9.0",
description="Discover devices on your local network",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/home-assistant/netdisco",
author="Paulus Schoutsen",
author_email="Paulus@PaulusSchoutsen.nl",
license="Apache License 2.0",
install_requires=["requests>=2.0", "zeroconf>=0.30.0"],
python_requires=">=3",
packages=find_packages(exclude=["tests", "tests.*"]),
zip_safe=False,
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: POSIX",
"Programming Language :: Python :: 3",
"Topic :: Utilities",
"Topic :: Home Automation",
"Topic :: System :: Networking",
],
)
netdisco-2.9.0/tests/ 0000775 0000000 0000000 00000000000 14064530656 0014505 5 ustar 00root root 0000000 0000000 netdisco-2.9.0/tests/__init__.py 0000664 0000000 0000000 00000000032 14064530656 0016611 0 ustar 00root root 0000000 0000000 """Tests for NetDisco."""
netdisco-2.9.0/tests/discoverables/ 0000775 0000000 0000000 00000000000 14064530656 0017332 5 ustar 00root root 0000000 0000000 netdisco-2.9.0/tests/discoverables/test_yamaha.py 0000664 0000000 0000000 00000011371 14064530656 0022206 0 ustar 00root root 0000000 0000000 """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.9.0/tests/discoverables/yamaha_files/ 0000775 0000000 0000000 00000000000 14064530656 0021754 5 ustar 00root root 0000000 0000000 netdisco-2.9.0/tests/discoverables/yamaha_files/desc_R-N602.xml 0000664 0000000 0000000 00000005607 14064530656 0024330 0 ustar 00root root 0000000 0000000
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