pax_global_header 0000666 0000000 0000000 00000000064 13450712060 0014510 g ustar 00root root 0000000 0000000 52 comment=7045c3ce8e0ff5c555b9513349d23b2370af1fa7
netdisco-2.6.0/ 0000775 0000000 0000000 00000000000 13450712060 0013325 5 ustar 00root root 0000000 0000000 netdisco-2.6.0/.github/ 0000775 0000000 0000000 00000000000 13450712060 0014665 5 ustar 00root root 0000000 0000000 netdisco-2.6.0/.github/release-drafter.yml 0000664 0000000 0000000 00000000054 13450712060 0020454 0 ustar 00root root 0000000 0000000 template: |
## What's Changed
$CHANGES
netdisco-2.6.0/.gitignore 0000664 0000000 0000000 00000000200 13450712060 0015305 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/
netdisco-2.6.0/.travis.yml 0000664 0000000 0000000 00000000707 13450712060 0015442 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.6.0/CLA.md 0000664 0000000 0000000 00000003266 13450712060 0014255 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.6.0/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000006557 13450712060 0016141 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.6.0/LICENSE.md 0000664 0000000 0000000 00000024360 13450712060 0014736 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.6.0/MANIFEST.in 0000664 0000000 0000000 00000000061 13450712060 0015060 0 ustar 00root root 0000000 0000000 include README.md
include LICENSE.md
graft tests
netdisco-2.6.0/README.md 0000664 0000000 0000000 00000002701 13450712060 0014604 0 ustar 00root root 0000000 0000000 # 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.6.0/example_service.py 0000664 0000000 0000000 00000001136 13450712060 0017053 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.6.0/netdisco/ 0000775 0000000 0000000 00000000000 13450712060 0015135 5 ustar 00root root 0000000 0000000 netdisco-2.6.0/netdisco/__init__.py 0000664 0000000 0000000 00000000117 13450712060 0017245 0 ustar 00root root 0000000 0000000 """Module to scan the network using uPnP and mDNS for devices and services."""
netdisco-2.6.0/netdisco/__main__.py 0000664 0000000 0000000 00000001401 13450712060 0017223 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.6.0/netdisco/const.py 0000664 0000000 0000000 00000002313 13450712060 0016634 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.6.0/netdisco/daikin.py 0000664 0000000 0000000 00000005573 13450712060 0016760 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.6.0/netdisco/discoverables/ 0000775 0000000 0000000 00000000000 13450712060 0017762 5 ustar 00root root 0000000 0000000 netdisco-2.6.0/netdisco/discoverables/__init__.py 0000664 0000000 0000000 00000011515 13450712060 0022076 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 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.address)),
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.6.0/netdisco/discoverables/apple_tv.py 0000664 0000000 0000000 00000001043 13450712060 0022144 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.6.0/netdisco/discoverables/arduino.py 0000664 0000000 0000000 00000000402 13450712060 0021771 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.6.0/netdisco/discoverables/asus_router.py 0000664 0000000 0000000 00000000642 13450712060 0022711 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.6.0/netdisco/discoverables/axis.py 0000664 0000000 0000000 00000002316 13450712060 0021302 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.6.0/netdisco/discoverables/belkin_wemo.py 0000664 0000000 0000000 00000001231 13450712060 0022624 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.6.0/netdisco/discoverables/bluesound.py 0000664 0000000 0000000 00000000521 13450712060 0022332 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.6.0/netdisco/discoverables/bose_soundtouch.py 0000664 0000000 0000000 00000000515 13450712060 0023540 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.6.0/netdisco/discoverables/cambridgeaudio.py 0000664 0000000 0000000 00000000731 13450712060 0023274 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.6.0/netdisco/discoverables/canon_printer.py 0000664 0000000 0000000 00000000627 13450712060 0023202 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.6.0/netdisco/discoverables/daikin.py 0000664 0000000 0000000 00000000574 13450712060 0021601 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.6.0/netdisco/discoverables/deconz.py 0000664 0000000 0000000 00000000713 13450712060 0021617 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.6.0/netdisco/discoverables/denonavr.py 0000664 0000000 0000000 00000001356 13450712060 0022155 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.6.0/netdisco/discoverables/digitalstrom.py 0000664 0000000 0000000 00000000576 13450712060 0023046 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.6.0/netdisco/discoverables/directv.py 0000664 0000000 0000000 00000000627 13450712060 0022001 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.6.0/netdisco/discoverables/dlna_dmr.py 0000664 0000000 0000000 00000000745 13450712060 0022122 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.6.0/netdisco/discoverables/dlna_dms.py 0000664 0000000 0000000 00000001036 13450712060 0022115 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.6.0/netdisco/discoverables/enigma2.py 0000664 0000000 0000000 00000000461 13450712060 0021657 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.6.0/netdisco/discoverables/esphome.py 0000664 0000000 0000000 00000000363 13450712060 0021776 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.6.0/netdisco/discoverables/freebox.py 0000664 0000000 0000000 00000000462 13450712060 0021770 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.6.0/netdisco/discoverables/fritzbox.py 0000664 0000000 0000000 00000000460 13450712060 0022203 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.6.0/netdisco/discoverables/frontier_silicon.py 0000664 0000000 0000000 00000000667 13450712060 0023715 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.6.0/netdisco/discoverables/google_cast.py 0000664 0000000 0000000 00000000537 13450712060 0022627 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.6.0/netdisco/discoverables/harmony.py 0000664 0000000 0000000 00000000624 13450712060 0022013 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.6.0/netdisco/discoverables/hass_ios.py 0000664 0000000 0000000 00000000425 13450712060 0022145 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.6.0/netdisco/discoverables/hass_mobile_app.py 0000664 0000000 0000000 00000000451 13450712060 0023461 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.6.0/netdisco/discoverables/heos.py 0000664 0000000 0000000 00000000450 13450712060 0021271 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.6.0/netdisco/discoverables/hikvision.py 0000664 0000000 0000000 00000000611 13450712060 0022335 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.6.0/netdisco/discoverables/home_assistant.py 0000664 0000000 0000000 00000000431 13450712060 0023353 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.6.0/netdisco/discoverables/homekit.py 0000664 0000000 0000000 00000001006 13450712060 0021771 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.6.0/netdisco/discoverables/hp_printer.py 0000664 0000000 0000000 00000000566 13450712060 0022515 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.6.0/netdisco/discoverables/huawei_router.py 0000664 0000000 0000000 00000000660 13450712060 0023220 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.6.0/netdisco/discoverables/igd.py 0000664 0000000 0000000 00000000705 13450712060 0021101 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.6.0/netdisco/discoverables/ikea_tradfri.py 0000664 0000000 0000000 00000000522 13450712060 0022757 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.6.0/netdisco/discoverables/kodi.py 0000664 0000000 0000000 00000000553 13450712060 0021265 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.6.0/netdisco/discoverables/konnected.py 0000664 0000000 0000000 00000000511 13450712060 0022303 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.6.0/netdisco/discoverables/lg_smart_device.py 0000664 0000000 0000000 00000000465 13450712060 0023470 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.6.0/netdisco/discoverables/logitech_mediaserver.py 0000664 0000000 0000000 00000000636 13450712060 0024525 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.6.0/netdisco/discoverables/lutron.py 0000664 0000000 0000000 00000000621 13450712060 0021656 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.6.0/netdisco/discoverables/nanoleaf_aurora.py 0000664 0000000 0000000 00000000426 13450712060 0023472 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.6.0/netdisco/discoverables/netgear_router.py 0000664 0000000 0000000 00000000643 13450712060 0023364 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.6.0/netdisco/discoverables/octoprint.py 0000664 0000000 0000000 00000000540 13450712060 0022354 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.6.0/netdisco/discoverables/openhome.py 0000664 0000000 0000000 00000000742 13450712060 0022151 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.6.0/netdisco/discoverables/panasonic_viera.py 0000664 0000000 0000000 00000000516 13450712060 0023477 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.6.0/netdisco/discoverables/philips_hue.py 0000664 0000000 0000000 00000000731 13450712060 0022646 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.6.0/netdisco/discoverables/plex_mediaserver.py 0000664 0000000 0000000 00000001357 13450712060 0023700 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.6.0/netdisco/discoverables/roku.py 0000664 0000000 0000000 00000000415 13450712060 0021314 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.6.0/netdisco/discoverables/sabnzbd.py 0000664 0000000 0000000 00000000571 13450712060 0021762 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.6.0/netdisco/discoverables/samsung_printer.py 0000664 0000000 0000000 00000000644 13450712060 0023560 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.6.0/netdisco/discoverables/samsung_tv.py 0000664 0000000 0000000 00000001544 13450712060 0022526 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.6.0/netdisco/discoverables/sercomm.py 0000664 0000000 0000000 00000000754 13450712060 0022007 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.6.0/netdisco/discoverables/songpal.py 0000664 0000000 0000000 00000004153 13450712060 0022002 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.6.0/netdisco/discoverables/sonos.py 0000664 0000000 0000000 00000000466 13450712060 0021503 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.6.0/netdisco/discoverables/spotify_connect.py 0000664 0000000 0000000 00000000543 13450712060 0023544 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.6.0/netdisco/discoverables/tellstick.py 0000664 0000000 0000000 00000000613 13450712060 0022332 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.6.0/netdisco/discoverables/tivo_dvr.py 0000664 0000000 0000000 00000000757 13450712060 0022201 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.6.0/netdisco/discoverables/volumio.py 0000664 0000000 0000000 00000000452 13450712060 0022027 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.6.0/netdisco/discoverables/webos_tv.py 0000664 0000000 0000000 00000000703 13450712060 0022164 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.6.0/netdisco/discoverables/wink.py 0000664 0000000 0000000 00000001114 13450712060 0021301 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.6.0/netdisco/discoverables/xbox_smartglass.py 0000664 0000000 0000000 00000000651 13450712060 0023556 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.6.0/netdisco/discoverables/xiaomi_gw.py 0000664 0000000 0000000 00000002350 13450712060 0022317 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.6.0/netdisco/discoverables/yamaha.py 0000664 0000000 0000000 00000002741 13450712060 0021600 0 ustar 00root root 0000000 0000000 """Discover Yamaha Receivers."""
from . import SSDPDiscoverable
class Discoverable(SSDPDiscoverable):
"""Add support for discovering Yamaha Receivers."""
INCOMPATIBLE_MODELS = set('N301')
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('modelNumber', '') not in
self.INCOMPATIBLE_MODELS]
netdisco-2.6.0/netdisco/discoverables/yeelight.py 0000664 0000000 0000000 00000001561 13450712060 0022151 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.6.0/netdisco/discoverables/ziggo_mediabox_xl.py 0000664 0000000 0000000 00000000670 13450712060 0024031 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.6.0/netdisco/discovery.py 0000664 0000000 0000000 00000007735 13450712060 0017532 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):
"""Start and tells scanners to scan."""
self.is_discovering = True
self.mdns = MDNS()
# Needs to be after MDNS init
self._load_device_support()
self.mdns.start()
self.ssdp = SSDP()
self.ssdp.scan()
self.gdm = GDM()
self.gdm.scan()
self.lms = LMS()
self.lms.scan()
self.tellstick = Tellstick()
self.tellstick.scan()
self.daikin = Daikin()
self.daikin.scan()
self.xbox_smartglass = XboxSmartGlass()
self.xbox_smartglass.scan()
def stop(self):
"""Turn discovery off."""
if not self.is_discovering:
return
self.mdns.stop()
# Not removing SSDP because it tracks state
self.mdns = None
self.gdm = None
self.lms = None
self.tellstick = None
self.daikin = None
self.xbox_smartglass = None
self.discoverables = None
self.is_discovering = False
def discover(self):
"""Return a list of discovered devices and services."""
if not self.is_discovering:
raise RuntimeError("Needs to be called after start, before stop")
return [dis for dis, checker in self.discoverables.items()
if checker.is_discovered()]
def get_info(self, dis):
"""Get a list with the most important info about discovered type."""
return self.discoverables[dis].get_info()
def get_entries(self, dis):
"""Get a list with all info about a discovered type."""
return self.discoverables[dis].get_entries()
def _load_device_support(self):
"""Load the devices and services that can be discovered."""
self.discoverables = {}
discoverables_format = __name__.rsplit('.', 1)[0] + '.discoverables.{}'
for module_name in os.listdir(os.path.join(os.path.dirname(__file__),
'discoverables')):
if module_name[-3:] != '.py' or module_name == '__init__.py':
continue
module_name = module_name[:-3]
module = importlib.import_module(
discoverables_format.format(module_name))
self.discoverables[module_name] = \
getattr(module, 'Discoverable')(self)
def print_raw_data(self):
"""Helper method to show what is discovered in your network."""
from pprint import pprint
print("Zeroconf")
pprint(self.mdns.entries)
print("")
print("SSDP")
pprint(self.ssdp.entries)
print("")
print("GDM")
pprint(self.gdm.entries)
print("")
print("LMS")
pprint(self.lms.entries)
print("")
print("Tellstick")
pprint(self.tellstick.entries)
print("")
print("Xbox SmartGlass")
pprint(self.xbox_smartglass.entries)
netdisco-2.6.0/netdisco/gdm.py 0000664 0000000 0000000 00000006533 13450712060 0016265 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.6.0/netdisco/lms.py 0000664 0000000 0000000 00000004404 13450712060 0016304 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.6.0/netdisco/mdns.py 0000664 0000000 0000000 00000002427 13450712060 0016455 0 ustar 00root root 0000000 0000000 """Add support for discovering mDNS services."""
from typing import List # noqa: F401
import zeroconf
class MDNS:
"""Base class to discover mDNS services."""
def __init__(self):
"""Initialize the discovery."""
self.zeroconf = None
self.services = [] # type: List[zeroconf.ServiceInfo]
self._browsers = [] # type: List[zeroconf.ServiceBrowser]
def register_service(self, service):
"""Register a mDNS service."""
self.services.append(service)
def start(self):
"""Start discovery."""
try:
self.zeroconf = zeroconf.Zeroconf()
for service in self.services:
self._browsers.append(zeroconf.ServiceBrowser(
self.zeroconf, service.typ, service))
except Exception: # pylint: disable=broad-except
self.stop()
raise
def stop(self):
"""Stop discovering."""
while self._browsers:
self._browsers.pop().cancel()
for service in self.services:
service.reset()
if self.zeroconf:
self.zeroconf.close()
self.zeroconf = None
@property
def entries(self):
"""Return all entries in the cache."""
return self.zeroconf.cache.entries()
netdisco-2.6.0/netdisco/service.py 0000664 0000000 0000000 00000005027 13450712060 0017153 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.6.0/netdisco/smartglass.py 0000664 0000000 0000000 00000010154 13450712060 0017670 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.6.0/netdisco/ssdp.py 0000664 0000000 0000000 00000021162 13450712060 0016462 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.6.0/netdisco/tellstick.py 0000664 0000000 0000000 00000003766 13450712060 0017521 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.6.0/netdisco/util.py 0000664 0000000 0000000 00000002115 13450712060 0016463 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.6.0/pylintrc 0000664 0000000 0000000 00000000040 13450712060 0015106 0 ustar 00root root 0000000 0000000 [MASTER]
disable=duplicate-code
netdisco-2.6.0/requirements.txt 0000664 0000000 0000000 00000000074 13450712060 0016612 0 ustar 00root root 0000000 0000000 zeroconf>=0.21.0
requests>=2.0
typing; python_version<'3.5'
netdisco-2.6.0/script/ 0000775 0000000 0000000 00000000000 13450712060 0014631 5 ustar 00root root 0000000 0000000 netdisco-2.6.0/script/release 0000775 0000000 0000000 00000000475 13450712060 0016205 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.6.0/setup.cfg 0000664 0000000 0000000 00000000567 13450712060 0015156 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.6.0/setup.py 0000664 0000000 0000000 00000002254 13450712060 0015042 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.6.0',
description='Discover devices on your local network',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/home-assistant/netdisco',
author='Paulus Schoutsen',
author_email='Paulus@PaulusSchoutsen.nl',
license='Apache License 2.0',
install_requires=['requests>=2.0', 'zeroconf>=0.21.0'],
python_requires='>=3',
packages=find_packages(exclude=['tests', 'tests.*']),
zip_safe=False,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX',
'Programming Language :: Python :: 3',
'Topic :: Utilities',
'Topic :: Home Automation',
'Topic :: System :: Networking',
],
)
netdisco-2.6.0/tests/ 0000775 0000000 0000000 00000000000 13450712060 0014467 5 ustar 00root root 0000000 0000000 netdisco-2.6.0/tests/__init__.py 0000664 0000000 0000000 00000000032 13450712060 0016573 0 ustar 00root root 0000000 0000000 """Tests for NetDisco."""
netdisco-2.6.0/tests/discoverables/ 0000775 0000000 0000000 00000000000 13450712060 0017314 5 ustar 00root root 0000000 0000000 netdisco-2.6.0/tests/discoverables/test_yamaha.py 0000664 0000000 0000000 00000011506 13450712060 0022170 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': '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': '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': '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_multiple_services_no_remote_control.xml")
devices = [
supported_model,
MockUPNPEntry("desc_incompatible_device.xml")
]
discoverable = Discoverable(None)
discoverable.INCOMPATIBLE_MODELS = ["aaa"]
discoverable.find_by_device_description = MagicMock(
return_value=devices)
self.assertEqual(discoverable.get_entries(), [supported_model])
netdisco-2.6.0/tests/discoverables/yamaha_files/ 0000775 0000000 0000000 00000000000 13450712060 0021736 5 ustar 00root root 0000000 0000000 netdisco-2.6.0/tests/discoverables/yamaha_files/desc_RX-V481.xml 0000664 0000000 0000000 00000005617 13450712060 0024460 0 ustar 00root root 0000000 0000000
1
0
DMR-1.50
urn:schemas-upnp-org:device:MediaRenderer:1
RX-V481 XXXXXX
Yamaha Corporation
http://www.yamaha.com/
AV Receiver
RX-V481
V481
http://www.yamaha.com/
XXXXXXXX
uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
image/jpeg
48
48
24
/Icons/48x48.jpg
image/jpeg
120
120
24
/Icons/120x120.jpg
image/png
48
48
24
/Icons/48x48.png
image/png
120
120
24
/Icons/120x120.png
urn:schemas-upnp-org:service:AVTransport:1
urn:upnp-org:serviceId:AVTransport
/AVTransport/desc.xml
/AVTransport/ctrl
/AVTransport/event
urn:schemas-upnp-org:service:RenderingControl:1
urn:upnp-org:serviceId:RenderingControl
/RenderingControl/desc.xml
/RenderingControl/ctrl
/RenderingControl/event
urn:schemas-upnp-org:service:ConnectionManager:1
urn:upnp-org:serviceId:ConnectionManager
/ConnectionManager/desc.xml
/ConnectionManager/ctrl
/ConnectionManager/event
http://192.168.XXX.XXX/
http://192.168.XXX.XXX:80/
urn:schemas-yamaha-com:service:X_YamahaRemoteControl:1
/YamahaRemoteControl/ctrl
/YamahaRemoteControl/desc.xml
urn:schemas-yamaha-com:service:X_YamahaExtendedControl:1
/YamahaExtendedControl/v1/
1131