From 3b2f76ed80e19371fcabbae40c910605d8ebb339 Mon Sep 17 00:00:00 2001 From: "bernhard.trinnes" Date: Mon, 6 Jul 2020 15:23:35 +0200 Subject: [PATCH] added lifx cloud connection --- lifx/integrationpluginlifx.cpp | 442 +++++++++++++++++++++++++++----- lifx/integrationpluginlifx.h | 35 ++- lifx/integrationpluginlifx.json | 47 +++- lifx/lifx.cpp | 100 +++----- lifx/lifx.h | 16 +- lifx/lifx.pro | 2 + lifx/lifxcloud.cpp | 284 ++++++++++++++++++++ lifx/lifxcloud.h | 122 +++++++++ 8 files changed, 902 insertions(+), 146 deletions(-) create mode 100644 lifx/lifxcloud.cpp create mode 100644 lifx/lifxcloud.h diff --git a/lifx/integrationpluginlifx.cpp b/lifx/integrationpluginlifx.cpp index d4f49add..1baacc85 100644 --- a/lifx/integrationpluginlifx.cpp +++ b/lifx/integrationpluginlifx.cpp @@ -33,10 +33,18 @@ #include "integrations/integrationplugin.h" #include "types/param.h" #include "plugininfo.h" +#include "platform/platformzeroconfcontroller.h" +#include "network/zeroconf/zeroconfservicebrowser.h" #include #include #include +#include +#include +#include +#include +#include +#include IntegrationPluginLifx::IntegrationPluginLifx() { @@ -47,26 +55,146 @@ void IntegrationPluginLifx::init() { m_connectedStateTypeIds.insert(colorBulbThingClassId, colorBulbConnectedStateTypeId); m_connectedStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbConnectedStateTypeId); + m_connectedStateTypeIds.insert(lifxAccountThingClassId, lifxAccountConnectedStateTypeId); m_powerStateTypeIds.insert(colorBulbThingClassId, colorBulbPowerStateTypeId); m_powerStateTypeIds.insert(dimmableBulbThingClassId, dimmableBulbPowerStateTypeId); m_brightnessStateTypeIds.insert(colorBulbThingClassId, colorBulbBrightnessStateTypeId); 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) { - if (!m_lifxConnection) { - m_lifxConnection = new Lifx(this); - m_lifxConnection->enable(); - } + if ((info->thingClassId() == colorBulbThingClassId) || (info->thingClassId() == dimmableBulbThingClassId)) { + QHash descriptors; + foreach (const ZeroConfServiceEntry avahiEntry, m_serviceBrowser->serviceEntries()) { + if (!avahiEntry.name().contains("lifx", Qt::CaseSensitivity::CaseInsensitive)) { + continue; + } - if (info->thingClassId() == colorBulbThingClassId) { - } else if () { + QString id; + 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 { - + 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(); - if (!m_lifxConnection) { - m_lifxConnection = new Lifx(this); - m_lifxConnection->enable(); - } + if (thing->thingClassId() == colorBulbThingClassId || thing->thingClassId() == dimmableBulbThingClassId) { + Lifx *lifx = new Lifx(QHostAddress(thing->paramValue(m_hostAddressParamTypeIds.value(thing->thingClassId())).toString()), + 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) { m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(60); 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) { Thing *thing = info->thing(); 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); + } if (thing->thingClassId() == colorBulbThingClassId) { - + QByteArray lightId = thing->paramValue(colorBulbThingIdParamTypeId).toByteArray(); if (action.actionTypeId() == colorBulbPowerActionTypeId) { 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);}); m_asyncActions.insert(requestId, info); + } else if (action.actionTypeId() == colorBulbBrightnessActionTypeId) { if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()) - m_lifxConnection->setPower(true); + lifx->setPower(true); - if (m_pendingBrightnessAction.contains(thing->id())) { - //Scrap old info and insert new one - m_pendingBrightnessAction.insert(thing->id(), info); + int brightness = info->action().param(colorBulbBrightnessActionBrightnessParamTypeId).value().toInt(); + int requestId; + if (cloudDevice) { + requestId = lifxCloud->setBrightnesss(lightId, brightness); } else { - m_pendingBrightnessAction.insert(thing->id(), info); - 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); - }); + requestId = lifx->setBrightness(brightness); } + connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); + m_asyncActions.insert(requestId, info); } else if (action.actionTypeId() == colorBulbColorActionColorParamTypeId) { QRgb color = QColor(action.param(colorBulbColorActionColorParamTypeId).value().toString()).rgba(); if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()) - m_lifxConnection->setPower(true); - int requestId = m_lifxConnection->setColor(color); + lifx->setPower(true); + 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);}); m_asyncActions.insert(requestId, info); } else if (action.actionTypeId() == colorBulbColorTemperatureActionTypeId) { int colorTemperature = 6500 - (action.param(colorBulbColorTemperatureActionColorTemperatureParamTypeId).value().toUInt() * -11.12); if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()) - m_lifxConnection->setPower(true); - int requestId = m_lifxConnection->setColorTemperature(colorTemperature); + lifx->setPower(true); + 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);}); m_asyncActions.insert(requestId, info); } 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) { 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);}); } else if (action.actionTypeId() == dimmableBulbBrightnessActionTypeId) { int brightness = action.param(dimmableBulbBrightnessActionBrightnessParamTypeId).value().toInt(); if (!thing->stateValue(dimmableBulbPowerStateTypeId).toBool()) - m_lifxConnection->setPower(true); - int requestId = m_lifxConnection->setBrightness(brightness); + lifx->setPower(true); + 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);}); m_asyncActions.insert(requestId, info); } else { - qCWarning(dcLifx()) << "Unhandled action"; + Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8()); } } 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) { - 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()) { - m_lifxConnection->deleteLater(); - m_lifxConnection = nullptr; + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + 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() { Thing *thing = static_cast(sender()); @@ -186,8 +420,12 @@ void IntegrationPluginLifx::onDeviceNameChanged() void IntegrationPluginLifx::onConnectionChanged(bool connected) { - Q_UNUSED(connected) -// thing->setStateValue(m_connectedStateTypeIds.value(thing->ThingClassId()), connected); + Q_UNUSED(connected) + Lifx *lifx = static_cast(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) @@ -200,27 +438,111 @@ void IntegrationPluginLifx::onRequestExecuted(int requestId, bool success) 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) - Lifx *Lifx = static_cast(sender()); - Device * device = myThings().findById(m_LifxConnections.key(Lifx)); - if(!device) + LifxCloud *lifxCloud = static_cast(sender()); + Thing *accountThing = m_lifxCloudConnections.key(lifxCloud); + if (!accountThing) 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 &lights) { - Q_UNUSED(percentage) - Lifx *lifx = static_cast(sender()); - Device * device = myThings().findById(m_lifxConnections.key(lifx)); - if(!device) - return; + LifxCloud *lifxCloud = static_cast(sender()); + if (m_asyncCloudSetups.contains(lifxCloud)) { + ThingSetupInfo *info = m_asyncCloudSetups.take(lifxCloud); + m_lifxCloudConnections.insert(info->thing(), lifxCloud); + 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 &scenes) +{ + LifxCloud *lifxCloud = static_cast(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); + } +} diff --git a/lifx/integrationpluginlifx.h b/lifx/integrationpluginlifx.h index 551ea103..f073486a 100644 --- a/lifx/integrationpluginlifx.h +++ b/lifx/integrationpluginlifx.h @@ -34,6 +34,11 @@ #include "integrations/integrationplugin.h" #include "plugintimer.h" #include "lifx.h" +#include "lifxcloud.h" + +#include "network/networkaccessmanager.h" +#include "network/zeroconf/zeroconfservicebrowser.h" +#include "network/zeroconf/zeroconfserviceentry.h" #include @@ -48,33 +53,51 @@ public: explicit IntegrationPluginLifx(); 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 setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; void executeAction(ThingActionInfo *info) override; void thingRemoved(Thing *thing) override; + void browseThing(BrowseResult *result) override; + void browserItem(BrowserItemResult *result) override; + void executeBrowserItem(BrowserActionInfo *info) override; + private: + NetworkAccessManager *m_networkManager = nullptr; PluginTimer *m_pluginTimer = nullptr; + QHash m_asyncCloudSetups; QHash m_asyncActions; - Lifx *m_lifxConnection; + QHash m_lifxConnections; + QHash m_lifxCloudConnections; + QHash m_asyncBrowseResults; + QHash m_asyncBrowserItem; + + ZeroConfServiceBrowser *m_serviceBrowser = nullptr; QHash m_connectedStateTypeIds; QHash m_powerStateTypeIds; QHash m_brightnessStateTypeIds; + QHash m_colorTemperatureStateTypeIds; + QHash m_hostAddressParamTypeIds; + QHash m_portParamTypeIds; + QHash m_idParamTypeIds; QHash m_pendingBrightnessAction; + QHash m_lifxProducts; private slots: void onDeviceNameChanged(); void onConnectionChanged(bool connected); void onRequestExecuted(int requestId, bool success); - /*void onPowerNotificationReceived(bool status); - void onBrightnessNotificationReceived(int percentage); - void onColorTemperatureNotificationReceived(int kelvin); - void onRgbNotificationReceived(QRgb rgbColor); - void onNameNotificationReceived(const QString &name);*/ + void onLifxCloudConnectionChanged(bool connected); + void onLifxCloudRequestExecuted(int requestId, bool success); + void onLifxCloudLightsListReceived(const QList &lights); + void onLifxCloudScenesListReceived(const QList &scenes); }; #endif // INTEGRATIONPLUGIN_LIFX_H diff --git a/lifx/integrationpluginlifx.json b/lifx/integrationpluginlifx.json index 74007705..eb69ebbd 100644 --- a/lifx/integrationpluginlifx.json +++ b/lifx/integrationpluginlifx.json @@ -8,11 +8,50 @@ "displayName": "LIFX", "id": "e5e48c0d-cff7-4c0f-983e-d23bd3e4ba87", "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", "name": "colorBulb", "displayName": "Color", - "createMethods": ["user", "discovery"], + "createMethods": ["auto", "discovery"], "interfaces": ["colorlight", "connectable"], "paramTypes": [ { @@ -20,7 +59,6 @@ "name": "host", "displayName": "Host address", "type" : "QString", - "inputType": "IPv4Address", "readOnly": true }, { @@ -33,7 +71,7 @@ { "id": "976ecea0-ac25-47d4-9dc5-362962ddb6c0", "name": "id", - "displayName": "Id", + "displayName": "ID", "type" : "QString", "readOnly": true } @@ -114,7 +152,7 @@ "id": "a5b02af8-7c97-4a78-9c78-bafee7407b5e", "name": "dimmableBulb", "displayName": "Day and Dusk", - "createMethods": ["user", "discovery"], + "createMethods": ["auto", "discovery"], "interfaces": ["colortemperaturelight", "connectable"], "paramTypes": [ { @@ -122,7 +160,6 @@ "name": "host", "displayName": "Host address", "type" : "QString", - "inputType": "IPv4Address", "readOnly": true }, { diff --git a/lifx/lifx.cpp b/lifx/lifx.cpp index b3193507..2fdf6503 100644 --- a/lifx/lifx.cpp +++ b/lifx/lifx.cpp @@ -33,47 +33,22 @@ #include "extern-plugininfo.h" #include -#include -#include -#include -#include -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->setSingleShot(true); connect(m_reconnectTimer, &QTimer::timeout, this, &Lifx::onReconnectTimer); m_clientId = qrand(); - QFile file; - 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(); + m_socket = new QUdpSocket(this); - if (!productsJson.isArray()) { - qCWarning(dcLifx()) << "Products JSON is not a valid array"; - } - 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); - } + m_socket->setSocketOption(QAbstractSocket::MulticastTtlOption, QVariant(1)); + m_socket->setSocketOption(QAbstractSocket::MulticastLoopbackOption, QVariant(1)); } Lifx::~Lifx() @@ -86,20 +61,7 @@ Lifx::~Lifx() bool Lifx::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 = 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)){ qCWarning(dcLifx()) << "could not bind to port" << m_port; delete m_socket; @@ -107,8 +69,8 @@ bool Lifx::enable() return false; } - if(!m_socket->joinMulticastGroup(m_host)){ - qCWarning(dcLifx()) << "could not join multicast group" << m_host; + if(!m_socket->joinMulticastGroup(QHostAddress("239.255.255.250"))){ + qCWarning(dcLifx()) << "could not join multicast group"; delete m_socket; m_socket = nullptr; return false; @@ -118,31 +80,37 @@ bool Lifx::enable() return true; } -void Lifx::discoverDevices() +void Lifx::setHostAddress(const QHostAddress &address) { - Message message; - sendMessage(message); + m_host = address; } -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(msFadeTime) int requestId = qrand(); - + Message message; + sendMessage(message); return requestId; } -int Lifx::setColor(QColor color, int msFadeTime) +int Lifx::setColor(QColor color, uint msFadeTime) { Q_UNUSED(color) Q_UNUSED(msFadeTime) int requestId = qrand(); - + Message message; + sendMessage(message); return requestId; } -int Lifx::setBrightness(int percentage, int msFadeTime) +int Lifx::setBrightness(uint percentage, uint msFadeTime) { Q_UNUSED(percentage) Q_UNUSED(msFadeTime) @@ -152,12 +120,13 @@ int Lifx::setBrightness(int percentage, int msFadeTime) return requestId; } -int Lifx::setPower(bool power, int msFadeTime) +int Lifx::setPower(bool power, uint msFadeTime) { Q_UNUSED(power) Q_UNUSED(msFadeTime) int requestId = qrand(); - + Message message; + sendMessage(message); return requestId; } @@ -219,14 +188,13 @@ void Lifx::sendMessage(const Lifx::Message &message) //Finally another reserved field of 16 bits (2 bytes). header.append(2, '0'); - QByteArray payload; - - QByteArray message; - message.append(header); - message.append(payload); - message.append(message.length()); - //header = QByteArray::fromHex("0x310000340000000000000000000000000000000000000000000000000000000066000000005555FFFFFFFFAC0D00040000"); - m_socket->writeDatagram(message, m_host, m_port); + QByteArray fullMessage; + //message.append(header); + //message.append(payload); + //message.append(message.length()); + fullMessage = QByteArray::fromHex("0x310000340000000000000000000000000000000000000000000000000000000066000000005555FFFFFFFFAC0D00040000"); + std::reverse(fullMessage.begin(), fullMessage.end()); + m_socket->writeDatagram(fullMessage, m_host, m_port); } void Lifx::onStateChanged(QAbstractSocket::SocketState state) diff --git a/lifx/lifx.h b/lifx/lifx.h index 708a9c21..5af8b849 100644 --- a/lifx/lifx.h +++ b/lifx/lifx.h @@ -28,7 +28,6 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - #ifndef LIFX_H #define LIFX_H @@ -123,15 +122,16 @@ public: bool chain; }; - explicit Lifx(QObject *parent = nullptr); + explicit Lifx(const QHostAddress &address, quint16 port = 56700, QObject *parent = nullptr); ~Lifx(); bool enable(); + void setHostAddress(const QHostAddress &address); + void setPort(quint16 port); - void discoverDevices(); - int setColorTemperature(int kelvin, int msFadeTime=500); - int setColor(QColor color, int msFadeTime = 500); - int setBrightness(int percentage, int msFadeTime = 500); - int setPower(bool power, int msFadeTime = 500); + int setColorTemperature(uint kelvin, uint msFadeTime=500); + int setColor(QColor color, uint msFadeTime = 500); + int setBrightness(uint percentage, uint msFadeTime = 500); + int setPower(bool power, uint msFadeTime = 500); int flash(); int flash15s(); @@ -142,7 +142,6 @@ private: QHostAddress m_host; quint16 m_port; quint8 m_sequenceNumber = 0; - QHash m_lifxProducts; void sendMessage(const Message &message); @@ -153,7 +152,6 @@ private slots: signals: void connectionChanged(bool connected); - void deviceDiscovered(const QHostAddress &address, int port, const LifxProduct &product); //void requestExecuted(int requestId, bool success); //void errorReceived(int code, const QString &message); diff --git a/lifx/lifx.pro b/lifx/lifx.pro index 9dea64c2..802b5391 100644 --- a/lifx/lifx.pro +++ b/lifx/lifx.pro @@ -5,8 +5,10 @@ QT += network SOURCES += \ integrationpluginlifx.cpp \ lifx.cpp \ + lifxcloud.cpp \ HEADERS += \ integrationpluginlifx.h \ lifx.h \ + lifxcloud.h \ diff --git a/lifx/lifxcloud.cpp b/lifx/lifxcloud.cpp new file mode 100644 index 00000000..2fbba9da --- /dev/null +++ b/lifx/lifxcloud.cpp @@ -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 . +* +* 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 +#include +#include +#include +#include +#include + +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 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 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; +} diff --git a/lifx/lifxcloud.h b/lifx/lifxcloud.h new file mode 100644 index 00000000..545768a0 --- /dev/null +++ b/lifx/lifxcloud.h @@ -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 . +* +* 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 +#include +#include +#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 &lights); + void scenesListReceived(const QList &scenes); + void requestExecuted(int requestId, bool susccess); +}; + +#endif // LIFXCLOUD_H