Skip to content
This repository has been archived by the owner on Oct 1, 2021. It is now read-only.

RFC: Introduce yeelight discovery by SSDP on a non standard port (1982) #193

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions netdisco/discoverables/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def find_by_st(self, st):
"""Find entries by ST (the device identifier)."""
return self.netdis.ssdp.find_by_st(st)

def find_by_location(self, location):
"""Find entries by location."""
return self.netdis.ssdp.find_by_location(location)

def find_by_device_description(self, values):
"""Find entries based on values from their description."""
return self.netdis.ssdp.find_by_device_description(values)
Expand Down
29 changes: 14 additions & 15 deletions netdisco/discoverables/yeelight.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
"""Discover Yeelight bulbs, based on Kodi discoverable."""
from . import MDNSDiscoverable
from ..const import ATTR_DEVICE_TYPE
"""Discover Yeelight devices."""
from . import SSDPDiscoverable
from ..const import ATTR_DEVICE_TYPE, ATTR_SERIAL

DEVICE_NAME_PREFIX = 'yeelink-light-'
ATTR_FIRMWARE_VERSION = 'firmware_version'
ATTR_SUPPORT = 'support'


# pylint: disable=too-few-public-methods
class Discoverable(MDNSDiscoverable):
class Discoverable(SSDPDiscoverable):
"""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."""
"""Return most important info from SSDP 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]
# The model doesn't align with the mDNS model names (color vs. color1)
info[ATTR_DEVICE_TYPE] = entry.model
info[ATTR_SERIAL] = entry.id
info[ATTR_FIRMWARE_VERSION] = entry.fw_ver
info[ATTR_SUPPORT] = entry.support

return info

def get_entries(self):
""" Return yeelight devices. """
return self.find_by_device_name(DEVICE_NAME_PREFIX)
"""Get all the Yeelight compliant device SSDP entries."""
return self.find_by_location("yeelight://")
18 changes: 15 additions & 3 deletions netdisco/ssdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# bound by our discovery timeout.
SSDP_MX = DISCOVER_TIMEOUT
SSDP_TARGET = ("239.255.255.250", 1900)
SSDP_TARGET_YEELIGHT = ("239.255.255.250", 1982)

RESPONSE_REGEX = re.compile(r'\n(.*?)\: *(.*)\r')

Expand All @@ -26,6 +27,9 @@
# Devices only, some devices will only respond to this query
ST_ROOTDEVICE = "upnp:rootdevice"

# Required ST of the yeelight SSDP protocol implementation
ST_YEELIGHT = "wifi_bulb"


class SSDP(object):
"""Control the scanning of uPnP devices and services and caches output."""
Expand Down Expand Up @@ -56,6 +60,13 @@ def find_by_st(self, st):
return [entry for entry in self.entries
if entry.st == st]

def find_by_location(self, location):
"""Return a list of entries that match the location."""
self.update()

return [entry for entry in self.entries
if entry.location.startswith(location)]

def find_by_device_description(self, values):
"""Return a list of entries that match the description.

Expand Down Expand Up @@ -195,14 +206,14 @@ def __repr__(self):
return "<UPNPEntry {} - {}>".format(self.location or '', self.st or '')


def ssdp_request(ssdp_st, ssdp_mx=SSDP_MX):
def ssdp_request(ssdp_st, ssdp_mx=SSDP_MX, ssdp_target=SSDP_TARGET):
"""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),
'HOST: {}:{}'.format(*ssdp_target),
'', '']).encode('utf-8')


Expand All @@ -216,7 +227,8 @@ def scan(timeout=DISCOVER_TIMEOUT):
Protocol explanation:
https://embeddedinn.wordpress.com/tutorials/upnp-device-architecture/
"""
ssdp_requests = ssdp_request(ST_ALL), ssdp_request(ST_ROOTDEVICE)
ssdp_requests = ssdp_request(ST_ALL), ssdp_request(ST_ROOTDEVICE), \
ssdp_request(ST_YEELIGHT, ssdp_target=SSDP_TARGET_YEELIGHT)

stop_wait = datetime.now() + timedelta(seconds=timeout)

Expand Down