added custom ssdp discovery

This commit is contained in:
Boernsman 2019-12-27 08:23:02 +01:00
parent e335524421
commit 6d956ce808
8 changed files with 432 additions and 58 deletions

View File

@ -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 <QDebug>
#include <QColor>
#include <QRgb>
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<Yeelight::Property> 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<Device*>(sender());
Yeelight *yeelight = m_yeelightConnections.value(device->id());
yeelight->setName(device->name());
}
void DevicePluginYeelight::onConnectionChanged(bool connected)
{
Yeelight *yeelight = static_cast<Yeelight *>(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<Yeelight *>(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());
}

View File

@ -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<DeviceDiscoveryInfo*, DiscoveryJob*> m_discoveries;
QList<QString> m_usedInterfaces;
QTimer *m_reconnectTimer = nullptr;
PluginTimer *m_pluginTimer = nullptr;
QHash<int, DeviceActionInfo *> m_asyncActions;
QHash<DeviceId, Yeelight *> 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

View File

@ -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
}
]
}

130
yeelight/ssdp.cpp Normal file
View File

@ -0,0 +1,130 @@
#include "ssdp.h"
#include "extern-plugininfo.h"
#include <QMetaObject>
#include <QNetworkInterface>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <QNetworkConfiguration>
#include <QNetworkConfigurationManager>
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;
}

47
yeelight/ssdp.h Normal file
View File

@ -0,0 +1,47 @@
#ifndef SSDP_H
#define SSDP_H
#include <QObject>
#include <QUdpSocket>
#include <QUrl>
#include <QTimer>
#include <QUdpSocket>
#include <QHostAddress>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QNetworkAccessManager>
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<UpnpDiscoveryRequest *> m_discoverRequests;
// QHash<QNetworkReply*, UpnpDeviceDescriptor> 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

View File

@ -28,6 +28,7 @@
#include <QJsonObject>
#include <QJsonArray>
#include <QColor>
#include <QRandomGenerator>
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<Yeelight::Property> properties)
{
@ -62,7 +69,7 @@ int Yeelight::getParam(QList<Yeelight::Property> 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<Yeelight::Property> 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);
}
}
}

View File

@ -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<Property> 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

View File

@ -7,7 +7,9 @@ TARGET = $$qtLibraryTarget(nymea_devicepluginyeelight)
SOURCES += \
devicepluginyeelight.cpp \
yeelight.cpp \
ssdp.cpp
HEADERS += \
devicepluginyeelight.h \
yeelight.h \
ssdp.h