From 6d956ce80888da1d0a457194497fd89d69762533 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Fri, 27 Dec 2019 08:23:02 +0100 Subject: [PATCH] added custom ssdp discovery --- yeelight/devicepluginyeelight.cpp | 169 ++++++++++++++++++++++------- yeelight/devicepluginyeelight.h | 14 ++- yeelight/devicepluginyeelight.json | 20 +++- yeelight/ssdp.cpp | 130 ++++++++++++++++++++++ yeelight/ssdp.h | 47 ++++++++ yeelight/yeelight.cpp | 91 +++++++++++++++- yeelight/yeelight.h | 17 ++- yeelight/yeelight.pro | 2 + 8 files changed, 432 insertions(+), 58 deletions(-) create mode 100644 yeelight/ssdp.cpp create mode 100644 yeelight/ssdp.h diff --git a/yeelight/devicepluginyeelight.cpp b/yeelight/devicepluginyeelight.cpp index ae84fc3e..061ed9c9 100644 --- a/yeelight/devicepluginyeelight.cpp +++ b/yeelight/devicepluginyeelight.cpp @@ -25,11 +25,11 @@ #include "devices/device.h" #include "types/param.h" #include "plugininfo.h" -#include "network/upnp/upnpdiscovery.h" -#include "network/upnp/upnpdiscoveryreply.h" +#include "ssdp.h" #include #include +#include DevicePluginYeelight::DevicePluginYeelight() { @@ -44,70 +44,157 @@ void DevicePluginYeelight::discoverDevices(DeviceDiscoveryInfo *info) m_discoveries.insert(info, discovery); qCDebug(dcYeelight()) << "Starting UPnP discovery..."; - UpnpDiscoveryReply *upnpReply = hardwareManager()->upnpDiscovery()->discoverDevices("wifi_bulb"); - discovery->upnpReply = upnpReply; - // Always clean up the upnp discovery - connect(upnpReply, &UpnpDiscoveryReply::finished, upnpReply, &UpnpDiscoveryReply::deleteLater); + Ssdp *ssdp = new Ssdp(this); + ssdp->enable(); + ssdp->discover(); + connect(ssdp, &Ssdp::discovered, this,[info, this](const QString &address, int port, int id, const QString &model) { - // Process results if info is still around - connect(upnpReply, &UpnpDiscoveryReply::finished, info, [this, upnpReply, discovery](){ - - // Indicate we're done... - discovery->upnpReply = nullptr; - - if (upnpReply->error() != UpnpDiscoveryReply::UpnpDiscoveryReplyErrorNoError) { - qCWarning(dcYeelight()) << "Upnp discovery error" << upnpReply->error(); - } - - foreach (const UpnpDeviceDescriptor &upnpDevice, upnpReply->deviceDescriptors()) { - if (upnpDevice.modelDescription().contains("color")) { - DeviceDescriptor descriptor(bridgeDeviceClassId, "Yeelight Color Bulb", upnpDevice.hostAddress().toString()); - ParamList params; - QString bridgeId = upnpDevice.serialNumber().toLower(); - foreach (Device *existingDevice, myDevices()) { - if (existingDevice->paramValue(bridgeDeviceIdParamTypeId).toString() == bridgeId) { - descriptor.setDeviceId(existingDevice->id()); - break; - } - } - params.append(Param(bridgeDeviceHostParamTypeId, upnpDevice.hostAddress().toString())); - params.append(Param(bridgeDeviceIdParamTypeId, bridgeId)); - descriptor.setParams(params); - qCDebug(dcYeelight()) << "UPnP: Found Hue bridge:" << bridgeId; - discovery->results.append(descriptor); + DeviceDescriptor descriptor(colorBulbDeviceClassId, "Yeelight"+ model + "Bulb", address); + ParamList params; + foreach (Device *existingDevice, myDevices()) { + if (existingDevice->paramValue(colorBulbDeviceIdParamTypeId).toString() == id) { + descriptor.setDeviceId(existingDevice->id()); + break; } } - //finishDiscovery(discovery); + params.append(Param(colorBulbDeviceHostParamTypeId, address)); + params.append(Param(colorBulbDeviceIdParamTypeId, id)); + params.append(Param(colorBulbDevicePortParamTypeId, port)); + descriptor.setParams(params); + qCDebug(dcYeelight()) << "UPnP: Found Yeelight device:" << id; + info->finish(Device::DeviceErrorNoError); }); - - info->finish(Device::DeviceErrorNoError); } } void DevicePluginYeelight::setupDevice(DeviceSetupInfo *info) { Device *device = info->device(); + if (device->deviceClassId() == colorBulbDeviceClassId) { - QHostAddress *address = device->paramValue(colorBulbDeviceAddressParamTypeId).toString(); - int port; - Yeelight *yeelight = new Yeelight(hardwareManager(), address, port, this); + QHostAddress address = QHostAddress(device->paramValue(colorBulbDeviceHostParamTypeId).toString()); + quint16 port = device->paramValue(colorBulbDevicePortParamTypeId).toUInt(); + Yeelight *yeelight = new Yeelight(hardwareManager()->networkManager(), address, port, this); + m_yeelightConnections.insert(device->id(), yeelight); + connect(yeelight, &Yeelight::connectionChanged, this, &DevicePluginYeelight::onConnectionChanged); + connect(yeelight, &Yeelight::requestExecuted, this, &DevicePluginYeelight::onRequestExecuted); + connect(yeelight, &Yeelight::propertyListReceived, this, &DevicePluginYeelight::onPropertyListReceived); info->finish(Device::DeviceErrorNoError); } } +void DevicePluginYeelight::postSetupDevice(Device *device) +{ + connect(device, &Device::nameChanged, this, &DevicePluginYeelight::onDeviceNameChanged); + + if (device->deviceClassId() == colorBulbDeviceClassId) { + } + + if (!m_pluginTimer) { + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { + foreach (Yeelight *yeelight, m_yeelightConnections.values()) { + if (yeelight->isConnected()) { + QList properties; + properties << Yeelight::Property::Name; + properties << Yeelight::Property::Ct; + properties << Yeelight::Property::Rgb; + properties << Yeelight::Property::Power; + properties << Yeelight::Property::Bright; + properties << Yeelight::Property::ColorMode; + yeelight->getParam(properties); + } else { + yeelight->connectDevice(); + } + } + }); + } +} + void DevicePluginYeelight::executeAction(DeviceActionInfo *info) { Device *device = info->device(); Action action = info->action(); if (device->deviceClassId() == colorBulbDeviceClassId) { + Yeelight *yeelight = m_yeelightConnections.value(device->id()); - // if (action.actionTypeId() == ) { - //} + if (action.actionTypeId() == colorBulbPowerActionTypeId) { + bool power = action.param(colorBulbPowerActionPowerParamTypeId).value().toBool(); + yeelight->setPower(power); + } else if (action.actionTypeId() == colorBulbBrightnessActionTypeId) { + int brightness = action.param(colorBulbBrightnessActionBrightnessParamTypeId).value().toInt(); + yeelight->setBrightness(brightness); + } else if (action.actionTypeId() == colorBulbColorActionColorParamTypeId) { + QRgb color = action.param(colorBulbColorActionColorParamTypeId).value().toUInt(); + yeelight->setRgb(color); + } else if (action.actionTypeId() == colorBulbColorTemperatureActionTypeId) { + int colorTemperature = action.param(colorBulbColorTemperatureActionColorTemperatureParamTypeId).value().toUInt() * 11.12; + yeelight->setColorTemperature(colorTemperature); + } } } void DevicePluginYeelight::deviceRemoved(Device *device) { - + if (device->deviceClassId() == colorBulbDeviceClassId){ + Yeelight *yeelight = m_yeelightConnections.take(device->id()); + yeelight->deleteLater(); + } +} + +void DevicePluginYeelight::onDeviceNameChanged() +{ + Device *device = static_cast(sender()); + Yeelight *yeelight = m_yeelightConnections.value(device->id()); + yeelight->setName(device->name()); +} + +void DevicePluginYeelight::onConnectionChanged(bool connected) +{ + Yeelight *yeelight = static_cast(sender()); + Device * device = myDevices().findById(m_yeelightConnections.key(yeelight)); + if(!device) + return; + + device->setStateValue(colorBulbConnectedStateTypeId, connected); +} + +void DevicePluginYeelight::onRequestExecuted(int requestId, bool success) +{ + if (m_asyncActions.contains(requestId)) { + DeviceActionInfo *info = m_asyncActions.take(requestId); + if (success) { + info->finish(Device::DeviceErrorNoError); + } else { + info->finish(Device::DeviceErrorHardwareFailure); + } + } +} + +void DevicePluginYeelight::onPropertyListReceived(QVariantList value) +{ + Yeelight *yeelight = static_cast(sender()); + Device * device = myDevices().findById(m_yeelightConnections.key(yeelight)); + if(!device) + return; + + /* + * properties << Yeelight::Property::Name; + * properties << Yeelight::Property::Ct; + * properties << Yeelight::Property::Rgb; + * properties << Yeelight::Property::Power; + * properties << Yeelight::Property::Bright; + * properties << Yeelight::Property::ColorMode; + */ + + if(value.length() < 6) + return; + + device->setName(value[0].toString()); + device->setStateValue(colorBulbColorTemperatureStateTypeId, value[1].toInt()); + device->setStateValue(colorBulbColorStateTypeId, value[2].toString()); + device->setStateValue(colorBulbPowerStateTypeId, (value[3].toString() == "on")); + device->setStateValue(colorBulbBrightnessStateTypeId, value[4].toInt()); + device->setStateValue(colorBulbColorModeStateTypeId, value[5].toString()); } diff --git a/yeelight/devicepluginyeelight.h b/yeelight/devicepluginyeelight.h index ec34c1c1..dc4a3e4d 100644 --- a/yeelight/devicepluginyeelight.h +++ b/yeelight/devicepluginyeelight.h @@ -43,6 +43,7 @@ public: void discoverDevices(DeviceDiscoveryInfo *info) override; void setupDevice(DeviceSetupInfo *info) override; + void postSetupDevice(Device *device) override; void executeAction(DeviceActionInfo *info) override; void deviceRemoved(Device *device) override; @@ -55,15 +56,16 @@ private: }; QHash m_discoveries; QList m_usedInterfaces; - QTimer *m_reconnectTimer = nullptr; + + PluginTimer *m_pluginTimer = nullptr; QHash m_asyncActions; + QHash m_yeelightConnections; private slots: - void onReadyRead(); - void onReconnectTimer(); - -signals: - + void onDeviceNameChanged(); + void onConnectionChanged(bool connected); + void onRequestExecuted(int requestId, bool success); + void onPropertyListReceived(QVariantList value); }; #endif // DEVICEPLUGINYEELIGHT_H diff --git a/yeelight/devicepluginyeelight.json b/yeelight/devicepluginyeelight.json index c0d23219..f71da70b 100644 --- a/yeelight/devicepluginyeelight.json +++ b/yeelight/devicepluginyeelight.json @@ -23,6 +23,13 @@ "inputType": "IPv4Address", "readOnly": true }, + { + "id": "99790910-ce6c-4b14-bb1f-f56133522dce", + "name": "port", + "displayName": "Port", + "type" : "int", + "readOnly": true + }, { "id": "bfe184d2-6e62-4979-8ff2-3fd190903c82", "name": "id", @@ -35,8 +42,7 @@ { "id": "e80aea31-7ac0-4f71-9650-efb70ea1e768", "name": "default", - "displayName": "Set default", - "type": "bool" + "displayName": "Set default" } ], "stateTypes": [ @@ -93,6 +99,16 @@ "minValue": 0, "maxValue": 100, "writable": true + }, + { + "id": "6babaf04-0247-447c-b429-d39f3d330f45", + "name": "colorMode", + "displayName": "Color mode", + "displayNameEvent": "Color mode changed", + "displayNameAction": "Set color mode", + "type": "QString", + "defaultValue": "-", + "writable": true } ] } diff --git a/yeelight/ssdp.cpp b/yeelight/ssdp.cpp new file mode 100644 index 00000000..baaa23f4 --- /dev/null +++ b/yeelight/ssdp.cpp @@ -0,0 +1,130 @@ +#include "ssdp.h" +#include "extern-plugininfo.h" + +#include +#include +#include +#include +#include +#include + +Ssdp::Ssdp(QObject *parent) : QObject(parent) +{ + +} + +Ssdp::~Ssdp() +{ + if (m_socket) { + m_socket->waitForBytesWritten(1000); + m_socket->close(); + } +} + +bool Ssdp::enable() +{ + // Clean up + if (m_socket) { + delete m_socket; + m_socket = nullptr; + } + + // Bind udp socket and join multicast group + m_socket = new QUdpSocket(this); + m_port = 1982; + m_host = QHostAddress("239.255.255.250"); + + m_socket->setSocketOption(QAbstractSocket::MulticastTtlOption,QVariant(1)); + m_socket->setSocketOption(QAbstractSocket::MulticastLoopbackOption,QVariant(1)); + + if(!m_socket->bind(QHostAddress::AnyIPv4, m_port, QUdpSocket::ShareAddress)){ + qCWarning(dcYeelight()) << "could not bind to port" << m_port; + m_available = false; + delete m_socket; + m_socket = nullptr; + return false; + } + + if(!m_socket->joinMulticastGroup(m_host)){ + qCWarning(dcYeelight()) << "could not join multicast group" << m_host; + m_available = false; + delete m_socket; + m_socket = nullptr; + return false; + } + connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(error(QAbstractSocket::SocketError))); + connect(m_socket, &QUdpSocket::readyRead, this, &Ssdp::readData); + return true; +} + +void Ssdp::discover() +{ + QByteArray searchMessage = QByteArray("M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1982\r\n" + "MAN: \"ssdp:discover\"\r\n" + "ST: wifi_bulb\r\n\r\n"); + m_socket->writeDatagram(searchMessage, m_host, m_port); +} + + +void Ssdp::error(QAbstractSocket::SocketError error) +{ + qCWarning(dcYeelight()) << "socket error:" << error << m_socket->errorString(); +} + +void Ssdp::readData() +{ + QByteArray data; + quint16 port; + QHostAddress hostAddress; + QUrl location; + int id; + QString model; + + // read the answere from the multicast + while (m_socket->hasPendingDatagrams()) { + data.resize(m_socket->pendingDatagramSize()); + m_socket->readDatagram(data.data(), data.size(), &hostAddress, &port); + } + qCDebug(dcYeelight())<< "SSDP message received " << data; + + if (data.contains("NOTIFY") && !QNetworkInterface::allAddresses().contains(hostAddress)) { + return; + } + + // if the data contains the HTTP OK header... + if (data.contains("HTTP/1.1 200 OK")) { + const QStringList lines = QString(data).split("\r\n"); + foreach (const QString& line, lines) { + int separatorIndex = line.indexOf(':'); + QString key = line.left(separatorIndex).toUpper(); + QString value = line.mid(separatorIndex+1).trimmed(); + + if (key.contains("Location")) { + location = QUrl(value); + } else if (key.contains("id")) { + id = value.toUInt(); + } else if (key.contains("model")) { + + } + } + emit discovered(location.host(), location.port(), id, model); + } +} + + +bool Ssdp::disable() +{ + if (!m_socket) { + return false; + } + + m_socket->waitForBytesWritten(); + m_socket->close(); + delete m_socket; + m_socket = nullptr; + + m_notificationTimer->stop(); + return true; +} + diff --git a/yeelight/ssdp.h b/yeelight/ssdp.h new file mode 100644 index 00000000..da7fbb12 --- /dev/null +++ b/yeelight/ssdp.h @@ -0,0 +1,47 @@ +#ifndef SSDP_H +#define SSDP_H + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class Ssdp : public QObject +{ + Q_OBJECT +public: + explicit Ssdp(QObject *parent = nullptr); + ~Ssdp(); + bool enable(); + void discover(); + bool disable(); +private: + QUdpSocket *m_socket = nullptr; + QHostAddress m_host; + quint16 m_port; + + QTimer *m_notificationTimer = nullptr; + + QNetworkAccessManager *m_networkAccessManager = nullptr; + + //QList m_discoverRequests; + // QHash m_informationRequestList; + + bool m_available = false; + bool m_enabled = false; + +signals: + void discovered(const QString &address, int port, int id, const QString &model); + +private slots: + void error(QAbstractSocket::SocketError error); + void readData(); +}; + +#endif // SSDP_H diff --git a/yeelight/yeelight.cpp b/yeelight/yeelight.cpp index b1f536fd..5080403c 100644 --- a/yeelight/yeelight.cpp +++ b/yeelight/yeelight.cpp @@ -28,6 +28,7 @@ #include #include +#include #include Yeelight::Yeelight(NetworkAccessManager *networkManager, const QHostAddress &address, quint16 port, QObject *parent) : @@ -38,14 +39,20 @@ Yeelight::Yeelight(NetworkAccessManager *networkManager, const QHostAddress &add { m_socket = new QTcpSocket(this); connect(m_socket, &QTcpSocket::stateChanged, this, &Yeelight::onStateChanged); + connect(m_socket, &QTcpSocket::readyRead, this, &Yeelight::onReadyRead); m_socket->connectToHost(address, port); } -bool Yeelight::connected() +bool Yeelight::isConnected() { return m_socket->isOpen(); } +void Yeelight::connectDevice() +{ + m_socket->connectToHost(m_address, m_port); +} + int Yeelight::getParam(QList properties) { @@ -62,7 +69,7 @@ int Yeelight::getParam(QList properties) params.append("ct"); break; case Property::Power: - params.append("powr"); + params.append("power"); break; case Property::Bright: params.append("bright"); @@ -132,7 +139,8 @@ int Yeelight::getParam(QList properties) obj["params"] = params; doc.setObject(obj); - m_socket->write(doc.toJson()); + qCDebug(dcYeelight()) << "Sending request" << doc.toJson(); + m_socket->write(doc.toJson() + "\r\n"); return requestId; } @@ -146,6 +154,9 @@ int Yeelight::setName(const QString &name) QJsonArray params; params.append(name); obj["params"] = params; + doc.setObject(obj); + qCDebug(dcYeelight()) << "Sending request" << doc.toJson(); + m_socket->write(doc.toJson() + "\r\n"); return requestId; } @@ -161,6 +172,9 @@ int Yeelight::setColorTemperature(int mirad, int msFadeTime) params.append("smooth"); params.append(msFadeTime); obj["params"] = params; + doc.setObject(obj); + qCDebug(dcYeelight()) << "Sending request" << doc.toJson(); + m_socket->write(doc.toJson() + "\r\n"); return requestId; } @@ -176,6 +190,9 @@ int Yeelight::setRgb(QRgb color, int msFadeTime) params.append("smooth"); params.append(msFadeTime); obj["params"] = params; + doc.setObject(obj); + qCDebug(dcYeelight()) << "Sending request" << doc.toJson(); + m_socket->write(doc.toJson() + "\r\n"); return requestId; } @@ -191,6 +208,9 @@ int Yeelight::setBrightness(int percentage, int msFadeTime) params.append("smooth"); params.append(msFadeTime); obj["params"] = params; + doc.setObject(obj); + qCDebug(dcYeelight()) << "Sending request" << doc.toJson(); + m_socket->write(doc.toJson() + "\r\n"); return requestId; } @@ -210,6 +230,9 @@ int Yeelight::setPower(bool power, int msFadeTime) params.append("smooth"); params.append(msFadeTime); obj["params"] = params; + doc.setObject(obj); + qCDebug(dcYeelight()) << "Sending request" << doc.toJson(); + m_socket->write(doc.toJson() + "\r\n"); return requestId; } @@ -222,6 +245,9 @@ int Yeelight::setDefault() obj["method"] = "set_default"; QJsonArray params; obj["params"] = params; + doc.setObject(obj); + qCDebug(dcYeelight()) << "Sending request" << doc.toJson(); + m_socket->write(doc.toJson() + "\r\n"); return requestId; } @@ -229,11 +255,68 @@ void Yeelight::onStateChanged(QAbstractSocket::SocketState state) { switch (state) { case QAbstractSocket::SocketState::ConnectedState: - + emit connectionChanged(true); break; default: + emit connectionChanged(false); break; } } +void Yeelight::onReadyRead() +{ + QByteArray data = m_socket->readAll(); + qCDebug(dcYeelight()) << "Message received" << data; + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) { + qDebug(dcYeelight()) << "Recieved invalide JSON object"; + return; + } + QVariantMap map = doc.toVariant().toMap(); + if (map.contains("method")) { + if (map["method"] == "props") { + QVariantMap params = map["params"].toMap(); + if (params.contains("power")) { + emit notificationReveiced(Property::Power, params["power"]); + } + if (params.contains("bright")) { + emit notificationReveiced(Property::Bright, params["bright"]); + } + if (params.contains("ct")) { + emit notificationReveiced(Property::Ct, params["ct"]); + } + if (params.contains("rgb")) { + emit notificationReveiced(Property::Rgb, params["rgb"]); + } + if (params.contains("hue")) { + emit notificationReveiced(Property::Hue, params["hue"]); + } + if (params.contains("name")) { + emit notificationReveiced(Property::Name, params["name"]); + } + if (params.contains("color_mode")) { + emit notificationReveiced(Property::ColorMode, params["color_mode"]); + } + if (params.contains("sat")) { + emit notificationReveiced(Property::Sat, params["sat"]); + } + } + } else { + int id = map["id"].toInt(); + QVariantList result = map["result"].toList(); + if ((result.length() == 1)) { + if (result.first().toString() == "ok") { + emit requestExecuted(id, true); + } else { + //TODO parse error + //emit errorReceived() + } + } else { + emit propertyListReceived(result); + } + } +} + diff --git a/yeelight/yeelight.h b/yeelight/yeelight.h index 092bb7ff..4730576d 100644 --- a/yeelight/yeelight.h +++ b/yeelight/yeelight.h @@ -37,6 +37,12 @@ class Yeelight : public QObject { Q_OBJECT public: + enum ColorMode { + RGB = 1, + Temperature, + HSV + }; + enum Property { Power, //on: smart LED is turned on / off: smart LED is turned off Bright, //Brightness percentage. Range 1 ~ 100 @@ -64,7 +70,8 @@ public: }; explicit Yeelight(NetworkAccessManager *networkManager, const QHostAddress &address, quint16 port = 55443, QObject *parent = nullptr); - bool connected(); + bool isConnected(); + void connectDevice(); int getParam(QList properties); int setName(const QString &name); @@ -79,17 +86,17 @@ private: QTcpSocket *m_socket = nullptr; QHostAddress m_address; - qint16 m_port; + quint16 m_port; NetworkAccessManager *m_networkManager = nullptr; private slots: - void onRefreshTimeout(); void onStateChanged(QAbstractSocket::SocketState state); + void onReadyRead(); signals: void connectionChanged(bool connected); - void authenticationStatusChanged(bool authenticated); void requestExecuted(int requestId, bool success); - void propertyReceived(Property property, QVariant value); + void propertyListReceived(QVariantList value); + void notificationReveiced(Property property, QVariant value); }; #endif // YEELIGHT_H diff --git a/yeelight/yeelight.pro b/yeelight/yeelight.pro index 780b952c..1de5ae03 100644 --- a/yeelight/yeelight.pro +++ b/yeelight/yeelight.pro @@ -7,7 +7,9 @@ TARGET = $$qtLibraryTarget(nymea_devicepluginyeelight) SOURCES += \ devicepluginyeelight.cpp \ yeelight.cpp \ + ssdp.cpp HEADERS += \ devicepluginyeelight.h \ yeelight.h \ + ssdp.h