added lifx cloud connection

This commit is contained in:
bernhard.trinnes 2020-07-06 15:23:35 +02:00
parent e54c35bdcc
commit 3b2f76ed80
8 changed files with 902 additions and 146 deletions

View File

@ -33,10 +33,18 @@
#include "integrations/integrationplugin.h" #include "integrations/integrationplugin.h"
#include "types/param.h" #include "types/param.h"
#include "plugininfo.h" #include "plugininfo.h"
#include "platform/platformzeroconfcontroller.h"
#include "network/zeroconf/zeroconfservicebrowser.h"
#include <QDebug> #include <QDebug>
#include <QColor> #include <QColor>
#include <QRgb> #include <QRgb>
#include <QFile>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QNetworkReply>
#include <QNetworkRequest>
IntegrationPluginLifx::IntegrationPluginLifx() IntegrationPluginLifx::IntegrationPluginLifx()
{ {
@ -47,26 +55,146 @@ void IntegrationPluginLifx::init()
{ {
m_connectedStateTypeIds.insert(colorBulbThingClassId, colorBulbConnectedStateTypeId); m_connectedStateTypeIds.insert(colorBulbThingClassId, colorBulbConnectedStateTypeId);
m_connectedStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbConnectedStateTypeId); m_connectedStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbConnectedStateTypeId);
m_connectedStateTypeIds.insert(lifxAccountThingClassId, lifxAccountConnectedStateTypeId);
m_powerStateTypeIds.insert(colorBulbThingClassId, colorBulbPowerStateTypeId); m_powerStateTypeIds.insert(colorBulbThingClassId, colorBulbPowerStateTypeId);
m_powerStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbPowerStateTypeId); m_powerStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbPowerStateTypeId);
m_brightnessStateTypeIds.insert(colorBulbThingClassId, colorBulbBrightnessStateTypeId); m_brightnessStateTypeIds.insert(colorBulbThingClassId, colorBulbBrightnessStateTypeId);
m_brightnessStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbBrightnessStateTypeId); m_brightnessStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbBrightnessStateTypeId);
m_colorTemperatureStateTypeIds.insert(colorBulbThingClassId, colorBulbColorTemperatureStateTypeId);
m_colorTemperatureStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbColorTemperatureStateTypeId);
m_hostAddressParamTypeIds.insert(colorBulbThingClassId, colorBulbThingHostParamTypeId);
m_hostAddressParamTypeIds.insert(dimmableBulbThingClassId, dimmableBulbThingHostParamTypeId);
m_portParamTypeIds.insert(colorBulbThingClassId, colorBulbThingPortParamTypeId);
m_portParamTypeIds.insert(dimmableBulbThingClassId, dimmableBulbThingPortParamTypeId);
m_idParamTypeIds.insert(colorBulbThingClassId, colorBulbThingIdParamTypeId);
m_idParamTypeIds.insert(dimmableBulbThingClassId, dimmableBulbThingIdParamTypeId);
m_serviceBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_hap._tcp"); // discovers all homekit devices
QFile file;
file.setFileName("/tmp/products.json");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qCWarning(dcLifx()) << "Could not open products file" << file.errorString() << "file name:" << file.fileName();
} else {
QJsonDocument productsJson = QJsonDocument::fromJson(file.readAll());
file.close();
if (!productsJson.isArray()) {
qCWarning(dcLifx()) << "Products JSON is not a valid array";
} else {
QJsonArray productsArray = productsJson.array().first().toObject().value("products").toArray();
foreach (QJsonValue value, productsArray) {
QJsonObject object = value.toObject();
Lifx::LifxProduct product;
product.pid = object["pid"].toInt();
product.name = object["name"].toString();
qCDebug(dcLifx()) << "Lifx product JSON, found product. PID:" << product.pid << "Name" << product.name;
QJsonObject features = object["features"].toObject();
product.color = features["color"].toBool();
product.infrared = features["infrared"].toBool();
product.matrix = features["matrix"].toBool();
product.multizone = features["multizone"].toBool();
product.minColorTemperature = features["temperature_range"].toArray().first().toInt();
product.maxColorTemperature = features["temperature_range"].toArray().last().toInt();
product.chain = features["chain"].toBool();
m_lifxProducts.insert(product.pid, product);
}
}
}
m_networkManager = hardwareManager()->networkManager();
}
void IntegrationPluginLifx::startPairing(ThingPairingInfo *info)
{
QUrl url("https://api.lifx.com/v1");
QNetworkReply *reply = m_networkManager->get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [this, reply, info] {
if (reply->error() == QNetworkReply::NetworkError::HostNotFoundError) {
info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("LIFX server is not reachable."));
} else {
info->finish(Thing::ThingErrorNoError, "Please enter your Token and password. Get the token from https://cloud.lifx.com/settings");
}
});
}
void IntegrationPluginLifx::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
{
QNetworkRequest request;
request.setUrl(QUrl("https://api.lifx.com/v1/lights/all"));
request.setRawHeader("Authorization","Bearer "+secret.toUtf8());
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [info, reply, secret, username, this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// check HTTP status code
if (status != 200) {
// Error setting up device with invalid token
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("This token is invalid."));
return;
}
qCDebug(dcLifx()) << "Confirm pairing successfull";
pluginStorage()->beginGroup(info->thingId().toString());
pluginStorage()->setValue("username", username);
pluginStorage()->setValue("token", secret);
pluginStorage()->endGroup();
info->finish(Thing::ThingErrorNoError);
});
} }
void IntegrationPluginLifx::discoverThings(ThingDiscoveryInfo *info) void IntegrationPluginLifx::discoverThings(ThingDiscoveryInfo *info)
{ {
if (!m_lifxConnection) { if ((info->thingClassId() == colorBulbThingClassId) || (info->thingClassId() == dimmableBulbThingClassId)) {
m_lifxConnection = new Lifx(this); QHash<QString, ThingDescriptor> descriptors;
m_lifxConnection->enable(); foreach (const ZeroConfServiceEntry avahiEntry, m_serviceBrowser->serviceEntries()) {
} if (!avahiEntry.name().contains("lifx", Qt::CaseSensitivity::CaseInsensitive)) {
continue;
}
if (info->thingClassId() == colorBulbThingClassId) { QString id;
} else if () { QString model;
foreach (const QString &txt, avahiEntry.txt()) {
//qCDebug(dcLifx()) << "txt entry. Key:" << txt.split("=").first() << "value:" << txt.split("=").last();
if (txt.startsWith("id", Qt::CaseSensitivity::CaseInsensitive)) {
id = txt.split("=").last();
} else if (txt.startsWith("md")) {
model = txt.split("=").last();
}
}
if (descriptors.contains(id)) {
// Might appear multiple times, IPv4 and IPv6
continue;
}
qCDebug(dcLifx()) << "Found LIFX device" << model << "ID" << id;
ThingDescriptor descriptor(info->thingClassId(), model, avahiEntry.name() + " (" + avahiEntry.hostAddress().toString() + ")");
ParamList params;
params << Param(m_idParamTypeIds.value(info->thingClassId()), id);
params << Param(m_hostAddressParamTypeIds.value(info->thingClassId()), avahiEntry.hostAddress().toString());
params << Param(m_portParamTypeIds.value(info->thingClassId()), avahiEntry.port());
descriptor.setParams(params);
Things existing = myThings().filterByParam(m_idParamTypeIds.value(info->thingClassId()), id);
if (existing.count() > 0) {
descriptor.setThingId(existing.first()->id());
}
descriptors.insert(id, descriptor);
}
info->addThingDescriptors(descriptors.values());
info->finish(Thing::ThingErrorNoError);
} else { } else {
Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8());
} }
} }
@ -74,14 +202,44 @@ void IntegrationPluginLifx::setupThing(ThingSetupInfo *info)
{ {
Thing *thing = info->thing(); Thing *thing = info->thing();
if (!m_lifxConnection) { if (thing->thingClassId() == colorBulbThingClassId || thing->thingClassId() == dimmableBulbThingClassId) {
m_lifxConnection = new Lifx(this); Lifx *lifx = new Lifx(QHostAddress(thing->paramValue(m_hostAddressParamTypeIds.value(thing->thingClassId())).toString()),
m_lifxConnection->enable(); thing->paramValue(m_portParamTypeIds.value(thing->thingClassId())).toInt(), this);
} if(lifx->enable()) {
m_lifxConnections.insert(thing, lifx);
//TODO async setup
info->finish(Thing::ThingErrorNoError);
} else {
lifx->deleteLater();
info->finish(Thing::ThingErrorSetupFailed);
}
} else if (thing->thingClassId() == lifxAccountThingClassId) {
if (thing->thingClassId() == colorBulbThingClassId) { pluginStorage()->beginGroup(thing->id().toString());
QByteArray token = pluginStorage()->value("token").toByteArray();
QByteArray username = pluginStorage()->value("username").toByteArray();
pluginStorage()->endGroup();
info->finish(Thing::ThingErrorNoError); if (token.isEmpty()) {
qCWarning(dcLifx()) << "Lifx setup, token is not stored";
return info->finish(Thing::ThingErrorAuthenticationFailure);
}
thing->setStateValue(lifxAccountUserDisplayNameStateTypeId, username);
LifxCloud *lifxCloud = new LifxCloud(hardwareManager()->networkManager(), this);
m_asyncCloudSetups.insert(lifxCloud, info);
connect(info, &ThingSetupInfo::aborted, info, [lifxCloud, this] {
m_asyncCloudSetups.remove(lifxCloud);
lifxCloud->deleteLater();
});
connect(lifxCloud, &LifxCloud::lightsListReceived, this, &IntegrationPluginLifx::onLifxCloudLightsListReceived);
connect(lifxCloud, &LifxCloud::scenesListReceived, this, &IntegrationPluginLifx::onLifxCloudScenesListReceived);
connect(lifxCloud, &LifxCloud::requestExecuted, this, &IntegrationPluginLifx::onLifxCloudRequestExecuted);
lifxCloud->setAuthorizationToken(token);
lifxCloud->listLights();
//TODO try setup again if it failes
} else {
Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
} }
} }
@ -92,92 +250,168 @@ void IntegrationPluginLifx::postSetupThing(Thing *thing)
if (!m_pluginTimer) { if (!m_pluginTimer) {
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60);
connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() {
foreach (Lifx *lifx, m_lifxConnections) {
Q_UNUSED(lifx)
}
foreach (LifxCloud *lifx, m_lifxCloudConnections) {
lifx->listLights();
}
}); });
} }
m_pluginTimer->timeout();
if (thing->thingClassId() == lifxAccountThingClassId) {
thing->setStateValue(lifxAccountConnectedStateTypeId, true);
thing->setStateValue(lifxAccountLoggedInStateTypeId, true);
}
} }
void IntegrationPluginLifx::executeAction(ThingActionInfo *info) void IntegrationPluginLifx::executeAction(ThingActionInfo *info)
{ {
Thing *thing = info->thing(); Thing *thing = info->thing();
Action action = info->action(); Action action = info->action();
bool cloudDevice = false;
Lifx *lifx;
LifxCloud *lifxCloud;
if (!m_lifxConnection) if (m_lifxConnections.contains(thing)) {
lifx = m_lifxConnections.value(thing);
} else if (m_lifxCloudConnections.contains(myThings().findById(thing->parentId()))) {
lifxCloud = m_lifxCloudConnections.value(myThings().findById(thing->parentId()));
cloudDevice = true;
} else {
qCWarning(dcLifx()) << "Could not find any LIFX connection for thing" << thing->name();
return info->finish(Thing::ThingErrorHardwareFailure); return info->finish(Thing::ThingErrorHardwareFailure);
}
if (thing->thingClassId() == colorBulbThingClassId) { if (thing->thingClassId() == colorBulbThingClassId) {
QByteArray lightId = thing->paramValue(colorBulbThingIdParamTypeId).toByteArray();
if (action.actionTypeId() == colorBulbPowerActionTypeId) { if (action.actionTypeId() == colorBulbPowerActionTypeId) {
bool power = action.param(colorBulbPowerActionPowerParamTypeId).value().toBool(); bool power = action.param(colorBulbPowerActionPowerParamTypeId).value().toBool();
int requestId = m_lifxConnection->setPower(power); int requestId;
if (cloudDevice) {
requestId = lifxCloud->setPower(lightId, power);
} else {
requestId = lifx->setPower(power);
}
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info); m_asyncActions.insert(requestId, info);
} else if (action.actionTypeId() == colorBulbBrightnessActionTypeId) { } else if (action.actionTypeId() == colorBulbBrightnessActionTypeId) {
if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()) if (!thing->stateValue(colorBulbPowerStateTypeId).toBool())
m_lifxConnection->setPower(true); lifx->setPower(true);
if (m_pendingBrightnessAction.contains(thing->id())) { int brightness = info->action().param(colorBulbBrightnessActionBrightnessParamTypeId).value().toInt();
//Scrap old info and insert new one int requestId;
m_pendingBrightnessAction.insert(thing->id(), info); if (cloudDevice) {
requestId = lifxCloud->setBrightnesss(lightId, brightness);
} else { } else {
m_pendingBrightnessAction.insert(thing->id(), info); requestId = lifx->setBrightness(brightness);
QTimer::singleShot(500, this, [info, this]{
int brightness = info->action().param(colorBulbBrightnessActionBrightnessParamTypeId).value().toInt();
m_pendingBrightnessAction.remove(info->thing()->id());
int requestId = m_lifxConnection->setBrightness(brightness);
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info);
});
} }
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info);
} else if (action.actionTypeId() == colorBulbColorActionColorParamTypeId) { } else if (action.actionTypeId() == colorBulbColorActionColorParamTypeId) {
QRgb color = QColor(action.param(colorBulbColorActionColorParamTypeId).value().toString()).rgba(); QRgb color = QColor(action.param(colorBulbColorActionColorParamTypeId).value().toString()).rgba();
if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()) if (!thing->stateValue(colorBulbPowerStateTypeId).toBool())
m_lifxConnection->setPower(true); lifx->setPower(true);
int requestId = m_lifxConnection->setColor(color); int requestId;
if (cloudDevice) {
requestId = lifxCloud->setColor(lightId, color);
} else {
requestId = lifx->setColor(color);
}
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info); m_asyncActions.insert(requestId, info);
} else if (action.actionTypeId() == colorBulbColorTemperatureActionTypeId) { } else if (action.actionTypeId() == colorBulbColorTemperatureActionTypeId) {
int colorTemperature = 6500 - (action.param(colorBulbColorTemperatureActionColorTemperatureParamTypeId).value().toUInt() * -11.12); int colorTemperature = 6500 - (action.param(colorBulbColorTemperatureActionColorTemperatureParamTypeId).value().toUInt() * -11.12);
if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()) if (!thing->stateValue(colorBulbPowerStateTypeId).toBool())
m_lifxConnection->setPower(true); lifx->setPower(true);
int requestId = m_lifxConnection->setColorTemperature(colorTemperature); int requestId;
if (cloudDevice) {
requestId = lifxCloud->setColorTemperature(lightId, colorTemperature);
} else {
requestId = lifx->setColorTemperature(colorTemperature);
}
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info); m_asyncActions.insert(requestId, info);
} else { } else {
qCWarning(dcLifx()) << "Unhandled action"; Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8());
} }
} else if (thing->thingClassId() == dimmableBulbThingClassId) { } else if (thing->thingClassId() == dimmableBulbThingClassId) {
QByteArray lightId = thing->paramValue(dimmableBulbThingIdParamTypeId).toByteArray();
if (action.actionTypeId() == dimmableBulbPowerActionTypeId) { if (action.actionTypeId() == dimmableBulbPowerActionTypeId) {
bool power = action.param(dimmableBulbPowerActionPowerParamTypeId).value().toBool(); bool power = action.param(dimmableBulbPowerActionPowerParamTypeId).value().toBool();
int requestId = m_lifxConnection->setPower(power); int requestId;
if (cloudDevice) {
requestId = lifxCloud->setPower(lightId, power);
} else {
requestId = lifx->setPower(power);
}
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
} else if (action.actionTypeId() == dimmableBulbBrightnessActionTypeId) { } else if (action.actionTypeId() == dimmableBulbBrightnessActionTypeId) {
int brightness = action.param(dimmableBulbBrightnessActionBrightnessParamTypeId).value().toInt(); int brightness = action.param(dimmableBulbBrightnessActionBrightnessParamTypeId).value().toInt();
if (!thing->stateValue(dimmableBulbPowerStateTypeId).toBool()) if (!thing->stateValue(dimmableBulbPowerStateTypeId).toBool())
m_lifxConnection->setPower(true); lifx->setPower(true);
int requestId = m_lifxConnection->setBrightness(brightness); int requestId;
if (cloudDevice) {
requestId = lifxCloud->setBrightnesss(lightId, brightness);
} else {
requestId = lifx->setBrightness(brightness);
}
connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info); m_asyncActions.insert(requestId, info);
} else { } else {
qCWarning(dcLifx()) << "Unhandled action"; Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8());
} }
} else { } else {
qCWarning(dcLifx()) << "Unhandled device class"; Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
} }
} }
void IntegrationPluginLifx::thingRemoved(Thing *thing) void IntegrationPluginLifx::thingRemoved(Thing *thing)
{ {
Q_UNUSED(thing) if (thing->thingClassId() == colorBulbThingClassId || thing->thingClassId() == dimmableBulbThingClassId) {
if (m_lifxConnections.contains(thing))
m_lifxConnections.take(thing)->deleteLater();
} else if (thing->thingClassId() == lifxAccountThingClassId) {
if (m_lifxCloudConnections.contains(thing))
m_lifxCloudConnections.take(thing)->deleteLater();
}
if (myThings().isEmpty()) { if (myThings().isEmpty()) {
m_lifxConnection->deleteLater(); hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
m_lifxConnection = nullptr; m_pluginTimer = nullptr;
} }
} }
void IntegrationPluginLifx::browseThing(BrowseResult *result)
{
Thing *thing = result->thing();
LifxCloud *lifxCloud = m_lifxCloudConnections.value(thing);
if (!lifxCloud)
return;
lifxCloud->listScenes();
m_asyncBrowseResults.insert(lifxCloud, result);
connect(result, &BrowseResult::aborted, this, [lifxCloud, this]{m_asyncBrowseResults.remove(lifxCloud);});
}
void IntegrationPluginLifx::browserItem(BrowserItemResult *result)
{
Q_UNUSED(result)
qCDebug(dcLifx()) << "BrowserItem called";
}
void IntegrationPluginLifx::executeBrowserItem(BrowserActionInfo *info)
{
Thing *thing = info->thing();
LifxCloud *lifxCloud = m_lifxCloudConnections.value(thing);
int requestId = lifxCloud->activateScene(info->browserAction().itemId());
m_asyncBrowserItem.insert(requestId, info);
connect(info, &BrowserActionInfo::aborted, this, [requestId, this] {m_asyncBrowserItem.remove(requestId);});
}
void IntegrationPluginLifx::onDeviceNameChanged() void IntegrationPluginLifx::onDeviceNameChanged()
{ {
Thing *thing = static_cast<Thing *>(sender()); Thing *thing = static_cast<Thing *>(sender());
@ -186,8 +420,12 @@ void IntegrationPluginLifx::onDeviceNameChanged()
void IntegrationPluginLifx::onConnectionChanged(bool connected) void IntegrationPluginLifx::onConnectionChanged(bool connected)
{ {
Q_UNUSED(connected) Q_UNUSED(connected)
// thing->setStateValue(m_connectedStateTypeIds.value(thing->ThingClassId()), connected); Lifx *lifx = static_cast<Lifx *>(sender());
Thing *thing = m_lifxConnections.key(lifx);
if (!thing)
return;
thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), connected);
} }
void IntegrationPluginLifx::onRequestExecuted(int requestId, bool success) void IntegrationPluginLifx::onRequestExecuted(int requestId, bool success)
@ -200,27 +438,111 @@ void IntegrationPluginLifx::onRequestExecuted(int requestId, bool success)
info->finish(Thing::ThingErrorHardwareFailure); info->finish(Thing::ThingErrorHardwareFailure);
} }
} }
if (m_asyncBrowserItem.contains(requestId)) {
BrowserActionInfo *info = m_asyncBrowserItem.take(requestId);
if (success) {
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareNotAvailable);
}
}
} }
/*void IntegrationPluginLifx::onPowerNotificationReceived(bool status) void IntegrationPluginLifx::onLifxCloudConnectionChanged(bool connected)
{ {
Q_UNUSED(status) LifxCloud *lifxCloud = static_cast<LifxCloud *>(sender());
Lifx *Lifx = static_cast<Lifx *>(sender()); Thing *accountThing = m_lifxCloudConnections.key(lifxCloud);
Device * device = myThings().findById(m_LifxConnections.key(Lifx)); if (!accountThing)
if(!device)
return; return;
accountThing->setStateValue(m_connectedStateTypeIds.value(accountThing->thingClassId()), connected);
thing->setStateValue(m_powerStateTypeIds.value(thing->ThingClassId()), status); foreach (Thing *thing, myThings().filterByParentId(accountThing->id())) {
if (!connected)
thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), connected);
}
} }
void IntegrationPluginLifx::onBrightnessNotificationReceived(int percentage) void IntegrationPluginLifx::onLifxCloudLightsListReceived(const QList<LifxCloud::Light> &lights)
{ {
Q_UNUSED(percentage) LifxCloud *lifxCloud = static_cast<LifxCloud *>(sender());
Lifx *lifx = static_cast<Lifx *>(sender()); if (m_asyncCloudSetups.contains(lifxCloud)) {
Device * device = myThings().findById(m_lifxConnections.key(lifx)); ThingSetupInfo *info = m_asyncCloudSetups.take(lifxCloud);
if(!device) m_lifxCloudConnections.insert(info->thing(), lifxCloud);
return; info->finish(Thing::ThingErrorNoError);
}
thing->setStateValue(m_brightnessStateTypeIds.value(thing->ThingClassId()), percentage); ThingDescriptors thingDescriptors;
}*/ Q_FOREACH(LifxCloud::Light light, lights) {
Thing *parentThing = m_lifxCloudConnections.key(lifxCloud);
if (!parentThing) {
qCWarning(dcLifx()) << "Could not find thing to cloud connection";
return;
}
ThingClassId thingClassId;
if (light.product.capabilities.color) {
thingClassId = colorBulbThingClassId;
} else if (light.product.capabilities.colorTemperature) {
thingClassId = dimmableBulbThingClassId;
} else {
qCWarning(dcLifx()) << "LIFX product is not supported";
}
qCDebug(dcLifx()) << "Light product:" << light.id << light.uuid << light.label << light.product.identifier;
ThingDescriptor thingDescriptor(thingClassId, light.product.name, light.location.name, parentThing->id());
foreach (Thing * thing, myThings().filterByParam(m_idParamTypeIds.value(thingClassId), light.id)) {
thing->setStateValue(m_connectedStateTypeIds.value(thingClassId), light.connected);
thing->setStateValue(m_brightnessStateTypeIds.value(thingClassId), light.brightness);
thing->setStateValue(m_colorTemperatureStateTypeIds.value(thingClassId), light.colorTemperature);
thing->setStateValue(m_powerStateTypeIds.value(thingClassId), light.power);
if (thingClassId == colorBulbThingClassId) {
thing->setStateValue(colorBulbColorStateTypeId, light.color);
}
thingDescriptor.setThingId(thing->id());
break;
}
ParamList params;
params << Param(m_idParamTypeIds.value(thingDescriptor.thingClassId()), light.id);
thingDescriptor.setParams(params);
thingDescriptors.append(thingDescriptor);
}
if (!thingDescriptors.isEmpty())
autoThingsAppeared(thingDescriptors);
}
void IntegrationPluginLifx::onLifxCloudRequestExecuted(int requestId, bool success)
{
ThingActionInfo *info = m_asyncActions.take(requestId);
if (!info) {
return;
}
if (success) {
info->finish(Thing::ThingErrorNoError);
} else {
info->finish(Thing::ThingErrorHardwareNotAvailable);
}
}
void IntegrationPluginLifx::onLifxCloudScenesListReceived(const QList<LifxCloud::Scene> &scenes)
{
LifxCloud *lifxCloud = static_cast<LifxCloud *>(sender());
Thing *thing = m_lifxCloudConnections.key(lifxCloud);
if (!thing)
return;
qCDebug(dcLifx()) << "Scene list received, count: " << scenes.length();
if (m_asyncBrowseResults.contains(lifxCloud)) {
BrowseResult *result = m_asyncBrowseResults.take(lifxCloud);
foreach (LifxCloud::Scene scene, scenes) {
BrowserItem item;
item.setId(scene.id);
item.setBrowsable(false);
item.setExecutable(true);
item.setDisplayName(scene.name);
item.setDisabled(false);
result->addItem(item);
}
result->finish(Thing::ThingErrorNoError);
}
}

View File

@ -34,6 +34,11 @@
#include "integrations/integrationplugin.h" #include "integrations/integrationplugin.h"
#include "plugintimer.h" #include "plugintimer.h"
#include "lifx.h" #include "lifx.h"
#include "lifxcloud.h"
#include "network/networkaccessmanager.h"
#include "network/zeroconf/zeroconfservicebrowser.h"
#include "network/zeroconf/zeroconfserviceentry.h"
#include <QTimer> #include <QTimer>
@ -48,33 +53,51 @@ public:
explicit IntegrationPluginLifx(); explicit IntegrationPluginLifx();
void init() override; void init() override;
void startPairing(ThingPairingInfo *info) override;
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
void discoverThings(ThingDiscoveryInfo *info) override; void discoverThings(ThingDiscoveryInfo *info) override;
void setupThing(ThingSetupInfo *info) override; void setupThing(ThingSetupInfo *info) override;
void postSetupThing(Thing *thing) override; void postSetupThing(Thing *thing) override;
void executeAction(ThingActionInfo *info) override; void executeAction(ThingActionInfo *info) override;
void thingRemoved(Thing *thing) override; void thingRemoved(Thing *thing) override;
void browseThing(BrowseResult *result) override;
void browserItem(BrowserItemResult *result) override;
void executeBrowserItem(BrowserActionInfo *info) override;
private: private:
NetworkAccessManager *m_networkManager = nullptr;
PluginTimer *m_pluginTimer = nullptr; PluginTimer *m_pluginTimer = nullptr;
QHash<LifxCloud *, ThingSetupInfo *> m_asyncCloudSetups;
QHash<int, ThingActionInfo *> m_asyncActions; QHash<int, ThingActionInfo *> m_asyncActions;
Lifx *m_lifxConnection; QHash<Thing *, Lifx *> m_lifxConnections;
QHash<Thing *, LifxCloud *> m_lifxCloudConnections;
QHash<LifxCloud *, BrowseResult *> m_asyncBrowseResults;
QHash<int, BrowserActionInfo *> m_asyncBrowserItem;
ZeroConfServiceBrowser *m_serviceBrowser = nullptr;
QHash<ThingClassId, StateTypeId> m_connectedStateTypeIds; QHash<ThingClassId, StateTypeId> m_connectedStateTypeIds;
QHash<ThingClassId, StateTypeId> m_powerStateTypeIds; QHash<ThingClassId, StateTypeId> m_powerStateTypeIds;
QHash<ThingClassId, StateTypeId> m_brightnessStateTypeIds; QHash<ThingClassId, StateTypeId> m_brightnessStateTypeIds;
QHash<ThingClassId, StateTypeId> m_colorTemperatureStateTypeIds;
QHash<ThingClassId, ParamTypeId> m_hostAddressParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_portParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_idParamTypeIds;
QHash<ThingId, ThingActionInfo *> m_pendingBrightnessAction; QHash<ThingId, ThingActionInfo *> m_pendingBrightnessAction;
QHash<int, Lifx::LifxProduct> m_lifxProducts;
private slots: private slots:
void onDeviceNameChanged(); void onDeviceNameChanged();
void onConnectionChanged(bool connected); void onConnectionChanged(bool connected);
void onRequestExecuted(int requestId, bool success); void onRequestExecuted(int requestId, bool success);
/*void onPowerNotificationReceived(bool status); void onLifxCloudConnectionChanged(bool connected);
void onBrightnessNotificationReceived(int percentage); void onLifxCloudRequestExecuted(int requestId, bool success);
void onColorTemperatureNotificationReceived(int kelvin); void onLifxCloudLightsListReceived(const QList<LifxCloud::Light> &lights);
void onRgbNotificationReceived(QRgb rgbColor); void onLifxCloudScenesListReceived(const QList<LifxCloud::Scene> &scenes);
void onNameNotificationReceived(const QString &name);*/
}; };
#endif // INTEGRATIONPLUGIN_LIFX_H #endif // INTEGRATIONPLUGIN_LIFX_H

View File

@ -8,11 +8,50 @@
"displayName": "LIFX", "displayName": "LIFX",
"id": "e5e48c0d-cff7-4c0f-983e-d23bd3e4ba87", "id": "e5e48c0d-cff7-4c0f-983e-d23bd3e4ba87",
"thingClasses": [ "thingClasses": [
{
"id": "387c87f6-3e5b-4d6a-ba4d-372d0efad79f",
"name": "lifxAccount",
"displayName": "LIFX cloud account",
"createMethods": ["user"],
"interfaces": ["account"],
"setupMethod": "userandpassword",
"browsable": true,
"paramTypes": [
],
"stateTypes": [
{
"id": "0db34069-5de0-4233-baec-27f039228524",
"name": "loggedIn",
"displayName": "Logged in",
"displayNameEvent": "Logged in changed",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "554afd9b-a2ec-4d28-9065-2b9ab3a9e3b2",
"name": "userDisplayName",
"displayName": "User name",
"displayNameEvent": "User name changed",
"type": "QString",
"defaultValue": "-"
},
{
"id": "3e7b358b-d7de-4db4-8a3a-b9860eae186f",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"defaultValue": false,
"type": "bool",
"cached": false
}
]
},
{ {
"id": "12907c9c-e7f0-47f2-bd58-39d52ffdf24e", "id": "12907c9c-e7f0-47f2-bd58-39d52ffdf24e",
"name": "colorBulb", "name": "colorBulb",
"displayName": "Color", "displayName": "Color",
"createMethods": ["user", "discovery"], "createMethods": ["auto", "discovery"],
"interfaces": ["colorlight", "connectable"], "interfaces": ["colorlight", "connectable"],
"paramTypes": [ "paramTypes": [
{ {
@ -20,7 +59,6 @@
"name": "host", "name": "host",
"displayName": "Host address", "displayName": "Host address",
"type" : "QString", "type" : "QString",
"inputType": "IPv4Address",
"readOnly": true "readOnly": true
}, },
{ {
@ -33,7 +71,7 @@
{ {
"id": "976ecea0-ac25-47d4-9dc5-362962ddb6c0", "id": "976ecea0-ac25-47d4-9dc5-362962ddb6c0",
"name": "id", "name": "id",
"displayName": "Id", "displayName": "ID",
"type" : "QString", "type" : "QString",
"readOnly": true "readOnly": true
} }
@ -114,7 +152,7 @@
"id": "a5b02af8-7c97-4a78-9c78-bafee7407b5e", "id": "a5b02af8-7c97-4a78-9c78-bafee7407b5e",
"name": "dimmableBulb", "name": "dimmableBulb",
"displayName": "Day and Dusk", "displayName": "Day and Dusk",
"createMethods": ["user", "discovery"], "createMethods": ["auto", "discovery"],
"interfaces": ["colortemperaturelight", "connectable"], "interfaces": ["colortemperaturelight", "connectable"],
"paramTypes": [ "paramTypes": [
{ {
@ -122,7 +160,6 @@
"name": "host", "name": "host",
"displayName": "Host address", "displayName": "Host address",
"type" : "QString", "type" : "QString",
"inputType": "IPv4Address",
"readOnly": true "readOnly": true
}, },
{ {

View File

@ -33,47 +33,22 @@
#include "extern-plugininfo.h" #include "extern-plugininfo.h"
#include <QColor> #include <QColor>
#include <QFile>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
Lifx::Lifx(QObject *parent) :
QObject(parent) Lifx::Lifx(const QHostAddress &address, quint16 port, QObject *parent) :
QObject(parent),
m_host(address),
m_port(port)
{ {
m_reconnectTimer = new QTimer(this); m_reconnectTimer = new QTimer(this);
m_reconnectTimer->setSingleShot(true); m_reconnectTimer->setSingleShot(true);
connect(m_reconnectTimer, &QTimer::timeout, this, &Lifx::onReconnectTimer); connect(m_reconnectTimer, &QTimer::timeout, this, &Lifx::onReconnectTimer);
m_clientId = qrand(); m_clientId = qrand();
QFile file; m_socket = new QUdpSocket(this);
file.setFileName("/tmp/products.json");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qCWarning(dcLifx()) << "Could not open products file" << file.errorString();
}
QJsonDocument productsJson = QJsonDocument::fromJson(file.readAll());
file.close();
if (!productsJson.isArray()) { m_socket->setSocketOption(QAbstractSocket::MulticastTtlOption, QVariant(1));
qCWarning(dcLifx()) << "Products JSON is not a valid array"; m_socket->setSocketOption(QAbstractSocket::MulticastLoopbackOption, QVariant(1));
}
QJsonArray productsArray = productsJson.array().first().toObject().value("products").toArray();
foreach (QJsonValue value, productsArray) {
QJsonObject object = value.toObject();
LifxProduct product;
product.pid = object["pid"].toInt();
product.name = object["name"].toString();
qCDebug(dcLifx()) << "Lifx product JSON, found product. PID:" << product.pid << "Name" << product.name;
QJsonObject features = object["features"].toObject();
product.color = features["color"].toBool();
product.infrared = features["infrared"].toBool();
product.matrix = features["matrix"].toBool();
product.multizone = features["multizone"].toBool();
product.minColorTemperature = features["temperature_range"].toArray().first().toInt();
product.maxColorTemperature = features["temperature_range"].toArray().last().toInt();
product.chain = features["chain"].toBool();
m_lifxProducts.insert(product.pid, product);
}
} }
Lifx::~Lifx() Lifx::~Lifx()
@ -86,20 +61,7 @@ Lifx::~Lifx()
bool Lifx::enable() bool Lifx::enable()
{ {
// Clean up
if (m_socket) {
delete m_socket;
m_socket = nullptr;
}
// Bind udp socket and join multicast group // Bind udp socket and join multicast group
m_socket = new QUdpSocket(this);
m_port = 56700;
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)){ if(!m_socket->bind(QHostAddress::AnyIPv4, m_port, QUdpSocket::ShareAddress)){
qCWarning(dcLifx()) << "could not bind to port" << m_port; qCWarning(dcLifx()) << "could not bind to port" << m_port;
delete m_socket; delete m_socket;
@ -107,8 +69,8 @@ bool Lifx::enable()
return false; return false;
} }
if(!m_socket->joinMulticastGroup(m_host)){ if(!m_socket->joinMulticastGroup(QHostAddress("239.255.255.250"))){
qCWarning(dcLifx()) << "could not join multicast group" << m_host; qCWarning(dcLifx()) << "could not join multicast group";
delete m_socket; delete m_socket;
m_socket = nullptr; m_socket = nullptr;
return false; return false;
@ -118,31 +80,37 @@ bool Lifx::enable()
return true; return true;
} }
void Lifx::discoverDevices() void Lifx::setHostAddress(const QHostAddress &address)
{ {
Message message; m_host = address;
sendMessage(message);
} }
int Lifx::setColorTemperature(int mirad, int msFadeTime) void Lifx::setPort(quint16 port)
{
m_port = port;
}
int Lifx::setColorTemperature(uint mirad, uint msFadeTime)
{ {
Q_UNUSED(mirad) Q_UNUSED(mirad)
Q_UNUSED(msFadeTime) Q_UNUSED(msFadeTime)
int requestId = qrand(); int requestId = qrand();
Message message;
sendMessage(message);
return requestId; return requestId;
} }
int Lifx::setColor(QColor color, int msFadeTime) int Lifx::setColor(QColor color, uint msFadeTime)
{ {
Q_UNUSED(color) Q_UNUSED(color)
Q_UNUSED(msFadeTime) Q_UNUSED(msFadeTime)
int requestId = qrand(); int requestId = qrand();
Message message;
sendMessage(message);
return requestId; return requestId;
} }
int Lifx::setBrightness(int percentage, int msFadeTime) int Lifx::setBrightness(uint percentage, uint msFadeTime)
{ {
Q_UNUSED(percentage) Q_UNUSED(percentage)
Q_UNUSED(msFadeTime) Q_UNUSED(msFadeTime)
@ -152,12 +120,13 @@ int Lifx::setBrightness(int percentage, int msFadeTime)
return requestId; return requestId;
} }
int Lifx::setPower(bool power, int msFadeTime) int Lifx::setPower(bool power, uint msFadeTime)
{ {
Q_UNUSED(power) Q_UNUSED(power)
Q_UNUSED(msFadeTime) Q_UNUSED(msFadeTime)
int requestId = qrand(); int requestId = qrand();
Message message;
sendMessage(message);
return requestId; return requestId;
} }
@ -219,14 +188,13 @@ void Lifx::sendMessage(const Lifx::Message &message)
//Finally another reserved field of 16 bits (2 bytes). //Finally another reserved field of 16 bits (2 bytes).
header.append(2, '0'); header.append(2, '0');
QByteArray payload; QByteArray fullMessage;
//message.append(header);
QByteArray message; //message.append(payload);
message.append(header); //message.append(message.length());
message.append(payload); fullMessage = QByteArray::fromHex("0x310000340000000000000000000000000000000000000000000000000000000066000000005555FFFFFFFFAC0D00040000");
message.append(message.length()); std::reverse(fullMessage.begin(), fullMessage.end());
//header = QByteArray::fromHex("0x310000340000000000000000000000000000000000000000000000000000000066000000005555FFFFFFFFAC0D00040000"); m_socket->writeDatagram(fullMessage, m_host, m_port);
m_socket->writeDatagram(message, m_host, m_port);
} }
void Lifx::onStateChanged(QAbstractSocket::SocketState state) void Lifx::onStateChanged(QAbstractSocket::SocketState state)

View File

@ -28,7 +28,6 @@
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef LIFX_H #ifndef LIFX_H
#define LIFX_H #define LIFX_H
@ -123,15 +122,16 @@ public:
bool chain; bool chain;
}; };
explicit Lifx(QObject *parent = nullptr); explicit Lifx(const QHostAddress &address, quint16 port = 56700, QObject *parent = nullptr);
~Lifx(); ~Lifx();
bool enable(); bool enable();
void setHostAddress(const QHostAddress &address);
void setPort(quint16 port);
void discoverDevices(); int setColorTemperature(uint kelvin, uint msFadeTime=500);
int setColorTemperature(int kelvin, int msFadeTime=500); int setColor(QColor color, uint msFadeTime = 500);
int setColor(QColor color, int msFadeTime = 500); int setBrightness(uint percentage, uint msFadeTime = 500);
int setBrightness(int percentage, int msFadeTime = 500); int setPower(bool power, uint msFadeTime = 500);
int setPower(bool power, int msFadeTime = 500);
int flash(); int flash();
int flash15s(); int flash15s();
@ -142,7 +142,6 @@ private:
QHostAddress m_host; QHostAddress m_host;
quint16 m_port; quint16 m_port;
quint8 m_sequenceNumber = 0; quint8 m_sequenceNumber = 0;
QHash<int, LifxProduct> m_lifxProducts;
void sendMessage(const Message &message); void sendMessage(const Message &message);
@ -153,7 +152,6 @@ private slots:
signals: signals:
void connectionChanged(bool connected); void connectionChanged(bool connected);
void deviceDiscovered(const QHostAddress &address, int port, const LifxProduct &product);
//void requestExecuted(int requestId, bool success); //void requestExecuted(int requestId, bool success);
//void errorReceived(int code, const QString &message); //void errorReceived(int code, const QString &message);

View File

@ -5,8 +5,10 @@ QT += network
SOURCES += \ SOURCES += \
integrationpluginlifx.cpp \ integrationpluginlifx.cpp \
lifx.cpp \ lifx.cpp \
lifxcloud.cpp \
HEADERS += \ HEADERS += \
integrationpluginlifx.h \ integrationpluginlifx.h \
lifx.h \ lifx.h \
lifxcloud.h \

284
lifx/lifxcloud.cpp Normal file
View File

@ -0,0 +1,284 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "lifxcloud.h"
#include "extern-plugininfo.h"
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
LifxCloud::LifxCloud(NetworkAccessManager *networkManager, QObject *parent) :
QObject(parent),
m_networkManager(networkManager)
{
}
void LifxCloud::setAuthorizationToken(const QByteArray &token)
{
m_authorizationToken = token;
}
void LifxCloud::listLights()
{
if (m_authorizationToken.isEmpty()) {
qCWarning(dcLifx()) << "Authorization token is not set";
return;
}
QNetworkRequest request;
request.setUrl(QUrl("https://api.lifx.com/v1/lights/all"));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization","Bearer "+m_authorizationToken);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// check HTTP status code
if (status != 200) {
qCWarning(dcLifx()) << "Error get lights list" << status << reply->errorString();
return;
}
QByteArray rawData = reply->readAll();
QJsonDocument data; QJsonParseError error;
data = QJsonDocument::fromJson(rawData, &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcLifx()) << "List lights: Received invalide JSON object" << error.errorString();
return;
}
if (!data.isArray())
qCWarning(dcLifx()) << "Data is not an array";
QJsonArray array = data.array();
QList<Light> descriptors;
foreach (QJsonValue jsonValue, array) {
QJsonObject object = jsonValue.toObject();
qCDebug(dcLifx()) << "Light object:" << object;
Light light;
light.id = object["id"].toString().toUtf8();
light.uuid = object["uuid"].toString().toUtf8();
light.label = object["label"].toString();
light.connected = object["connected"].toBool();
light.brightness = object["brightness"].toDouble();
int hue = object["hue"].toObject().value("saturation").toDouble();
int saturation = object["color"].toObject().value("saturation").toDouble();
light.colorTemperature = object["color"].toObject().value("kelvin").toDouble();
light.color = QColor::fromHsv(hue, saturation, light.brightness);
Group group;
group.name = object["group"].toObject().value("name").toString();
group.id = object["group"].toObject().value("id").toString().toUtf8();
light.group = group;
Location location;
location.name = object["location"].toObject().value("name").toString();
location.id = object["location"].toObject().value("id").toString().toUtf8();
light.location = location;
Product product;
QJsonObject productObject = object["product"].toObject();
product.name = productObject["name"].toString();
product.identifier = productObject["identifier"].toString();
product.manufacturer = productObject["manufacturer"].toString();
product.secondsSinceLastSeen = productObject["seconds_since_seen"].toInt();
Capabilities capabilities;
QJsonObject capabilitiesObject = productObject["capabilities"].toObject();
capabilities.color = capabilitiesObject["has_color"].toBool();
capabilities.colorTemperature = capabilitiesObject["has_variable_color_temp"].toBool();
capabilities.ir = capabilitiesObject["has_ir"].toBool();
capabilities.chain = capabilitiesObject["has_chain"].toBool();
capabilities.multizone = capabilitiesObject["has_multizone"].toBool();
capabilities.minKelvin= capabilitiesObject["min_kelvin"].toInt();
capabilities.maxKelvin = capabilitiesObject["max_kelvin"].toInt();
product.capabilities = capabilities;
light.product = product;
descriptors.append(light);
}
emit lightsListReceived(descriptors);
});
}
void LifxCloud::listScenes()
{
if (m_authorizationToken.isEmpty()) {
qCWarning(dcLifx()) << "Authorization token is not set";
return;
}
QNetworkRequest request;
request.setUrl(QUrl("https://api.lifx.com/v1/scenes"));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization","Bearer "+m_authorizationToken);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// check HTTP status code
if (status != 200) {
qCWarning(dcLifx()) << "Error get scene list" << status << reply->errorString();
return;
}
QByteArray rawData = reply->readAll();
qCDebug(dcLifx()) << "Got list scenes reply" << rawData;
QJsonDocument data; QJsonParseError error;
data = QJsonDocument::fromJson(rawData, &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcLifx()) << "List scenes: Received invalide JSON object" << error.errorString();
return;
}
if (!data.isArray())
qCWarning(dcLifx()) << "Data is not an array";
QJsonArray array = data.array();
QList<Scene> scenes;
foreach (QJsonValue value, array) {
Scene scene;
scene.id = value.toObject().value("id").toString().toUtf8();
scene.name = value.toObject().value("name").toString();
scenes.append(scene);
}
emit scenesListReceived(scenes);
});
}
int LifxCloud::setPower(const QString &lightId, bool power, int duration)
{
return setState(lightId, StatePower, power, duration);
}
int LifxCloud::setBrightnesss(const QString &lightId, int brightness, int duration)
{
return setState(lightId, StateBrightness, brightness/100.00, duration);
}
int LifxCloud::setColor(const QString &lightId, QColor color, int duration)
{
return setState(lightId, StateColor, color.name(), duration);
}
int LifxCloud::setColorTemperature(const QString &selector, int kelvin, int duration)
{
return setState(selector, StateColorTemperature, kelvin, duration);
}
int LifxCloud::setInfrared(const QString &lightId, int infrared, int duration)
{
return setState(lightId, StateColor, infrared/100.00, duration);
}
int LifxCloud::activateScene(const QString &sceneId)
{
if (m_authorizationToken.isEmpty()) {
qCWarning(dcLifx()) << "Authorization token is not set";
return -1;
}
int requestId = qrand();
QNetworkRequest request;
request.setUrl(QUrl(QString("https://api.lifx.com/v1/scenes/scene_id::%1/activate").arg(sceneId)));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization","Bearer "+m_authorizationToken);
QByteArray payload;
payload.append("duration:5");
QNetworkReply *reply = m_networkManager->put(request, payload);
connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// check HTTP status code
if (status != 200) {
qCWarning(dcLifx()) << "Error get scene list" << status << reply->errorString();
return;
}
QByteArray rawData = reply->readAll();
qCDebug(dcLifx()) << "Got list lights reply" << rawData;
});
return requestId;
}
int LifxCloud::setState(const QString &selector, State state, QVariant stateValue, int duration)
{
if (m_authorizationToken.isEmpty()) {
qCWarning(dcLifx()) << "Authorization token is not set";
return -1;
}
int requestId = qrand();
QNetworkRequest request;
request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/%1/state").arg(selector)));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization","Bearer "+m_authorizationToken);
QJsonDocument doc;
QJsonObject payload;
payload["duration"] = duration;
payload["fast"] = false;
switch (state) {
case StatePower:
if (stateValue.toBool())
payload["power"] = "ON";
else
payload["power"] = "OFF";
break;
case StateBrightness:
payload["brightness"] = stateValue.toDouble();
break;
case StateColor:
payload["color"] = stateValue.toString();
break;
case StateColorTemperature:
payload["color"] = "kelvin:"+stateValue.toString();
break;
case StateInfrared:
payload["infrared"] = stateValue.toDouble();
}
doc.setObject(payload);
QNetworkReply *reply = m_networkManager->post(request, doc.toJson());
connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// check HTTP status code
if (status != 200) {
qCWarning(dcLifx()) << "Error get scene list" << status << reply->errorString();
emit requestExecuted(requestId, false);
return;
}
QByteArray rawData = reply->readAll();
qCDebug(dcLifx()) << "Got set state reply" << rawData;
emit requestExecuted(requestId, true);
});
return requestId;
}

122
lifx/lifxcloud.h Normal file
View File

@ -0,0 +1,122 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef LIFXCLOUD_H
#define LIFXCLOUD_H
#include <QObject>
#include <QColor>
#include <QTimer>
#include "network/networkaccessmanager.h"
class LifxCloud : public QObject
{
Q_OBJECT
public:
enum State {
StatePower,
StateBrightness,
StateColor,
StateColorTemperature,
StateInfrared
};
struct Group {
QByteArray id;
QString name;
};
struct Location {
QByteArray id;
QString name;
};
struct Scene {
QByteArray id;
QString name;
};
struct Capabilities {
bool color;
bool colorTemperature;
bool ir;
bool chain;
bool multizone;
int minKelvin;
int maxKelvin;
};
struct Product {
QString name;
QString identifier;
QString manufacturer;
uint secondsSinceLastSeen;
Capabilities capabilities;
};
struct Light {
QByteArray id;
QByteArray uuid;
QString label;
bool connected;
bool power;
QColor color;
int colorTemperature;
double brightness;
Group group;
Location location;
Product product;
};
explicit LifxCloud(NetworkAccessManager *networkManager, QObject *parent = nullptr);
void setAuthorizationToken(const QByteArray &token);
void listLights();
void listScenes();
int setPower(const QString &lightId, bool power, int duration = 3);
int setBrightnesss(const QString &lightId, int brightness, int duration = 3);
int setColor(const QString &selector, QColor color, int duration = 3);
int setColorTemperature(const QString &selector, int kelvin, int duration = 3);
int setInfrared(const QString &lightId, int infrared, int duration = 3);
int activateScene(const QString &sceneId);
private:
NetworkAccessManager *m_networkManager = nullptr;
QByteArray m_authorizationToken;
int setState(const QString &lightId, State state, QVariant stateValue, int duration);
signals:
void lightsListReceived(const QList<Light> &lights);
void scenesListReceived(const QList<Scene> &scenes);
void requestExecuted(int requestId, bool susccess);
};
#endif // LIFXCLOUD_H