Package: sddp-discovery-protocol
sddp-discovery-protocol: Protocol library for Control4's Simple Device Discovery Protocol (SDDP)A command-line tool and API for servers and clients of Control4's Simple Device Discovery Protocol (SDDP).Table of contentsIntroductionInstallationUsageCommand lineAPIKnown issues and limitationsGetting helpContributingLicenseAuthors and historyIntroductionSimple Device Discovery Protocol (SDDP) is a simple multicast discovery protocol implemented by many "smart
home" devices to allow a controlling agent to easily discover and connect to devices on a local subnet.SDDP was created byControl4, a provider of whole-home automation systems. While
it is quite similar to UPnP's standard Simple Service Discovery Protocol
(SSDP--note the similar but different spelling),
and it serves a virtually identical purpose, SDDP isnota standard protocol and it is not publicly
documented. The protocol as implemented by this package was inferred through observation of network trafficServersControllable smart devices implement the server side of the SDDP protocol. They listen on a well-known
UDP multicast address 239.255.255.250:1902, and do the following:Periodically (typically every 20 minutes) send an SDDP NOTIFY packet to the multicast address
advertising their presence. An example of such a packet is:NOTIFY ALIVE SDDP/1.0\r\n
From: "192.168.4.237:1902"\r\n
Host: "JVC_PROJECTOR-E0DADC152802"\r\n
Max-Age: 1800\r\n
Type: "JVCKENWOOD:Projector"\r\n
Primary-Proxy: "projector"\r\n
Proxies: "projector"\r\n
Manufacturer: "JVCKENWOOD"\r\n
Model: "DLA-RS3100_NZ8"\r\n
Driver: "projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i"\r\nNote that, as with all SDDP packets, there is a statement line, followed by some header lines, each separated with "\r\n". Formatting of the headers is with the same rules as HTTP headers. All string
header values are enclosed in double quotes. This package assumes that json.loads() will correctly
decode all header values.Receive SDDP NOTIFY packets sent to the multicast address from other devices. Most devices
will probably discard these unless they are interested in seeing other devices come and goReceive and respond to SDDP SEARCH packets sent to the multicast address by clients that are performing discovery.
An example of such a search packet is:SEARCH * SDDP/1.0\r\n
Host: "192.168.4.237:24378"\r\nThe device will respond to a valid SEARCH request packet with a SEARCH response packet sent
from their own unicast address on port 1902 to the unicast address from which the request came.
The response looks very much like the NOTIFY packet, but the statement line is different; e.g.,:SDDP/1.0 200 OK\r\n
From: "192.168.4.237:1902"\r\n
Host: "JVC_PROJECTOR-E0DADC152802"\r\n
Max-Age: 1800\r\n
Type: "JVCKENWOOD:Projector"\r\n
Primary-Proxy: "projector"\r\n
Proxies: "projector"\r\n
Manufacturer: "JVCKENWOOD"\r\n
Model: "DLA-RS3100_NZ8"\r\n
Driver: "projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i"\r\nClientsApps that wish to discover devices on the local subnet implement the client side of SDDP.
They send from and receive on an arbitrary dynamic UDP port on their unicast address.To perform discovery, an SDDP client sends an SDDP SEARCH packet from its unicast address
and port to to the SDDP multicast address 239.255.255.250:1902; e.g.:SEARCH * SDDP/1.0\r\n
Host: "192.168.4.237:24378"\r\nwhere the 'Host' header is set to the client's unicast address and port.Each SDDP server device on the network will immediately respond to the SEARCH packet
with a SEARCH response packet sent directly to the client's unicast address and port; e.g.,:SDDP/1.0 200 OK\r\n
From: "192.168.4.237:1902"\r\n
Host: "JVC_PROJECTOR-E0DADC152802"\r\n
Max-Age: 1800\r\n
Type: "JVCKENWOOD:Projector"\r\n
Primary-Proxy: "projector"\r\n
Proxies: "projector"\r\n
Manufacturer: "JVCKENWOOD"\r\n
Model: "DLA-RS3100_NZ8"\r\n
Driver: "projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i"\r\nThe client will then collect the responses from all server devices and use them
for discovery.The client waits for an arbitrary period of time (by default, in this package, 3 seconds)
before assuming that all devices have responded.PackagePython packagesddp-discovery-protocolprovides a command-line tool as well as a runtime API for implementing the client and server sides of SDDP.Some key features of sddp-discovery-protocol:Cross-platform (Linux, MacOS, Windows)Fully async model with coroutine pattern (no synchronous callbacks).Both client and server supports multi-homed operation (multiple network adapters)Server customizes 'From' header for eachServer supports periodic multicast NOTIFY advertisement and responds to SEARCH queriesServer supports collection of NOTIFY advertisements from other SDDP servers on the subnetClient waits for a configurable time for SEARCH responses'sddp' command-line tool allow performing searches as well as running a standalone serverInstallationPrerequisitesPython: Python 3.8+ is required. See your OS documentation for instructions.From PyPiThe current released version ofsddp-discovery-protocolcan be installed withpip3installsddp-discovery-protocolFrom GitHubPoetryis required; it can be installed with:curl-sSLhttps://install.python-poetry.org|python3-Clone the repository and install sddp-discovery-protocol into a private virtualenv with:cd<parent-folder>
gitclonehttps://github.com/sammck/sddp-discovery-protocol.gitcdsddp-discovery-protocol
poetryinstallYou can then launch a bash shell with the virtualenv activated using:poetryshellUsageCommand LineThere is a single command toolsddpthat is installed with the package.Getting the package version$sddpversion0.1.0
$Discovering devices on the local subnet$sddpsearch--help
usage:sddpsearch[-h][--patternPATTERN][--wait-timeWAIT_TIME][-bBIND_ADDRESSES][--include-error-responses]SearchforSDDPdevices

options:-h,--helpshowthishelpmessageandexit--patternPATTERNThedevicepatterntosearchfor.Default:"*"--wait-timeWAIT_TIMETheamountoftimetowaitforresponses,inseconds.Default:3.0-bBIND_ADDRESSES,--bindBIND_ADDRESSESThelocalunicastIPaddresstobindto.Mayberepeated.Default:alllocalnon-loopbackunicastaddresses.--include-error-responsesIncludeerrorresponsesintheoutput.Default:False
$sddpsearch[{"headers":{"Driver":"repeater_ip_lutron_radiora2.c4i","From":"192.168.4.201:1902","Host":"lutron-032e345c","Manufacturer":"Lutron","Max-Age":1800,"Model":"RadioRA2 Main Repeater","Primary-Proxy":"repeater_ip_lutron_radiora2","Proxies":"repeater_ip_lutron_radiora2","Type":"lutron:repeater_ip_lutron_radiora2"},"local_addr":"192.168.4.238:59060","monotonic_time":200338.359314361,"sddp_version":"1.0","src_addr":"192.168.4.64:54838","status":"OK","status_code":200,"utc_time":"2023-07-24T00:13:30.734784"},{"headers":{"Driver":"sonos.c4z","From":"192.168.4.183:1902","Host":"Sonos-347E5CD9C83A","Manufacturer":"Sonos","Max-Age":1800,"Model":"Zoneplayer","Primary-Proxy":"media_service","Proxies":"media_service,amplifier","Type":"sonos:Zoneplayer"},"local_addr":"192.168.4.238:59060","monotonic_time":200338.474668528,"sddp_version":"1.0","src_addr":"192.168.4.183:55292","status":"OK","status_code":200,"utc_time":"2023-07-24T00:13:30.850138"}]If you know the expected value of one or more response headers and are looking for a specific response, you
can speed up the search in the successful case by applying a header filter and limiting the responses
to 1; in this case the search will terminate as soon as the relevant response is received:$sddpsearch--max-responses1-FType=JVCKENWOOD:Projector{"headers":{"Driver":"projector_JVCKENWOOD_DLA-RS3100_NZ8.c4i","From":"192.168.4.237:1902","Host":"JVC_PROJECTOR-E0DADC152802","Manufacturer":"JVCKENWOOD","Max-Age":1800,"Model":"DLA-RS3100_NZ8","Primary-Proxy":"projector","Proxies":"projector","Type":"JVCKENWOOD:Projector"},"local_addr":"192.168.4.198:58941","monotonic_time":0.093766125,"sddp_version":"1.0","src_addr":"192.168.4.237:1902","status":"OK","status_code":200,"utc_time":"2023-07-24T22:23:54.175783"}Running an SDDP server$sddpserver--help
usage:sddpserver[-h][--advertise-intervalADVERTISE_INTERVAL][-HHEADERS][-bBIND_ADDRESSES]RunanSDDPserver

options:-h,--helpshowthishelpmessageandexit--advertise-intervalADVERTISE_INTERVALTheintervalatwhichtosenddeviceadvertisements,inseconds.Default:2/3ofMax-Ageheader,or1200seconds(20minutes)-HHEADERS,--headerHEADERSA<name>=<value>headertoincludeinthedeviceadvertisement.Mayberepeated.-bBIND_ADDRESSES,--bindBIND_ADDRESSESThelocalunicastIPaddresstobindto.Mayberepeated.Default:alllocalnon-loopbackunicastaddresses.
$sddp--log-level=debugserver
...APIDiscovering devices on the local subnetimportloggingimportasyncioimportsddp_discovery_protocolassddp#logging.basicConfig(level=logging.DEBUG)asyncdefamain():# all parameters to SddpClient are optional; they allow you to set the IP addresses to bind to, etc.asyncwithsddp.SddpClient()asclient:# Entering the client.search() context manager sends the search multicast request and reliably collects responses.# Parameters are optional; they allow you to set search filters, max wait time, max returned responses, etc.asyncwithclient.search()assearch_request:# search_request.iter_responses() is an async generator that yields SddpResponseInfo objects# as they come in until the max wait time has elapsed or the max number of responses has been received.asyncforresponse_infoinsearch_request.iter_responses():print(response_info.datagram)# It is possible to exit the loop early here if you found what you're looking forloop=asyncio.new_event_loop()try:asyncio.set_event_loop(loop)loop.run_until_complete(amain())finally:loop.close()Running an SDDP serverimportloggingimportasyncioimportsddp_discovery_protocolassddplogging.basicConfig(level=logging.DEBUG)device_headers={"Type":"Acme:TestDevice","Primary-Proxy":"test-device","Proxies":"test-device","Manufacturer":"Acme","Model":"TestDevPlus","Driver":"test-device_Acme_TestDevPlus.c4i",}asyncdefamain():# The SddpServerContext manager starts the server listening on the multicast port, sending out advertisements,# and responding to search requests.  When the context manager exits, the server will be stopped.asyncwithsddp.SddpServer(device_headers=device_headers)asserver:# This will wait forever unless another task stops the serverawaitserver.wait_for_done()loop=asyncio.new_event_loop()try:asyncio.set_event_loop(loop)loop.run_until_complete(amain())finally:loop.close()Known issues and limitationsSDDP is a proprietary protocol defined byControl4with responders implemented by its
smart-home business partners. The protocol is not standard and is not publicly documented. This
package is a best effort to provide an open implementation of the protocol based on limited observation of
network traffic. There is no way to know if the implementation is complete or totally accurate.Getting helpPlease report any problems/issueshere.ContributingPull requests welcome.Licensesddp-discovery-protocol is distributed under the terms of theMIT License.
The license applies to this file and other files in theGitHub repositoryhosting this file.Authors and historyThe author of sddp-discovery-protocol isSam McKelvie.