Skip to content

Commit

Permalink
Merge branch 'v0.3-freeze' into devel
Browse files Browse the repository at this point in the history
  • Loading branch information
s-kostyuk committed May 4, 2018
2 parents 62f3aa0 + 3d394e1 commit 0df1de1
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 1 deletion.
62 changes: 62 additions & 0 deletions docs/source/api/local_discovery.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
Local network discovery
=======================

General information
-------------------

Starting from v0.3 of the platform all everpl instances (everpl hubs)
are able to be discovered in a local network by default.

Hubs announce their presence and can be discovered using a Zeroconf
(Avahi/Bonjour) protocol - Zero Configuration Networking protocol.
This protocol allows services to announce their presence in the system,
to assign constant domain names in a ".local" domain zone, to resolve
such domain names and to look for a specific service in the system.

For more information about Zeroconf you can read an article on Medium
titled `"Bonjour Android, it’s Zeroconf"`_. It tells about Zeroconf
protocols in general, about Bonjour/Avahi approach and how it relates
with client applications and service discovery.

.. _`"Bonjour Android, it’s Zeroconf"`:
https://medium.com/@_tiwiz/bonjour-android-its-zeroconf-8e3d3fde760e

Unfortunately, Zeroconf (and UDP multicast in general) isn't supported
by modern web browsers.

For more detailed information see:

- DNS-SD protocol website: `<http://www.dns-sd.org/>`_, covers service
discovery part of functionality;
- mDNS protocol website: `<http://www.multicastdns.org/>`_, covers
domain name association in ".local" domain zone;
- and corresponding RFCs.

For testing purposes you can use such handy tools as:

- Avahi-Discover GUI utility for Linux:
https://linux.die.net/man/1/avahi-discover
- Service browser for Android:
https://play.google.com/store/apps/details?id=com.druk.servicebrowser&hl=en_US
- dns-sd CLI tool for macOS

How to discover an everpl hub
-----------------------------

In order to discover an everpl hub you need to use one of the Zeroconf
libraries (like build-in NSD for Android) and search for a service type
``_everpl._tcp``. By default such devices will have a name defined as
"everpl hub @ hostname". To access an everpl REST API on a device you
can use name and port, defined in Hostname (Server) and Port fields of
a discovery response correspondingly.

Here is an example of a complete discovery response (as displayed by
console avahi-browse utility):

::

= virbr0 IPv4 everpl hub @ hostname_was_here _everpl._tcp local
hostname = [hostname_was_here.local]
address = [192.168.20.1]
port = [10800]
txt = []
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Platform itself is hosted on GitHub: https://github.com/s-kostyuk/adpl/
:name: api-docs

api/api_types
api/local_discovery
api/rest_api
api/handling_errors
api/streaming_api
Expand Down
75 changes: 75 additions & 0 deletions dpl/api/local_announce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
This module contains an implementation of everpl announcement logic for
local networks
"""

import socket

import zeroconf


class LocalAnnounce(object):
"""
This class controls the announcement of everpl instance presence in the
local network using Zeroconf (Avahi / Bonjour) protocol
"""
_service_type = "_everpl._tcp.local."

def __init__(self):
"""
Initializes Zeroconf instance and starts corresponding threads
"""
self._zeroconf = zeroconf.Zeroconf()

def create_server(
self, instance_name: str = None,
address: str = "127.0.0.1", port: str = 10800,
server_host: str = None
) -> None:
"""
Initializes zeroconf service (everpl hub) announcement with the
specified optional params
:param instance_name: the name of this service (instance, hub);
set to the "everpl hub @ hostname._everpl.tcp" by default
:param address: an IP address of this instance; set to the 127.0.0.1
by default
:param port: the port of announced service; set to the 10800 by default
:param server_host: hostname of this instance; set to the
"hostname.local" by if not set specifically (if server_host is
None or is equal to the "0.0.0.0" address)
:return: None
"""
hostname = socket.gethostname()

if instance_name is None:
instance_name = "everpl hub @ %s.%s" % (
hostname, self._service_type
)

coded_address = socket.inet_aton(address)

if server_host is None or server_host == "0.0.0.0":
server_host = "%s.local" % hostname

txt_properties = {}

service_info = zeroconf.ServiceInfo(
type_=self._service_type,
name=instance_name,
address=coded_address,
port=port,
properties=txt_properties,
server=server_host
)

self._zeroconf.register_service(service_info)

def shutdown_server(self) -> None:
"""
Unregisters everpl zeroconf service. Stops announcement of this hub
:return: None
"""
self._zeroconf.unregister_all_services()
self._zeroconf.close()
31 changes: 31 additions & 0 deletions dpl/core/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
from dpl.api.rest_api.messages_subapp import build_messages_subapp
from dpl.api.rest_api.rest_api_provider import RestApiProvider

from dpl.api.local_announce import LocalAnnounce

module_logger = logging.getLogger(__name__)
dpl_root_logger = logging.getLogger(name='dpl')

Expand Down Expand Up @@ -179,6 +181,8 @@ def __init__(self):
auth_service=self._auth_service
)

self._local_announce = LocalAnnounce()

def parse_arguments(self):
"""
Parses command-line arguments and alters everpl configuration
Expand Down Expand Up @@ -288,6 +292,32 @@ async def _start_apis(self):
if 'rest_api' in enabled_apis:
await self._start_rest_api()

if 'local_announce' in enabled_apis:
self._start_local_announce()

def _start_local_announce(self):
local_announce_config = self._apis_config['local_announce']
rest_api_config = self._apis_config.get('rest_api', dict())

if local_announce_config['use_rest_host']:
assert 'rest_api' in self._apis_config
host = rest_api_config['host']
else:
host = local_announce_config['host']

if local_announce_config['use_rest_port']:
assert 'rest_api' in self._apis_config
port = rest_api_config['port']
else:
port = local_announce_config['port']

self._local_announce.create_server(
instance_name=None, # use default
# address=, # use default
port=port, # use default REST API listening port
server_host=host # use REST API listening hostname
)

async def _start_rest_api(self):
"""
Starts REST API server
Expand Down Expand Up @@ -321,4 +351,5 @@ async def _bootstrap_integrations(self):

async def shutdown(self):
await self._rest_api.shutdown_server()
self._local_announce.shutdown_server()
self._thing_service_raw.disable_all()
19 changes: 19 additions & 0 deletions dpl/internal_config/default_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ core: # This section contains configuration for the core of everpl
apis: # This section contains configuration of API providers
enabled_apis: # A list of APIs to be enabled
- 'rest_api'
- 'local_announce'

rest_api: # This section configures parameters of the default API
# hostname or an IP address used for listening; set to '0.0.0.0'
Expand All @@ -50,6 +51,24 @@ apis: # This section contains configuration of API providers
# is non-secured (non-TLS) connection is forbidden
is_strict_tls: false

local_announce: # This section allows to override default parameters of
# the Zeroconf (Avahi) announcement.
# By default REST API params will be used
# use the host name specified in 'host' section of REST API configuration
# for service announcement
use_rest_host: true

# use the port specified in 'port' section of REST API configuration
# for service announcement
use_rest_port: true

# optional parameters; are applied if and only if the use_rest_host
# and use_rest_port parameters correspondingly are set to 'false';
# allow specify the host name and port for service announcement explicitly
host: null # allows to set a host explicitly, use 'null' value or
# "0.0.0.0" string to use a default (Zeroconf-assigned) host name
port: 10800 # allows to set a port explicitly; only numbers allowed


integrations: # This section contains configuration of Integrations
# a list of names of Integrations to be enabled; minus (-) sign
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ passlib
sqlalchemy >= 1.1 # Used to store and manage persistent data like settings and Placements
appdirs # to determine where user .config dir is located
pyyaml # to read configuration file in YAML
zeroconf # to announce itself in the local network
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
# Similar to `install_requires` above, these must be valid existing
# projects.
extras_require={ # Optional

'discovery': ['zeroconf']
},

# If there are data files included in your packages that need to be
Expand Down

0 comments on commit 0df1de1

Please sign in to comment.