diff --git a/debian/control b/debian/control index e1975c7a..87f28ea3 100644 --- a/debian/control +++ b/debian/control @@ -432,6 +432,21 @@ Description: nymea.io plugin for lgsmarttv This package will install the nymea.io plugin for lgsmarttv +Package: nymea-plugin-lifx +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin for lifx + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for lifx + + Package: nymea-plugin-mailnotification Architecture: any Depends: ${shlibs:Depends}, @@ -1045,6 +1060,7 @@ Depends: nymea-plugin-anel, nymea-plugin-genericthings, nymea-plugin-kodi, nymea-plugin-lgsmarttv, + nymea-plugin-lifx, nymea-plugin-mailnotification, nymea-plugin-texasinstruments, nymea-plugin-nanoleaf, diff --git a/debian/nymea-plugin-lifx.install.in b/debian/nymea-plugin-lifx.install.in new file mode 100644 index 00000000..d8f7c34a --- /dev/null +++ b/debian/nymea-plugin-lifx.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginlifx.so diff --git a/lifx/README.md b/lifx/README.md new file mode 100644 index 00000000..efe39ff9 --- /dev/null +++ b/lifx/README.md @@ -0,0 +1,18 @@ +# Lifx + +This plug-in integrates LIFX lights to nymea. + +## Supported Things + +* All LIFX lights + +## Requirements + +* LIFX cloud access token. + ** Get the token from https://cloud.lifx.com/settings +* Internet connection +* The package 'nymea-plugin-lifx' must be installed. + +## More + +https://www.lifx.com/ diff --git a/lifx/integrationpluginlifx.cpp b/lifx/integrationpluginlifx.cpp new file mode 100644 index 00000000..6f39f9e5 --- /dev/null +++ b/lifx/integrationpluginlifx.cpp @@ -0,0 +1,609 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "integrationpluginlifx.h" + +#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() +{ + +} + +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_idParamTypeIds.insert(colorBulbThingClassId, colorBulbThingIdParamTypeId); + m_idParamTypeIds.insert(dimmableBulbThingClassId, dimmableBulbThingIdParamTypeId); + + m_serviceBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_hap._tcp"); // discovers all homekit devices + + // TODO for LAN connection, get id and device features + // 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(); + // LifxLan::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, [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, QT_TR_NOOP("Please enter your user name and token. 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("The 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) +{ + // NOTE: the LAN API is not yet finished, to enable LAN discovery add "discovery" to the createMethods + 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; + } + + 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()); + } +} + +void IntegrationPluginLifx::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (thing->thingClassId() == colorBulbThingClassId || thing->thingClassId() == dimmableBulbThingClassId) { + if (thing->parentId().isNull()) { + // Lifx LAN + //LifxLan *lifx = new LifxLan(, this); + //if(lifx->enable()) { + // m_lifxLanConnections.insert(thing, lifx); + //TODO async setup for LAN devices + // info->finish(Thing::ThingErrorNoError); + //} else { + // lifx->deleteLater(); + info->finish(Thing::ThingErrorSetupFailed); + //} + } else { + // Lifx Cloud + info->finish(Thing::ThingErrorNoError); + } + } else if (thing->thingClassId() == lifxAccountThingClassId) { + + pluginStorage()->beginGroup(thing->id().toString()); + QByteArray token = pluginStorage()->value("token").toByteArray(); + QByteArray username = pluginStorage()->value("username").toByteArray(); + pluginStorage()->endGroup(); + + 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); + connect(lifxCloud, &LifxCloud::connectionChanged, this, &IntegrationPluginLifx::onLifxCloudConnectionChanged); + connect(lifxCloud, &LifxCloud::authenticationChanged, this, &IntegrationPluginLifx::onLifxCloudAuthenticationChanged); + lifxCloud->setAuthorizationToken(token); + lifxCloud->listLights(); + QTimer::singleShot(2000, info, [this, info] { + setupThing(info); + }); + } else { + Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginLifx::postSetupThing(Thing *thing) +{ + if (!m_pluginTimer) { + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(15); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { + foreach (LifxLan *lifx, m_lifxLanConnections) { + Q_UNUSED(lifx) + //TODO update LAN device states + } + foreach (LifxCloud *lifx, m_lifxCloudConnections) { + lifx->listLights(); + } + }); + } + + 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; + LifxLan *lifx = nullptr; + LifxCloud *lifxCloud = nullptr; + + if (m_lifxLanConnections.contains(thing)) { + // Local connection first + lifx = m_lifxLanConnections.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; + 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()){ + if (cloudDevice) { + lifxCloud->setPower(lightId, true); + } else { + lifx->setPower(true); + } + } + int brightness = info->action().param(colorBulbBrightnessActionBrightnessParamTypeId).value().toInt(); + 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 if (action.actionTypeId() == colorBulbColorActionColorParamTypeId) { + QRgb color = QColor(action.param(colorBulbColorActionColorParamTypeId).value().toString()).rgba(); + if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()){ + if (cloudDevice) { + lifxCloud->setPower(lightId, true); + } else { + 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() * 8); //range 2500 to 6500 kelvin + if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()){ + if (cloudDevice) { + lifxCloud->setPower(lightId, true); + } else { + 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 if (action.actionTypeId() == colorBulbEffectStateTypeId) { + if (!thing->stateValue(colorBulbPowerStateTypeId).toBool()){ + if (cloudDevice) { + lifxCloud->setPower(lightId, true); + } else { + lifx->setPower(true); + } + } + QString effectString = action.param(colorBulbEffectActionEffectParamTypeId).value().toString(); + int requestId; + LifxCloud::Effect effect = LifxCloud::EffectNone; + if (effectString == "None") { + effect = LifxCloud::EffectNone; + } else if (effectString == "Breathe") { + effect = LifxCloud::EffectBreathe; + } else if (effectString == "Pulse") { + effect = LifxCloud::EffectPulse; + } + if (cloudDevice) { + //QColor color = QColor(thing->stateValue(colorBulbColorStateTypeId).toString()); + requestId = lifxCloud->setEffect(lightId, effect, "#FFFFFF"); + } else { + qCWarning(dcLifx()) << "LAN devices are not yet supported"; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + connect(info, &ThingActionInfo::aborted, this, [requestId, this] {m_asyncActions.remove(requestId);}); + m_asyncActions.insert(requestId, info); + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8()); + } + } else if (thing->thingClassId() == dimmableBulbThingClassId) { + QByteArray lightId = thing->paramValue(dimmableBulbThingIdParamTypeId).toByteArray(); + if (action.actionTypeId() == dimmableBulbPowerActionTypeId) { + bool power = action.param(dimmableBulbPowerActionPowerParamTypeId).value().toBool(); + 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(colorBulbPowerStateTypeId).toBool()){ + if (cloudDevice) { + lifxCloud->setPower(lightId, true); + } else { + 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 { + Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8()); + } + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginLifx::thingRemoved(Thing *thing) +{ + if (thing->thingClassId() == colorBulbThingClassId || thing->thingClassId() == dimmableBulbThingClassId) { + if (m_lifxLanConnections.contains(thing)) + m_lifxLanConnections.take(thing)->deleteLater(); + } else if (thing->thingClassId() == lifxAccountThingClassId) { + if (m_lifxCloudConnections.contains(thing)) + m_lifxCloudConnections.take(thing)->deleteLater(); + } + + if (myThings().isEmpty()) { + 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::onLifxLanConnectionChanged(bool connected) +{ + Q_UNUSED(connected) + LifxLan *lifx = static_cast(sender()); + Thing *thing = m_lifxLanConnections.key(lifx); + if (!thing) + return; + thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), connected); +} + +void IntegrationPluginLifx::onLifxLanRequestExecuted(int requestId, bool success) +{ + if (m_asyncActions.contains(requestId)) { + ThingActionInfo *info = m_asyncActions.take(requestId); + if (success) { + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + } else if (m_asyncBrowserItem.contains(requestId)) { + BrowserActionInfo *info = m_asyncBrowserItem.take(requestId); + if (success) { + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareNotAvailable); + } + } +} + +void IntegrationPluginLifx::onLifxCloudConnectionChanged(bool connected) +{ + LifxCloud *lifxCloud = static_cast(sender()); + Thing *accountThing = m_lifxCloudConnections.key(lifxCloud); + if (!accountThing) + return; + accountThing->setStateValue(m_connectedStateTypeIds.value(accountThing->thingClassId()), connected); + + foreach (Thing *thing, myThings().filterByParentId(accountThing->id())) { + if (!connected) + thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), connected); + } +} + +void IntegrationPluginLifx::onLifxCloudAuthenticationChanged(bool authenticated) +{ + LifxCloud *lifxCloud = static_cast(sender()); + Thing *accountThing = m_lifxCloudConnections.key(lifxCloud); + if (!accountThing) + return; + accountThing->setStateValue(lifxAccountLoggedInStateTypeId, authenticated); +} + +void IntegrationPluginLifx::onLifxCloudLightsListReceived(const QList &lights) +{ + 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); + } + + 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*100.00); + thing->setStateValue(m_colorTemperatureStateTypeIds.value(thingClassId), light.colorTemperature); //TODO Kelvin to mired + 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); + params << Param(m_hostAddressParamTypeIds.value(thingDescriptor.thingClassId()), "-"); + params << Param(m_portParamTypeIds.value(thingDescriptor.thingClassId()), 0); + thingDescriptor.setParams(params); + thingDescriptors.append(thingDescriptor); + } + if (!thingDescriptors.isEmpty()) + autoThingsAppeared(thingDescriptors); +} + +void IntegrationPluginLifx::onLifxCloudRequestExecuted(int requestId, bool success) +{ + if (m_asyncActions.contains(requestId)) { + ThingActionInfo *info = m_asyncActions.take(requestId); + if (!info) { + return; + } + if (success) { + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareNotAvailable); + } + } else if (m_asyncBrowserItem.contains(requestId)) { + BrowserActionInfo *info = m_asyncBrowserItem.value(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 new file mode 100644 index 00000000..2b440d5d --- /dev/null +++ b/lifx/integrationpluginlifx.h @@ -0,0 +1,103 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 INTEGRATIONPLUGINLIFX_H +#define INTEGRATIONPLUGINLIFX_H + +#include "integrations/integrationplugin.h" +#include "plugintimer.h" +#include "lifxlan.h" +#include "lifxcloud.h" + +#include "network/networkaccessmanager.h" +#include "network/zeroconf/zeroconfservicebrowser.h" +#include "network/zeroconf/zeroconfserviceentry.h" + +#include + +class IntegrationPluginLifx : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginlifx.json") + Q_INTERFACES(IntegrationPlugin) + +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; + QHash m_lifxLanConnections; + 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 onLifxLanConnectionChanged(bool connected); + void onLifxLanRequestExecuted(int requestId, bool success); + + void onLifxCloudConnectionChanged(bool connected); + void onLifxCloudAuthenticationChanged(bool authenticated); + 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 new file mode 100644 index 00000000..b5e6c52c --- /dev/null +++ b/lifx/integrationpluginlifx.json @@ -0,0 +1,203 @@ +{ + "displayName": "LIFX", + "name": "Lifx", + "id": "4e00ee30-79e2-447b-8dcc-c34470f41992", + "vendors": [ + { + "name": "lifx", + "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": ["auto"], + "interfaces": ["colorlight", "connectable"], + "paramTypes": [ + { + "id": "976ecea0-ac25-47d4-9dc5-362962ddb6c0", + "name": "id", + "displayName": "ID", + "type" : "QString", + "readOnly": true + } + ], + "stateTypes": [ + { + "id": "dc4c1640-90f3-4fe0-af9b-db7fa105f18a", + "name": "connected", + "displayName": "Reachable", + "displayNameEvent": "Reachable changed", + "defaultValue": false, + "type": "bool", + "cached": false + }, + { + "id": "12de3f8f-2454-4057-aa12-9290296fdbdd", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "dd7d7e70-5552-4531-8789-2d0f750488be", + "name": "colorTemperature", + "displayName": "Color temperature", + "displayNameEvent": "Color temperature changed", + "displayNameAction": "Set color temperature", + "type": "int", + "unit": "Mired", + "defaultValue": 170, + "minValue": 153, + "maxValue": 500, + "writable": true + }, + { + "id": "a47d8164-5023-4ffb-8298-73293e93e7f6", + "name": "color", + "displayName": "Color", + "displayNameEvent": "Color changed", + "displayNameAction": "Set color", + "type": "QColor", + "defaultValue": "#000000", + "writable": true + }, + { + "id": "8bd20350-0e79-45dc-b68a-84da99356863", + "name": "brightness", + "displayName": "Brightness", + "displayNameEvent": "Brightness changed", + "displayNameAction": "Set brightness", + "type": "int", + "unit": "Percentage", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100, + "writable": true + }, + { + "id": "65f88396-2958-480e-b0be-c4695400a343", + "name": "effect", + "displayName": "Effect", + "displayNameEvent": "Effect changed", + "displayNameAction": "Set effect", + "type": "QString", + "defaultValue": "None", + "possibleValues": [ + "None", + "Breathe", + "Pulse" + ], + "writable": true + } + ] + }, + { + "id": "a5b02af8-7c97-4a78-9c78-bafee7407b5e", + "name": "dimmableBulb", + "displayName": "Day and Dusk", + "createMethods": ["auto"], + "interfaces": ["colortemperaturelight", "connectable"], + "paramTypes": [ + { + "id": "f157a97b-3fe5-4d9e-b5e3-5636f80d46ed", + "name": "id", + "displayName": "ID", + "type" : "QString", + "readOnly": true + } + ], + "stateTypes": [ + { + "id": "d33f98ef-5e0f-464c-afed-88b95cc701cd", + "name": "connected", + "displayName": "Reachable", + "displayNameEvent": "Reachable changed", + "defaultValue": false, + "type": "bool" + }, + { + "id": "9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "a0a1bdcc-2761-4d90-85d1-5ce887546611", + "name": "brightness", + "displayName": "Brightness", + "displayNameEvent": "Brightness changed", + "displayNameAction": "Set brightness", + "type": "int", + "unit": "Percentage", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100, + "writable": true + }, + { + "id": "95797dee-b836-4047-98d5-afbbce4f8c42", + "name": "colorTemperature", + "displayName": "Color temperature", + "displayNameEvent": "Color temperature changed", + "displayNameAction": "Set color temperature", + "type": "int", + "unit": "Mired", + "defaultValue": 170, + "minValue": 153, + "maxValue": 500, + "writable": true + } + ] + } + ] + } + ] +} diff --git a/lifx/lifx.png b/lifx/lifx.png new file mode 100644 index 00000000..89fbeb25 Binary files /dev/null and b/lifx/lifx.png differ diff --git a/lifx/lifx.pro b/lifx/lifx.pro new file mode 100644 index 00000000..20671af6 --- /dev/null +++ b/lifx/lifx.pro @@ -0,0 +1,14 @@ +include(../plugins.pri) + +QT += network + +SOURCES += \ + integrationpluginlifx.cpp \ + lifxcloud.cpp \ + lifxlan.cpp \ + +HEADERS += \ + integrationpluginlifx.h \ + lifxcloud.h \ + lifxlan.h \ + diff --git a/lifx/lifxcloud.cpp b/lifx/lifxcloud.cpp new file mode 100644 index 00000000..1aabda98 --- /dev/null +++ b/lifx/lifxcloud.cpp @@ -0,0 +1,373 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +#include + +LifxCloud::LifxCloud(NetworkAccessManager *networkManager, QObject *parent) : + QObject(parent), + m_networkManager(networkManager) +{ + +} + +void LifxCloud::setAuthorizationToken(const QByteArray &token) +{ + m_authorizationToken = token; +} + +bool LifxCloud::cloudAuthenticated() +{ + return m_authenticated; +} + +bool LifxCloud::cloudConnected() +{ + return m_connected; +} + +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] { + if(!checkHttpStatusCode(reply)) { + 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(); + if (object["power"].toString() == "on") { + light.power = true; + } else { + light.power = false; + } + 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] { + if(!checkHttpStatusCode(reply)) { + 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("uuid").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("id:"+lightId, StatePower, power, duration); +} + +int LifxCloud::setBrightnesss(const QString &lightId, int brightness, int duration) +{ + return setState("id:"+lightId, StateBrightness, brightness/100.00, duration); +} + +int LifxCloud::setColor(const QString &lightId, QColor color, int duration) +{ + return setState("id:"+lightId, StateColor, color.name(), duration); +} + +int LifxCloud::setColorTemperature(const QString &lightId, int kelvin, int duration) +{ + return setState("id:"+lightId, StateColorTemperature, kelvin, duration); +} + +int LifxCloud::setInfrared(const QString &lightId, int infrared, int duration) +{ + return setState("id:"+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); + QNetworkReply *reply = m_networkManager->put(request, ""); + connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { + emit requestExecuted(requestId, checkHttpStatusCode(reply)); + QByteArray rawData = reply->readAll(); + qCDebug(dcLifx()) << "Got activate scene reply" << rawData; + }); + return requestId; +} + +int LifxCloud::setEffect(const QString &lightId, LifxCloud::Effect effect, QColor color) +{ + if (m_authorizationToken.isEmpty()) { + qCWarning(dcLifx()) << "Authorization token is not set"; + return -1; + } + int requestId = qrand(); + QNetworkRequest request; + QUrlQuery params; + switch (effect) { + case LifxCloud::EffectNone: + request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/off").arg(lightId))); + break; + case LifxCloud::EffectBreathe: + request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/breathe").arg(lightId))); + params.addQueryItem("color", color.name().trimmed()); + params.addQueryItem("period", "2"); + params.addQueryItem("cycles", "3"); + break; + case LifxCloud::EffectMove: + request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/move").arg(lightId))); + break; + case LifxCloud::EffectMorph: + request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/morph").arg(lightId))); + break; + case LifxCloud::EffectFlame: + request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/flame").arg(lightId))); + break; + case LifxCloud::EffectPulse: + request.setUrl(QUrl(QString("https://api.lifx.com/v1/lights/id:%1/effects/pulse").arg(lightId))); + params.addQueryItem("color", color.name().trimmed()); + params.addQueryItem("period", "2"); + params.addQueryItem("cycles", "3"); + break; + } + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/x-www-form-urlencoded."); + request.setRawHeader("Authorization","Bearer "+m_authorizationToken); + qCDebug(dcLifx()) << "Set effect request" << request.url() << params.toString().toUtf8(); + + QNetworkReply *reply = m_networkManager->post(request, params.toString().toUtf8()); + connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] { + + QByteArray rawData = reply->readAll(); + qCDebug(dcLifx()) << "Got set effect reply" << rawData; + emit requestExecuted(requestId, checkHttpStatusCode(reply)); + }); + 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"; + + qCDebug(dcLifx()) << "Set state power" << stateValue.toBool(); + break; + case StateBrightness: + payload["brightness"] = stateValue.toDouble(); + qCDebug(dcLifx()) << "Set state brightness" << stateValue; + break; + case StateColor: + payload["color"] = stateValue.toString(); + qCDebug(dcLifx()) << "Set state color" << stateValue; + break; + case StateColorTemperature: + payload["color"] = "kelvin:"+stateValue.toString(); + qCDebug(dcLifx()) << "Set state color" << stateValue; + break; + case StateInfrared: + payload["infrared"] = stateValue.toDouble(); + qCDebug(dcLifx()) << "Set state infrared" << stateValue; + } + + doc.setObject(payload); + qCDebug(dcLifx()) << "Set state request" << request.url() << doc.toJson(); + QNetworkReply *reply = m_networkManager->put(request, doc.toJson()); + connect(reply, &QNetworkReply::finished, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [requestId, duration,reply, this] { + + QByteArray rawData = reply->readAll(); + qCDebug(dcLifx()) << "Got set state reply" << rawData; + if (checkHttpStatusCode(reply)) { + emit requestExecuted(requestId, true); + QTimer::singleShot(duration*1000+500, this, [=] {listLights();}); + } else { + emit requestExecuted(requestId, false); + } + }); + return requestId; +} + +bool LifxCloud::checkHttpStatusCode(QNetworkReply *reply) +{ + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (reply->error() != QNetworkReply::NoError) { + qCWarning(dcLifx()) << "Request error:" << status << reply->errorString(); + if (m_connected) { + m_connected = false; + emit connectionChanged(false); + } + return false; + } + // check HTTP status code + if (status == 401 || status == 403) { + if (m_authenticated) { + m_authenticated = false; + emit authenticationChanged(false); + } + } + if (status > 207) { + qCWarning(dcLifx()) << "Error get scene list" << status; + return false; + } + if (!m_authenticated) { + m_authenticated = true; + emit authenticationChanged(true); + } + if (!m_connected) { + m_connected = true; + emit connectionChanged(true); + } + return true; +} diff --git a/lifx/lifxcloud.h b/lifx/lifxcloud.h new file mode 100644 index 00000000..df95369b --- /dev/null +++ b/lifx/lifxcloud.h @@ -0,0 +1,140 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 + }; + + enum Effect { + EffectNone, + EffectBreathe, + EffectMove, + EffectMorph, + EffectFlame, + EffectPulse + }; + + 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); + bool cloudAuthenticated(); + bool cloudConnected(); + + void listLights(); + void listScenes(); + int setPower(const QString &lightId, bool power, int duration = 0); + int setBrightnesss(const QString &lightId, int brightness, int duration = 0); + int setColor(const QString &lightId, QColor color, int duration = 0); + int setColorTemperature(const QString &lightId, int kelvin, int duration = 0); + int setInfrared(const QString &lightId, int infrared, int duration = 0); + + int activateScene(const QString &sceneId); + + int setEffect(const QString &lightId, Effect effect, QColor color = "#FFFFFF"); + +private: + NetworkAccessManager *m_networkManager = nullptr; + QByteArray m_authorizationToken; + + int setState(const QString &lightId, State state, QVariant stateValue, int duration); + bool checkHttpStatusCode(QNetworkReply *reply); + bool m_authenticated = false; + bool m_connected = false; +signals: + void connectionChanged(bool m_connected); + void authenticationChanged(bool m_authenticated); + void lightsListReceived(const QList &lights); + void scenesListReceived(const QList &scenes); + void requestExecuted(int requestId, bool susccess); +}; + +#endif // LIFXCLOUD_H diff --git a/lifx/lifxlan.cpp b/lifx/lifxlan.cpp new file mode 100644 index 00000000..2c707627 --- /dev/null +++ b/lifx/lifxlan.cpp @@ -0,0 +1,201 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "lifxlan.h" +#include "extern-plugininfo.h" + +#include + +LifxLan::LifxLan(const QHostAddress &address, quint16 port, QObject *parent) : + QObject(parent), + m_host(address), + m_port(port) +{ + m_clientId = qrand(); + + m_socket = new QUdpSocket(this); + + m_socket->setSocketOption(QAbstractSocket::MulticastTtlOption, QVariant(1)); + m_socket->setSocketOption(QAbstractSocket::MulticastLoopbackOption, QVariant(1)); +} + +LifxLan::~LifxLan() +{ + if (m_socket) { + m_socket->waitForBytesWritten(1000); + m_socket->close(); + } +} + +bool LifxLan::enable() +{ + // Bind udp socket and join multicast group + if(!m_socket->bind(QHostAddress::AnyIPv4, m_port, QUdpSocket::ShareAddress)){ + qCWarning(dcLifx()) << "could not bind to port" << m_port; + delete m_socket; + m_socket = nullptr; + return false; + } + + if(!m_socket->joinMulticastGroup(QHostAddress("239.255.255.250"))){ + qCWarning(dcLifx()) << "could not join multicast group"; + delete m_socket; + m_socket = nullptr; + return false; + } + connect(m_socket, &QUdpSocket::readyRead, this, &LifxLan::onReadyRead); + return true; +} + +void LifxLan::setHostAddress(const QHostAddress &address) +{ + m_host = address; +} + +void LifxLan::setPort(quint16 port) +{ + m_port = port; +} + +int LifxLan::setColorTemperature(uint mirad, uint msFadeTime) +{ + Q_UNUSED(mirad) + Q_UNUSED(msFadeTime) + int requestId = qrand(); + Message message; + sendMessage(message); + return requestId; +} + +int LifxLan::setColor(QColor color, uint msFadeTime) +{ + Q_UNUSED(color) + Q_UNUSED(msFadeTime) + int requestId = qrand(); + Message message; + //TODO create LAN message + sendMessage(message); + return requestId; +} + +int LifxLan::setBrightness(uint percentage, uint msFadeTime) +{ + Q_UNUSED(percentage) + Q_UNUSED(msFadeTime) + int requestId = qrand(); + Message message; + sendMessage(message); + //TODO create LAN message + return requestId; +} + +int LifxLan::setPower(bool power, uint msFadeTime) +{ + Q_UNUSED(power) + Q_UNUSED(msFadeTime) + int requestId = qrand(); + Message message; + sendMessage(message); + //TODO create LAN message + return requestId; +} + +void LifxLan::sendMessage(const LifxLan::Message &message) +{ + QByteArray header; + // -- FRAME -- + // Protocol number: must be 1024 (decimal) + quint16 protocol = 1024; + protocol |= (0x0001 << 4); //Message includes a target address: must be one (1) + protocol |= (message.frame.Tagged << 5); // Determines usage of the Frame Address target field + protocol &= ~(0x0003); // Message origin indicator: must be zero (0) + header.append(protocol >> 8); + header.append(protocol & 0xff); + + //Source identifier: unique value set by the client, used by responses + header.append(m_clientId); + + // -- FRAME ADDRESS -- + //Target - frame address starts with 64 bits + + + //ADD RESERVED SECTION a reserved section of 48 bits (6 bytes) + //header.append(6, '\x00'); //that must be all zeros. + + //ADD ACK and RES + //header.append(2, '\x01'); + + //ADD SEQUENCE NUMBER 1Byte + //header.append(m_sequenceNumber++); + + //Protocol header. which begins with 64 reserved bits (8 bytes). Set these all to zero. + //header.append(8, '\x00'); //that must be all zeros. + + //ADD MESSAGE TYPE + //header.append(static_cast(LightMessages::SetColor)); + + // Finally another reserved field of 16 bits (2 bytes). + //header.append(2, '\x00'); + + //ADD SIZE + //header.append(((static_cast(header.length()+1) & 0xff00) >> 8)); + //header.append((static_cast(header.length()+1) & 0x00ff)); + + //Finally another reserved field of 16 bits (2 bytes). + //header.append(2, '\x00'); + + QByteArray fullMessage; + //fullMessage = QByteArray::fromHex("0x310000340000000000000000000000000000000000000000000000000000000066000000005555FFFFFFFFAC0D00040000"); // test message - set all lights green + //std::reverse(fullMessage.begin(), fullMessage.end()); + m_socket->writeDatagram(fullMessage, m_host, m_port); +} + +void LifxLan::onStateChanged(QAbstractSocket::SocketState state) +{ + switch (state) { + case QAbstractSocket::SocketState::ConnectedState: + emit connectionChanged(true); + break; + case QAbstractSocket::SocketState::UnconnectedState: + m_reconnectTimer->start(10 * 1000); + emit connectionChanged(false); + break; + default: + emit connectionChanged(false); + break; + } +} + +void LifxLan::onReadyRead() +{ + QByteArray data = m_socket->readAll(); + qCDebug(dcLifx()) << "Message received" << data; +} diff --git a/lifx/lifxlan.h b/lifx/lifxlan.h new file mode 100644 index 00000000..e15dea84 --- /dev/null +++ b/lifx/lifxlan.h @@ -0,0 +1,154 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 LIFXLAN_H +#define LIFXLAN_H + +#include +#include +#include +#include + +#include "network/networkaccessmanager.h" + +#include + +class LifxLan : public QObject +{ + Q_OBJECT +public: + +#pragma pack(push, 1) + typedef struct { + /* frame */ + uint16_t size; + uint16_t protocol:12; + uint8_t addressable:1; + uint8_t tagged:1; + uint8_t origin:2; + uint32_t source; + /* frame address */ + uint8_t target[8]; + uint8_t reserved[6]; + uint8_t res_required:1; + uint8_t ack_required:1; + uint8_t :6; + uint8_t sequence; + /* protocol header */ + uint64_t :64; + uint16_t type; + uint16_t :16; + /* variable length payload follows */ + } ProtocolHeader_t; +#pragma pack(pop) + + struct Frame { + //quint16 Size; //Size of entire message in bytes including this field + //quint16 Protocol; //Protocol number: must be 1024 (decimal) + //bool Addressable; //Message includes a target address: must be one (1) + bool Tagged; //Determines usage of the Frame Address target field + //quint8 Origin; //Message origin indicator: must be zero (0) + quint32 Source; //Source identifier: unique value set by the client, used by responses + }; + + struct FrameAddress { + quint64 Target; //6 byte device address (MAC address) or zero (0) means all devices. The last two bytes should be 0 bytes. + bool ResponseRequired; //Response message required + bool AckRequired; //Acknowledgement message required + quint8 Sequence; //Wrap around message sequence number + }; + + struct ProtocolHeader { + quint16 Type; //Message type determines the payload being used + }; + + struct Message { + Frame frame; + FrameAddress frameAddress; + ProtocolHeader protocolHeader; + QByteArray payload; + }; + + enum LightMessages { + Get = 101, + SetColor = 102, + SetWaveform = 103, + SetWaveformOptional = 119, + State = 107, + GetPower = 116, + SetPower = 117, + StatePower = 118, + GetInfrared = 120, + StateInfrared = 121, + SetInfrared = 122 + }; + + struct LifxProduct { + int pid; + QString name; + bool color; + bool infrared; + bool matrix; + bool multizone; + uint minColorTemperature; + uint maxColorTemperature; + bool chain; + }; + + explicit LifxLan(const QHostAddress &address, quint16 port = 56700, QObject *parent = nullptr); + ~LifxLan(); + bool enable(); + void setHostAddress(const QHostAddress &address); + void setPort(quint16 port); + + 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); + +private: + quint32 m_clientId = 0; + QTimer *m_reconnectTimer = nullptr; + QUdpSocket *m_socket = nullptr; + QHostAddress m_host; + quint16 m_port; + quint8 m_sequenceNumber = 0; + + void sendMessage(const Message &message); + +private slots: + void onStateChanged(QAbstractSocket::SocketState state); + void onReadyRead(); + +signals: + void connectionChanged(bool connected); + void requestExecuted(int requestId, bool success); +}; +#endif // LIFXLAN_H diff --git a/lifx/meta.json b/lifx/meta.json new file mode 100644 index 00000000..37064e23 --- /dev/null +++ b/lifx/meta.json @@ -0,0 +1,13 @@ +{ + "title": "LIFX", + "tagline": "Control LIFX light bulbs.", + "icon": "lifx.png", + "stability": "consumer", + "offline": false, + "technologies": [ + "network" + ], + "categories": [ + "light" + ] +} diff --git a/lifx/products.json b/lifx/products.json new file mode 100644 index 00000000..99f8a77a --- /dev/null +++ b/lifx/products.json @@ -0,0 +1,373 @@ +[ + { + "vid": 1, + "name": "LIFX", + "products": [ + { + "pid": 1, + "name": "Original 1000", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 3, + "name": "Color 650", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 10, + "name": "White 800 (Low Voltage)", + "features": { + "color": false, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2700, 6500], + "chain": false + } + }, + { + "pid": 11, + "name": "White 800 (High Voltage)", + "features": { + "color": false, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2700, 6500], + "chain": false + } + }, + { + "pid": 18, + "name": "White 900 BR30 (Low Voltage)", + "features": { + "color": false, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2700, 6500], + "chain": false + } + }, + { + "pid": 20, + "name": "Color 1000 BR30", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 22, + "name": "Color 1000", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 27, + "name": "LIFX A19", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 28, + "name": "LIFX BR30", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 29, + "name": "LIFX+ A19", + "features": { + "color": true, + "infrared": true, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 30, + "name": "LIFX+ BR30", + "features": { + "color": true, + "infrared": true, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 31, + "name": "LIFX Z", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": true, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 32, + "name": "LIFX Z 2", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": true, + "temperature_range": [2500, 9000], + "chain": false, + "min_ext_mz_firmware": 1532997580, + "min_ext_mz_firmware_components": [2, 77] + } + }, + { + "pid": 36, + "name": "LIFX Downlight", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 37, + "name": "LIFX Downlight", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 38, + "name": "LIFX Beam", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": true, + "temperature_range": [2500, 9000], + "chain": false, + "min_ext_mz_firmware": 1532997580, + "min_ext_mz_firmware_components": [2, 77] + } + }, + { + "pid": 43, + "name": "LIFX A19", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 44, + "name": "LIFX BR30", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 45, + "name": "LIFX+ A19", + "features": { + "color": true, + "infrared": true, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 46, + "name": "LIFX+ BR30", + "features": { + "color": true, + "infrared": true, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 49, + "name": "LIFX Mini", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 50, + "name": "LIFX Mini Day and Dusk", + "features": { + "color": false, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [1500, 4000], + "chain": false + } + }, + { + "pid": 51, + "name": "LIFX Mini White", + "features": { + "color": false, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2700, 2700], + "chain": false + } + }, + { + "pid": 52, + "name": "LIFX GU10", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 55, + "name": "LIFX Tile", + "features": { + "color": true, + "infrared": false, + "matrix": true, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": true + } + }, + { + "pid": 57, + "name": "LIFX Candle", + "features": { + "color": true, + "infrared": false, + "matrix": true, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 59, + "name": "LIFX Mini Color", + "features": { + "color": true, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + }, + { + "pid": 60, + "name": "LIFX Mini Day and Dusk", + "features": { + "color": false, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [1500, 4000], + "chain": false + } + }, + { + "pid": 61, + "name": "LIFX Mini White", + "features": { + "color": false, + "infrared": false, + "matrix": false, + "multizone": false, + "temperature_range": [2700, 2700], + "chain": false + } + }, + { + "pid": 68, + "name": "LIFX Candle", + "features": { + "color": true, + "infrared": false, + "matrix": true, + "multizone": false, + "temperature_range": [2500, 9000], + "chain": false + } + } + ] + } +] + diff --git a/lifx/translations/4e00ee30-79e2-447b-8dcc-c34470f41992-de.ts b/lifx/translations/4e00ee30-79e2-447b-8dcc-c34470f41992-de.ts new file mode 100644 index 00000000..70809735 --- /dev/null +++ b/lifx/translations/4e00ee30-79e2-447b-8dcc-c34470f41992-de.ts @@ -0,0 +1,227 @@ + + + + + IntegrationPluginLifx + + LIFX server is not reachable. + LIFX Server ist nicht erreichbar + + + Please enter your user name and token. Get the token from https://cloud.lifx.com/settings + Bitte geben Sie Ihren Lifx Benutzernamen und Token ein. Holen Sie sich Ihren Token von https://cloud.lifx.com/settings + + + The token is invalid. + Der Token ist ungültig. + + + + Lifx + + Brightness + The name of the ParamType (ThingClass: dimmableBulb, ActionType: brightness, ID: {a0a1bdcc-2761-4d90-85d1-5ce887546611}) +---------- +The name of the ParamType (ThingClass: dimmableBulb, EventType: brightness, ID: {a0a1bdcc-2761-4d90-85d1-5ce887546611}) +---------- +The name of the StateType ({a0a1bdcc-2761-4d90-85d1-5ce887546611}) of ThingClass dimmableBulb +---------- +The name of the ParamType (ThingClass: colorBulb, ActionType: brightness, ID: {8bd20350-0e79-45dc-b68a-84da99356863}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: brightness, ID: {8bd20350-0e79-45dc-b68a-84da99356863}) +---------- +The name of the StateType ({8bd20350-0e79-45dc-b68a-84da99356863}) of ThingClass colorBulb + Helligkeit + + + Brightness changed + The name of the EventType ({a0a1bdcc-2761-4d90-85d1-5ce887546611}) of ThingClass dimmableBulb +---------- +The name of the EventType ({8bd20350-0e79-45dc-b68a-84da99356863}) of ThingClass colorBulb + Helligkeit geändert + + + Color + The name of the ParamType (ThingClass: colorBulb, ActionType: color, ID: {a47d8164-5023-4ffb-8298-73293e93e7f6}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: color, ID: {a47d8164-5023-4ffb-8298-73293e93e7f6}) +---------- +The name of the StateType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb +---------- +The name of the ThingClass ({12907c9c-e7f0-47f2-bd58-39d52ffdf24e}) + Farbe + + + Color changed + The name of the EventType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb + Farbe geändert + + + Color temperature + The name of the ParamType (ThingClass: dimmableBulb, ActionType: colorTemperature, ID: {95797dee-b836-4047-98d5-afbbce4f8c42}) +---------- +The name of the ParamType (ThingClass: dimmableBulb, EventType: colorTemperature, ID: {95797dee-b836-4047-98d5-afbbce4f8c42}) +---------- +The name of the StateType ({95797dee-b836-4047-98d5-afbbce4f8c42}) of ThingClass dimmableBulb +---------- +The name of the ParamType (ThingClass: colorBulb, ActionType: colorTemperature, ID: {dd7d7e70-5552-4531-8789-2d0f750488be}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: colorTemperature, ID: {dd7d7e70-5552-4531-8789-2d0f750488be}) +---------- +The name of the StateType ({dd7d7e70-5552-4531-8789-2d0f750488be}) of ThingClass colorBulb + Farbtemperatur + + + Color temperature changed + The name of the EventType ({95797dee-b836-4047-98d5-afbbce4f8c42}) of ThingClass dimmableBulb +---------- +The name of the EventType ({dd7d7e70-5552-4531-8789-2d0f750488be}) of ThingClass colorBulb + Farbtemperatur geändert + + + Day and Dusk + The name of the ThingClass ({a5b02af8-7c97-4a78-9c78-bafee7407b5e}) + Tag und Sonnenaufgang + + + LIFX + The name of the vendor ({e5e48c0d-cff7-4c0f-983e-d23bd3e4ba87}) +---------- +The name of the plugin Lifx ({4e00ee30-79e2-447b-8dcc-c34470f41992}) + LIFX + + + Power + The name of the ParamType (ThingClass: dimmableBulb, ActionType: power, ID: {9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) +---------- +The name of the ParamType (ThingClass: dimmableBulb, EventType: power, ID: {9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) +---------- +The name of the StateType ({9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) of ThingClass dimmableBulb +---------- +The name of the ParamType (ThingClass: colorBulb, ActionType: power, ID: {12de3f8f-2454-4057-aa12-9290296fdbdd}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: power, ID: {12de3f8f-2454-4057-aa12-9290296fdbdd}) +---------- +The name of the StateType ({12de3f8f-2454-4057-aa12-9290296fdbdd}) of ThingClass colorBulb + Eingeschalten + + + Power changed + The name of the EventType ({9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) of ThingClass dimmableBulb +---------- +The name of the EventType ({12de3f8f-2454-4057-aa12-9290296fdbdd}) of ThingClass colorBulb + Eingeschalten changed + + + Reachable + The name of the ParamType (ThingClass: dimmableBulb, EventType: connected, ID: {d33f98ef-5e0f-464c-afed-88b95cc701cd}) +---------- +The name of the StateType ({d33f98ef-5e0f-464c-afed-88b95cc701cd}) of ThingClass dimmableBulb +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: connected, ID: {dc4c1640-90f3-4fe0-af9b-db7fa105f18a}) +---------- +The name of the StateType ({dc4c1640-90f3-4fe0-af9b-db7fa105f18a}) of ThingClass colorBulb + Erreichbar + + + Reachable changed + The name of the EventType ({d33f98ef-5e0f-464c-afed-88b95cc701cd}) of ThingClass dimmableBulb +---------- +The name of the EventType ({dc4c1640-90f3-4fe0-af9b-db7fa105f18a}) of ThingClass colorBulb + Erreichbar changed + + + Set color + The name of the ActionType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb + Setze Farbe + + + Set color temperature + The name of the ActionType ({95797dee-b836-4047-98d5-afbbce4f8c42}) of ThingClass dimmableBulb +---------- +The name of the ActionType ({dd7d7e70-5552-4531-8789-2d0f750488be}) of ThingClass colorBulb + Setze Farbtemperatur + + + Set effect + The name of the ActionType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb + Setze Effekt + + + Set power + The name of the ActionType ({9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) of ThingClass dimmableBulb +---------- +The name of the ActionType ({12de3f8f-2454-4057-aa12-9290296fdbdd}) of ThingClass colorBulb + Setze Eingeschalten + + + Effect + The name of the ParamType (ThingClass: colorBulb, ActionType: effect, ID: {65f88396-2958-480e-b0be-c4695400a343}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: effect, ID: {65f88396-2958-480e-b0be-c4695400a343}) +---------- +The name of the StateType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb + Effekt + + + Effect changed + The name of the EventType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb + Effekt geändert + + + ID + The name of the ParamType (ThingClass: dimmableBulb, Type: thing, ID: {f157a97b-3fe5-4d9e-b5e3-5636f80d46ed}) +---------- +The name of the ParamType (ThingClass: colorBulb, Type: thing, ID: {976ecea0-ac25-47d4-9dc5-362962ddb6c0}) + ID + + + Set brightness + The name of the ActionType ({a0a1bdcc-2761-4d90-85d1-5ce887546611}) of ThingClass dimmableBulb +---------- +The name of the ActionType ({8bd20350-0e79-45dc-b68a-84da99356863}) of ThingClass colorBulb + Setze Helligkeit + + + Connected + The name of the ParamType (ThingClass: lifxAccount, EventType: connected, ID: {3e7b358b-d7de-4db4-8a3a-b9860eae186f}) +---------- +The name of the StateType ({3e7b358b-d7de-4db4-8a3a-b9860eae186f}) of ThingClass lifxAccount + Verbunden + + + Connected changed + The name of the EventType ({3e7b358b-d7de-4db4-8a3a-b9860eae186f}) of ThingClass lifxAccount + Verbunden geändert + + + LIFX cloud account + The name of the ThingClass ({387c87f6-3e5b-4d6a-ba4d-372d0efad79f}) + LIFX Cloud-Account + + + Logged in + The name of the ParamType (ThingClass: lifxAccount, EventType: loggedIn, ID: {0db34069-5de0-4233-baec-27f039228524}) +---------- +The name of the StateType ({0db34069-5de0-4233-baec-27f039228524}) of ThingClass lifxAccount + Eingelogged + + + Logged in changed + The name of the EventType ({0db34069-5de0-4233-baec-27f039228524}) of ThingClass lifxAccount + Eingelogged geändert + + + User name + The name of the ParamType (ThingClass: lifxAccount, EventType: userDisplayName, ID: {554afd9b-a2ec-4d28-9065-2b9ab3a9e3b2}) +---------- +The name of the StateType ({554afd9b-a2ec-4d28-9065-2b9ab3a9e3b2}) of ThingClass lifxAccount + Benutzername + + + User name changed + The name of the EventType ({554afd9b-a2ec-4d28-9065-2b9ab3a9e3b2}) of ThingClass lifxAccount + Benutzername geändert + + + diff --git a/lifx/translations/4e00ee30-79e2-447b-8dcc-c34470f41992-en_US.ts b/lifx/translations/4e00ee30-79e2-447b-8dcc-c34470f41992-en_US.ts new file mode 100644 index 00000000..f869d2e1 --- /dev/null +++ b/lifx/translations/4e00ee30-79e2-447b-8dcc-c34470f41992-en_US.ts @@ -0,0 +1,227 @@ + + + + + IntegrationPluginLifx + + LIFX server is not reachable. + + + + Please enter your user name and token. Get the token from https://cloud.lifx.com/settings + + + + The token is invalid. + + + + + Lifx + + Brightness + The name of the ParamType (ThingClass: dimmableBulb, ActionType: brightness, ID: {a0a1bdcc-2761-4d90-85d1-5ce887546611}) +---------- +The name of the ParamType (ThingClass: dimmableBulb, EventType: brightness, ID: {a0a1bdcc-2761-4d90-85d1-5ce887546611}) +---------- +The name of the StateType ({a0a1bdcc-2761-4d90-85d1-5ce887546611}) of ThingClass dimmableBulb +---------- +The name of the ParamType (ThingClass: colorBulb, ActionType: brightness, ID: {8bd20350-0e79-45dc-b68a-84da99356863}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: brightness, ID: {8bd20350-0e79-45dc-b68a-84da99356863}) +---------- +The name of the StateType ({8bd20350-0e79-45dc-b68a-84da99356863}) of ThingClass colorBulb + + + + Brightness changed + The name of the EventType ({a0a1bdcc-2761-4d90-85d1-5ce887546611}) of ThingClass dimmableBulb +---------- +The name of the EventType ({8bd20350-0e79-45dc-b68a-84da99356863}) of ThingClass colorBulb + + + + Color + The name of the ParamType (ThingClass: colorBulb, ActionType: color, ID: {a47d8164-5023-4ffb-8298-73293e93e7f6}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: color, ID: {a47d8164-5023-4ffb-8298-73293e93e7f6}) +---------- +The name of the StateType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb +---------- +The name of the ThingClass ({12907c9c-e7f0-47f2-bd58-39d52ffdf24e}) + + + + Color changed + The name of the EventType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb + + + + Color temperature + The name of the ParamType (ThingClass: dimmableBulb, ActionType: colorTemperature, ID: {95797dee-b836-4047-98d5-afbbce4f8c42}) +---------- +The name of the ParamType (ThingClass: dimmableBulb, EventType: colorTemperature, ID: {95797dee-b836-4047-98d5-afbbce4f8c42}) +---------- +The name of the StateType ({95797dee-b836-4047-98d5-afbbce4f8c42}) of ThingClass dimmableBulb +---------- +The name of the ParamType (ThingClass: colorBulb, ActionType: colorTemperature, ID: {dd7d7e70-5552-4531-8789-2d0f750488be}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: colorTemperature, ID: {dd7d7e70-5552-4531-8789-2d0f750488be}) +---------- +The name of the StateType ({dd7d7e70-5552-4531-8789-2d0f750488be}) of ThingClass colorBulb + + + + Color temperature changed + The name of the EventType ({95797dee-b836-4047-98d5-afbbce4f8c42}) of ThingClass dimmableBulb +---------- +The name of the EventType ({dd7d7e70-5552-4531-8789-2d0f750488be}) of ThingClass colorBulb + + + + Day and Dusk + The name of the ThingClass ({a5b02af8-7c97-4a78-9c78-bafee7407b5e}) + + + + LIFX + The name of the vendor ({e5e48c0d-cff7-4c0f-983e-d23bd3e4ba87}) +---------- +The name of the plugin Lifx ({4e00ee30-79e2-447b-8dcc-c34470f41992}) + + + + Power + The name of the ParamType (ThingClass: dimmableBulb, ActionType: power, ID: {9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) +---------- +The name of the ParamType (ThingClass: dimmableBulb, EventType: power, ID: {9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) +---------- +The name of the StateType ({9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) of ThingClass dimmableBulb +---------- +The name of the ParamType (ThingClass: colorBulb, ActionType: power, ID: {12de3f8f-2454-4057-aa12-9290296fdbdd}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: power, ID: {12de3f8f-2454-4057-aa12-9290296fdbdd}) +---------- +The name of the StateType ({12de3f8f-2454-4057-aa12-9290296fdbdd}) of ThingClass colorBulb + + + + Power changed + The name of the EventType ({9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) of ThingClass dimmableBulb +---------- +The name of the EventType ({12de3f8f-2454-4057-aa12-9290296fdbdd}) of ThingClass colorBulb + + + + Reachable + The name of the ParamType (ThingClass: dimmableBulb, EventType: connected, ID: {d33f98ef-5e0f-464c-afed-88b95cc701cd}) +---------- +The name of the StateType ({d33f98ef-5e0f-464c-afed-88b95cc701cd}) of ThingClass dimmableBulb +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: connected, ID: {dc4c1640-90f3-4fe0-af9b-db7fa105f18a}) +---------- +The name of the StateType ({dc4c1640-90f3-4fe0-af9b-db7fa105f18a}) of ThingClass colorBulb + + + + Reachable changed + The name of the EventType ({d33f98ef-5e0f-464c-afed-88b95cc701cd}) of ThingClass dimmableBulb +---------- +The name of the EventType ({dc4c1640-90f3-4fe0-af9b-db7fa105f18a}) of ThingClass colorBulb + + + + Set color + The name of the ActionType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb + + + + Set color temperature + The name of the ActionType ({95797dee-b836-4047-98d5-afbbce4f8c42}) of ThingClass dimmableBulb +---------- +The name of the ActionType ({dd7d7e70-5552-4531-8789-2d0f750488be}) of ThingClass colorBulb + + + + Set effect + The name of the ActionType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb + + + + Set power + The name of the ActionType ({9e1344ea-cd05-4dd8-8948-8d2f5e00e1b0}) of ThingClass dimmableBulb +---------- +The name of the ActionType ({12de3f8f-2454-4057-aa12-9290296fdbdd}) of ThingClass colorBulb + + + + Effect + The name of the ParamType (ThingClass: colorBulb, ActionType: effect, ID: {65f88396-2958-480e-b0be-c4695400a343}) +---------- +The name of the ParamType (ThingClass: colorBulb, EventType: effect, ID: {65f88396-2958-480e-b0be-c4695400a343}) +---------- +The name of the StateType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb + + + + Effect changed + The name of the EventType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb + + + + ID + The name of the ParamType (ThingClass: dimmableBulb, Type: thing, ID: {f157a97b-3fe5-4d9e-b5e3-5636f80d46ed}) +---------- +The name of the ParamType (ThingClass: colorBulb, Type: thing, ID: {976ecea0-ac25-47d4-9dc5-362962ddb6c0}) + + + + Set brightness + The name of the ActionType ({a0a1bdcc-2761-4d90-85d1-5ce887546611}) of ThingClass dimmableBulb +---------- +The name of the ActionType ({8bd20350-0e79-45dc-b68a-84da99356863}) of ThingClass colorBulb + + + + Connected + The name of the ParamType (ThingClass: lifxAccount, EventType: connected, ID: {3e7b358b-d7de-4db4-8a3a-b9860eae186f}) +---------- +The name of the StateType ({3e7b358b-d7de-4db4-8a3a-b9860eae186f}) of ThingClass lifxAccount + + + + Connected changed + The name of the EventType ({3e7b358b-d7de-4db4-8a3a-b9860eae186f}) of ThingClass lifxAccount + + + + LIFX cloud account + The name of the ThingClass ({387c87f6-3e5b-4d6a-ba4d-372d0efad79f}) + + + + Logged in + The name of the ParamType (ThingClass: lifxAccount, EventType: loggedIn, ID: {0db34069-5de0-4233-baec-27f039228524}) +---------- +The name of the StateType ({0db34069-5de0-4233-baec-27f039228524}) of ThingClass lifxAccount + + + + Logged in changed + The name of the EventType ({0db34069-5de0-4233-baec-27f039228524}) of ThingClass lifxAccount + + + + User name + The name of the ParamType (ThingClass: lifxAccount, EventType: userDisplayName, ID: {554afd9b-a2ec-4d28-9065-2b9ab3a9e3b2}) +---------- +The name of the StateType ({554afd9b-a2ec-4d28-9065-2b9ab3a9e3b2}) of ThingClass lifxAccount + + + + User name changed + The name of the EventType ({554afd9b-a2ec-4d28-9065-2b9ab3a9e3b2}) of ThingClass lifxAccount + + + + diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 1194d487..5605b2b5 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -29,6 +29,7 @@ PLUGIN_DIRS = \ keba \ kodi \ lgsmarttv \ + lifx \ mailnotification \ mqttclient \ nanoleaf \