Merge PR #211: New Plugin: LIFX

This commit is contained in:
Jenkins nymea 2020-11-09 15:07:04 +01:00
commit b388b917f7
17 changed files with 2673 additions and 0 deletions

16
debian/control vendored
View File

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

1
debian/nymea-plugin-lifx.install.in vendored Normal file
View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginlifx.so

18
lifx/README.md Normal file
View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginlifx.h"
#include "integrations/integrationplugin.h"
#include "types/param.h"
#include "plugininfo.h"
#include "platform/platformzeroconfcontroller.h"
#include "network/zeroconf/zeroconfservicebrowser.h"
#include <QDebug>
#include <QColor>
#include <QRgb>
#include <QFile>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QNetworkReply>
#include <QNetworkRequest>
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<QString, ThingDescriptor> 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<LifxLan *>(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<LifxCloud *>(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<LifxCloud *>(sender());
Thing *accountThing = m_lifxCloudConnections.key(lifxCloud);
if (!accountThing)
return;
accountThing->setStateValue(lifxAccountLoggedInStateTypeId, authenticated);
}
void IntegrationPluginLifx::onLifxCloudLightsListReceived(const QList<LifxCloud::Light> &lights)
{
LifxCloud *lifxCloud = static_cast<LifxCloud *>(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<LifxCloud::Scene> &scenes)
{
LifxCloud *lifxCloud = static_cast<LifxCloud *>(sender());
Thing *thing = m_lifxCloudConnections.key(lifxCloud);
if (!thing)
return;
qCDebug(dcLifx()) << "Scene list received, count: " << scenes.length();
if (m_asyncBrowseResults.contains(lifxCloud)) {
BrowseResult *result = m_asyncBrowseResults.take(lifxCloud);
foreach (LifxCloud::Scene scene, scenes) {
BrowserItem item;
item.setId(scene.id);
item.setBrowsable(false);
item.setExecutable(true);
item.setDisplayName(scene.name);
item.setDisabled(false);
result->addItem(item);
}
result->finish(Thing::ThingErrorNoError);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef 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 <QTimer>
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<LifxCloud *, ThingSetupInfo *> m_asyncCloudSetups;
QHash<int, ThingActionInfo *> m_asyncActions;
QHash<Thing *, LifxLan *> m_lifxLanConnections;
QHash<Thing *, LifxCloud *> m_lifxCloudConnections;
QHash<LifxCloud *, BrowseResult *> m_asyncBrowseResults;
QHash<int, BrowserActionInfo *> m_asyncBrowserItem;
ZeroConfServiceBrowser *m_serviceBrowser = nullptr;
QHash<ThingClassId, StateTypeId> m_connectedStateTypeIds;
QHash<ThingClassId, StateTypeId> m_powerStateTypeIds;
QHash<ThingClassId, StateTypeId> m_brightnessStateTypeIds;
QHash<ThingClassId, StateTypeId> m_colorTemperatureStateTypeIds;
QHash<ThingClassId, ParamTypeId> m_hostAddressParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_portParamTypeIds;
QHash<ThingClassId, ParamTypeId> m_idParamTypeIds;
QHash<ThingId, ThingActionInfo *> m_pendingBrightnessAction;
QHash<int, LifxLan::LifxProduct> 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<LifxCloud::Light> &lights);
void onLifxCloudScenesListReceived(const QList<LifxCloud::Scene> &scenes);
};
#endif // INTEGRATIONPLUGIN_LIFX_H

View File

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

BIN
lifx/lifx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

14
lifx/lifx.pro Normal file
View File

@ -0,0 +1,14 @@
include(../plugins.pri)
QT += network
SOURCES += \
integrationpluginlifx.cpp \
lifxcloud.cpp \
lifxlan.cpp \
HEADERS += \
integrationpluginlifx.h \
lifxcloud.h \
lifxlan.h \

373
lifx/lifxcloud.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "lifxcloud.h"
#include "extern-plugininfo.h"
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include <QUrlQuery>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
LifxCloud::LifxCloud(NetworkAccessManager *networkManager, QObject *parent) :
QObject(parent),
m_networkManager(networkManager)
{
}
void LifxCloud::setAuthorizationToken(const QByteArray &token)
{
m_authorizationToken = token;
}
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<Light> descriptors;
foreach (QJsonValue jsonValue, array) {
QJsonObject object = jsonValue.toObject();
qCDebug(dcLifx()) << "Light object:" << object;
Light light;
light.id = object["id"].toString().toUtf8();
light.uuid = object["uuid"].toString().toUtf8();
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<Scene> 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;
}

140
lifx/lifxcloud.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef LIFXCLOUD_H
#define LIFXCLOUD_H
#include <QObject>
#include <QColor>
#include <QTimer>
#include "network/networkaccessmanager.h"
class LifxCloud : public QObject
{
Q_OBJECT
public:
enum State {
StatePower,
StateBrightness,
StateColor,
StateColorTemperature,
StateInfrared
};
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<Light> &lights);
void scenesListReceived(const QList<Scene> &scenes);
void requestExecuted(int requestId, bool susccess);
};
#endif // LIFXCLOUD_H

201
lifx/lifxlan.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "lifxlan.h"
#include "extern-plugininfo.h"
#include <QColor>
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<uint16_t>(LightMessages::SetColor));
// Finally another reserved field of 16 bits (2 bytes).
//header.append(2, '\x00');
//ADD SIZE
//header.append(((static_cast<uint16_t>(header.length()+1) & 0xff00) >> 8));
//header.append((static_cast<uint16_t>(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;
}

154
lifx/lifxlan.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef LIFXLAN_H
#define LIFXLAN_H
#include <QObject>
#include <QTimer>
#include <QHostAddress>
#include <QUdpSocket>
#include "network/networkaccessmanager.h"
#include <QColor>
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

13
lifx/meta.json Normal file
View File

@ -0,0 +1,13 @@
{
"title": "LIFX",
"tagline": "Control LIFX light bulbs.",
"icon": "lifx.png",
"stability": "consumer",
"offline": false,
"technologies": [
"network"
],
"categories": [
"light"
]
}

373
lifx/products.json Normal file
View File

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

View File

@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de">
<context>
<name>IntegrationPluginLifx</name>
<message>
<source>LIFX server is not reachable.</source>
<translation>LIFX Server ist nicht erreichbar</translation>
</message>
<message>
<source>Please enter your user name and token. Get the token from https://cloud.lifx.com/settings</source>
<translation>Bitte geben Sie Ihren Lifx Benutzernamen und Token ein. Holen Sie sich Ihren Token von https://cloud.lifx.com/settings</translation>
</message>
<message>
<source>The token is invalid.</source>
<translation>Der Token ist ungültig.</translation>
</message>
</context>
<context>
<name>Lifx</name>
<message>
<source>Brightness</source>
<extracomment>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</extracomment>
<translation>Helligkeit</translation>
</message>
<message>
<source>Brightness changed</source>
<extracomment>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</extracomment>
<translation>Helligkeit geändert</translation>
</message>
<message>
<source>Color</source>
<extracomment>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})</extracomment>
<translation>Farbe</translation>
</message>
<message>
<source>Color changed</source>
<extracomment>The name of the EventType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb</extracomment>
<translation>Farbe geändert</translation>
</message>
<message>
<source>Color temperature</source>
<extracomment>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</extracomment>
<translation>Farbtemperatur</translation>
</message>
<message>
<source>Color temperature changed</source>
<extracomment>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</extracomment>
<translation>Farbtemperatur geändert</translation>
</message>
<message>
<source>Day and Dusk</source>
<extracomment>The name of the ThingClass ({a5b02af8-7c97-4a78-9c78-bafee7407b5e})</extracomment>
<translation>Tag und Sonnenaufgang</translation>
</message>
<message>
<source>LIFX</source>
<extracomment>The name of the vendor ({e5e48c0d-cff7-4c0f-983e-d23bd3e4ba87})
----------
The name of the plugin Lifx ({4e00ee30-79e2-447b-8dcc-c34470f41992})</extracomment>
<translation>LIFX</translation>
</message>
<message>
<source>Power</source>
<extracomment>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</extracomment>
<translation>Eingeschalten</translation>
</message>
<message>
<source>Power changed</source>
<extracomment>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</extracomment>
<translation>Eingeschalten changed</translation>
</message>
<message>
<source>Reachable</source>
<extracomment>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</extracomment>
<translation>Erreichbar</translation>
</message>
<message>
<source>Reachable changed</source>
<extracomment>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</extracomment>
<translation>Erreichbar changed</translation>
</message>
<message>
<source>Set color</source>
<extracomment>The name of the ActionType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb</extracomment>
<translation>Setze Farbe</translation>
</message>
<message>
<source>Set color temperature</source>
<extracomment>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</extracomment>
<translation>Setze Farbtemperatur</translation>
</message>
<message>
<source>Set effect</source>
<extracomment>The name of the ActionType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb</extracomment>
<translation>Setze Effekt</translation>
</message>
<message>
<source>Set power</source>
<extracomment>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</extracomment>
<translation>Setze Eingeschalten</translation>
</message>
<message>
<source>Effect</source>
<extracomment>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</extracomment>
<translation>Effekt</translation>
</message>
<message>
<source>Effect changed</source>
<extracomment>The name of the EventType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb</extracomment>
<translation>Effekt geändert</translation>
</message>
<message>
<source>ID</source>
<extracomment>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})</extracomment>
<translation>ID</translation>
</message>
<message>
<source>Set brightness</source>
<extracomment>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</extracomment>
<translation>Setze Helligkeit</translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation>Verbunden</translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({3e7b358b-d7de-4db4-8a3a-b9860eae186f}) of ThingClass lifxAccount</extracomment>
<translation>Verbunden geändert</translation>
</message>
<message>
<source>LIFX cloud account</source>
<extracomment>The name of the ThingClass ({387c87f6-3e5b-4d6a-ba4d-372d0efad79f})</extracomment>
<translation>LIFX Cloud-Account</translation>
</message>
<message>
<source>Logged in</source>
<extracomment>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</extracomment>
<translation>Eingelogged</translation>
</message>
<message>
<source>Logged in changed</source>
<extracomment>The name of the EventType ({0db34069-5de0-4233-baec-27f039228524}) of ThingClass lifxAccount</extracomment>
<translation>Eingelogged geändert</translation>
</message>
<message>
<source>User name</source>
<extracomment>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</extracomment>
<translation>Benutzername</translation>
</message>
<message>
<source>User name changed</source>
<extracomment>The name of the EventType ({554afd9b-a2ec-4d28-9065-2b9ab3a9e3b2}) of ThingClass lifxAccount</extracomment>
<translation>Benutzername geändert</translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginLifx</name>
<message>
<source>LIFX server is not reachable.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Please enter your user name and token. Get the token from https://cloud.lifx.com/settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The token is invalid.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Lifx</name>
<message>
<source>Brightness</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Brightness changed</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Color</source>
<extracomment>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})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Color changed</source>
<extracomment>The name of the EventType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Color temperature</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Color temperature changed</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Day and Dusk</source>
<extracomment>The name of the ThingClass ({a5b02af8-7c97-4a78-9c78-bafee7407b5e})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>LIFX</source>
<extracomment>The name of the vendor ({e5e48c0d-cff7-4c0f-983e-d23bd3e4ba87})
----------
The name of the plugin Lifx ({4e00ee30-79e2-447b-8dcc-c34470f41992})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Power</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Power changed</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reachable</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reachable changed</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set color</source>
<extracomment>The name of the ActionType ({a47d8164-5023-4ffb-8298-73293e93e7f6}) of ThingClass colorBulb</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set color temperature</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set effect</source>
<extracomment>The name of the ActionType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set power</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Effect</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Effect changed</source>
<extracomment>The name of the EventType ({65f88396-2958-480e-b0be-c4695400a343}) of ThingClass colorBulb</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>ID</source>
<extracomment>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})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set brightness</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Connected</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Connected changed</source>
<extracomment>The name of the EventType ({3e7b358b-d7de-4db4-8a3a-b9860eae186f}) of ThingClass lifxAccount</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>LIFX cloud account</source>
<extracomment>The name of the ThingClass ({387c87f6-3e5b-4d6a-ba4d-372d0efad79f})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Logged in</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Logged in changed</source>
<extracomment>The name of the EventType ({0db34069-5de0-4233-baec-27f039228524}) of ThingClass lifxAccount</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>User name</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>User name changed</source>
<extracomment>The name of the EventType ({554afd9b-a2ec-4d28-9065-2b9ab3a9e3b2}) of ThingClass lifxAccount</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -29,6 +29,7 @@ PLUGIN_DIRS = \
keba \
kodi \
lgsmarttv \
lifx \
mailnotification \
mqttclient \
nanoleaf \