Merge PR #203: New Plugin: Nanoleaf

master
Jenkins nymea 2020-01-18 23:58:22 +01:00
commit b57c9e44d6
11 changed files with 1935 additions and 0 deletions

16
debian/control vendored
View File

@ -434,6 +434,21 @@ Description: nymea.io plugin for netatmo
This package will install the nymea.io plugin for netatmo
Package: nymea-plugin-nanoleaf
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Description: nymea.io plugin for nanoleaf
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 nanoleaf
Package: nymea-plugin-networkdetector
Architecture: any
Depends: ${shlibs:Depends},
@ -890,6 +905,7 @@ Depends: nymea-plugin-anel,
nymea-plugin-lgsmarttv,
nymea-plugin-mailnotification,
nymea-plugin-texasinstruments,
nymea-plugin-nanoleaf,
nymea-plugin-netatmo,
nymea-plugin-networkdetector,
nymea-plugin-openweathermap,

View File

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

34
nanoleaf/README.md Normal file
View File

@ -0,0 +1,34 @@
# Nanoleaf
This Plug-In allows to control Nanoleaf Light Panels.
## Features
Controls:
* Power
* Brightness
* Color Temperature
* Color
* Set Effect
States:
* Connected
Browsing:
This plug-in implements also browsing for light effects, means if a new light effect is beeing added
nymea will find that.
## Device Setup
The Nanoleaf App is required to connect the device to the WiFi Network.
This Plug-In uses the local API of Nanoleaf devices, means
nymea must be in the same local area network.
The device will be discovered through Zeroconf, if it
can't be discovered the Network might not support Zeroconf
and the IP-Address must be entered manually.
More about Nanoleaf devices:
https://nanoleaf.me

View File

@ -0,0 +1,448 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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
* This project may also contain libraries licensed under the open source software license GNU GPL v.3.
* 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 "devicepluginnanoleaf.h"
#include "plugininfo.h"
#include "network/zeroconf/zeroconfservicebrowser.h"
#include "platform/platformzeroconfcontroller.h"
#include <QHash>
#include <QDebug>
DevicePluginNanoleaf::DevicePluginNanoleaf()
{
}
void DevicePluginNanoleaf::init()
{
m_zeroconfBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_nanoleafapi._tcp");
}
void DevicePluginNanoleaf::discoverDevices(DeviceDiscoveryInfo *info)
{
QStringList serialNumbers;
foreach (const ZeroConfServiceEntry &entry, m_zeroconfBrowser->serviceEntries()) {
DeviceDescriptor descriptor(lightPanelsDeviceClassId, entry.name(), entry.hostAddress().toString());
ParamList params;
QString serialNo;
QString model;
QString firmwareVersion;
foreach (QString value, entry.txt()) {
if (value.contains("id=")) {
serialNo = value.split("=").last();
} else if (value.contains("md=")) {
model = value.split("=").last();
} else if (value.contains("srcvers=")) {
firmwareVersion = value.split("=").last();
}
}
if (serialNumbers.contains(serialNo)) {
continue; //To avoid duplicated devices
}
Device *existingDevice = myDevices().findByParams(ParamList() << Param(lightPanelsDeviceSerialNoParamTypeId, serialNo));
if (existingDevice) {
//For device rediscovery
descriptor.setDeviceId(existingDevice->id());
}
serialNumbers.append(serialNo);
qCDebug(dcNanoleaf()) << "Have device" << entry.name() << serialNo << model << firmwareVersion;
params << Param(lightPanelsDeviceAddressParamTypeId, entry.hostAddress().toString());
params << Param(lightPanelsDevicePortParamTypeId, entry.port());
params << Param(lightPanelsDeviceModelParamTypeId, model);
params << Param(lightPanelsDeviceSerialNoParamTypeId, serialNo);
params << Param(lightPanelsDeviceFirmwareVersionParamTypeId, firmwareVersion);
descriptor.setParams(params);
info->addDeviceDescriptor(descriptor);
}
info->finish(Device::DeviceErrorNoError);
}
void DevicePluginNanoleaf::startPairing(DevicePairingInfo *info)
{
info->finish(Device::DeviceErrorNoError, tr("On the Nanoleaf controller, hold the on-off button for 5-7 seconds until the LED starts flashing."));
}
void DevicePluginNanoleaf::confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret)
{
Q_UNUSED(username)
Q_UNUSED(secret)
Nanoleaf *nanoleaf = createNanoleafConnection(QHostAddress(info->params().paramValue(lightPanelsDeviceAddressParamTypeId).toString()), info->params().paramValue(lightPanelsDevicePortParamTypeId).toInt());
nanoleaf->addUser(); //push button pairing
m_unfinishedNanoleafConnections.insert(info->deviceId(), nanoleaf);
m_unfinishedPairing.insert(nanoleaf, info);
connect(info, &DevicePairingInfo::aborted, this, [info, this] {
Nanoleaf *nanoleaf = m_unfinishedNanoleafConnections.take(info->deviceId());
m_unfinishedPairing.remove(nanoleaf);
nanoleaf->deleteLater();
});
}
void DevicePluginNanoleaf::setupDevice(DeviceSetupInfo *info)
{
Device *device = info->device();
if(device->deviceClassId() == lightPanelsDeviceClassId) {
pluginStorage()->beginGroup(device->id().toString());
QString token = pluginStorage()->value("authToken").toString();
pluginStorage()->endGroup();
Nanoleaf *nanoleaf;
if (m_unfinishedNanoleafConnections.contains(device->id())) {
// This setupDevice is called after a discovery
nanoleaf = m_unfinishedNanoleafConnections.take(device->id());
m_nanoleafConnections.insert(device->id(), nanoleaf);
return info->finish(Device::DeviceErrorNoError);
} else {
// This setupDevice is called after a (re)start, with an already added device
QHostAddress address(device->paramValue(lightPanelsDeviceAddressParamTypeId).toString());
int port = device->paramValue(lightPanelsDevicePortParamTypeId).toInt();
nanoleaf = createNanoleafConnection(address, port);
nanoleaf->setAuthToken(token);
nanoleaf->getControllerInfo(); //This is just to check if the device is available
m_nanoleafConnections.insert(device->id(), nanoleaf);
m_asyncDeviceSetup.insert(nanoleaf, info);
connect(info, &DeviceSetupInfo::aborted, this, [nanoleaf, this](){m_asyncDeviceSetup.remove(nanoleaf);});
return;
}
}
}
void DevicePluginNanoleaf::postSetupDevice(Device *device)
{
if (device->deviceClassId() == lightPanelsDeviceClassId) {
Nanoleaf *nanoleaf = m_nanoleafConnections.value(device->id());
if (!nanoleaf)
return;
nanoleaf->getControllerInfo();
nanoleaf->registerForEvents();
}
if(!m_pluginTimer) {
m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(5);
connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() {
foreach (Nanoleaf *nanoleaf, m_nanoleafConnections) {
nanoleaf->getControllerInfo();
}
});
}
}
void DevicePluginNanoleaf::deviceRemoved(Device *device)
{
if(device->deviceClassId() == lightPanelsDeviceClassId) {
Nanoleaf *nanoleaf = m_nanoleafConnections.take(device->id());
nanoleaf->deleteLater();
}
if (myDevices().isEmpty()) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
m_pluginTimer = nullptr;
}
}
void DevicePluginNanoleaf::executeAction(DeviceActionInfo *info)
{
Device *device = info->device();
Action action = info->action();
if (device->deviceClassId() == lightPanelsDeviceClassId) {
Nanoleaf *nanoleaf = m_nanoleafConnections.value(device->id());
if (!nanoleaf) {
return info->finish(Device::DeviceErrorHardwareFailure);
}
if (action.actionTypeId() == lightPanelsPowerActionTypeId) {
bool power = action.param(lightPanelsPowerActionPowerParamTypeId).value().toBool();
QUuid requestId = nanoleaf->setPower(power);
connect(info, &DeviceActionInfo::aborted,[requestId, this](){m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info);
} else if (action.actionTypeId() == lightPanelsBrightnessActionTypeId) {
int brightness = action.param(lightPanelsBrightnessActionBrightnessParamTypeId).value().toInt();
QUuid requestId = nanoleaf->setBrightness(brightness);
connect(info, &DeviceActionInfo::aborted,[requestId, this](){m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info);
} else if (action.actionTypeId() == lightPanelsColorActionTypeId) {
QColor color(action.param(lightPanelsColorActionColorParamTypeId).value().toString());
QUuid requestId = nanoleaf->setColor(color);
connect(info, &DeviceActionInfo::aborted,[requestId, this](){m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info);
} else if (action.actionTypeId() == lightPanelsColorTemperatureActionTypeId) {
int colorTemperature = action.param(lightPanelsColorTemperatureActionColorTemperatureParamTypeId).value().toInt();
QUuid requestId = nanoleaf->setMired(colorTemperature);
connect(info, &DeviceActionInfo::aborted,[requestId, this](){m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info);
} else if (action.actionTypeId() == lightPanelsAlertActionTypeId) {
QUuid requestId = nanoleaf->identify();
connect(info, &DeviceActionInfo::aborted,[requestId, this](){m_asyncActions.remove(requestId);});
m_asyncActions.insert(requestId, info);
}
}
}
void DevicePluginNanoleaf::browseDevice(BrowseResult *result)
{
Device *device = result->device();
Nanoleaf *nanoleaf = m_nanoleafConnections.value(device->id());
nanoleaf->getEffects();
m_asyncBrowseResults.insert(nanoleaf, result);
connect(result, &BrowseResult::aborted, this, [nanoleaf, this]{m_asyncBrowseResults.remove(nanoleaf);});
}
void DevicePluginNanoleaf::browserItem(BrowserItemResult *result)
{
Q_UNUSED(result)
qCDebug(dcNanoleaf()) << "BrowserItem called";
}
void DevicePluginNanoleaf::executeBrowserItem(BrowserActionInfo *info)
{
Device *device = info->device();
Nanoleaf *nanoleaf = m_nanoleafConnections.value(device->id());
QUuid requestId = nanoleaf->setEffect(info->browserAction().itemId());
m_asyncBrowserItem.insert(requestId, info);
connect(info, &BrowserActionInfo::aborted, this, [requestId, this]{m_asyncBrowserItem.remove(requestId);});
}
Nanoleaf *DevicePluginNanoleaf::createNanoleafConnection(const QHostAddress &address, int port)
{
Nanoleaf *nanoleaf = new Nanoleaf(hardwareManager()->networkManager(), address, port, this);
connect(nanoleaf, &Nanoleaf::authTokenRecieved, this, &DevicePluginNanoleaf::onAuthTokenReceived);
connect(nanoleaf, &Nanoleaf::authenticationStatusChanged, this, &DevicePluginNanoleaf::onAuthenticationStatusChanged);
connect(nanoleaf, &Nanoleaf::requestExecuted, this, &DevicePluginNanoleaf::onRequestExecuted);
connect(nanoleaf, &Nanoleaf::connectionChanged, this, &DevicePluginNanoleaf::onConnectionChanged);
connect(nanoleaf, &Nanoleaf::controllerInfoReceived, this, &DevicePluginNanoleaf::onControllerInfoReceived);
connect(nanoleaf, &Nanoleaf::brightnessReceived, this, &DevicePluginNanoleaf::onBrightnessReceived);
connect(nanoleaf, &Nanoleaf::powerReceived, this, &DevicePluginNanoleaf::onPowerReceived);
connect(nanoleaf, &Nanoleaf::colorModeReceived, this, &DevicePluginNanoleaf::onColorModeReceived);
connect(nanoleaf, &Nanoleaf::saturationReceived, this, &DevicePluginNanoleaf::onSaturationReceived);
connect(nanoleaf, &Nanoleaf::hueReceived, this, &DevicePluginNanoleaf::onHueReceived);
connect(nanoleaf, &Nanoleaf::colorTemperatureReceived, this, &DevicePluginNanoleaf::onColorTemperatureReceived);
connect(nanoleaf, &Nanoleaf::effectListReceived, this, &DevicePluginNanoleaf::onEffectListReceived);
connect(nanoleaf, &Nanoleaf::selectedEffectReceived, this, &DevicePluginNanoleaf::onSelectedEffectReceived);
return nanoleaf;
}
void DevicePluginNanoleaf::onAuthTokenReceived(const QString &token)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
if (m_unfinishedPairing.contains(nanoleaf)) {
DevicePairingInfo *info = m_unfinishedPairing.take(nanoleaf);
pluginStorage()->beginGroup(info->deviceId().toString());
pluginStorage()->setValue("authToken", token);
pluginStorage()->endGroup();
info->finish(Device::DeviceErrorNoError);
}
}
void DevicePluginNanoleaf::onAuthenticationStatusChanged(bool authenticated)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
if (m_asyncDeviceSetup.contains(nanoleaf)) {
DeviceSetupInfo *info = m_asyncDeviceSetup.take(nanoleaf);
if (authenticated) {
info->finish(Device::DeviceErrorNoError);
} else {
info->finish(Device::DeviceErrorSetupFailed);
}
}
}
void DevicePluginNanoleaf::onRequestExecuted(QUuid requestId, bool success)
{
if (m_asyncActions.contains(requestId)) {
DeviceActionInfo *info = m_asyncActions.take(requestId);
if (success) {
info->finish(Device::DeviceErrorNoError);
} else {
info->finish(Device::DeviceErrorHardwareNotAvailable);
}
}
if (m_asyncBrowserItem.contains(requestId)) {
BrowserActionInfo *info = m_asyncBrowserItem.take(requestId);
if (success) {
info->finish(Device::DeviceErrorNoError);
} else {
info->finish(Device::DeviceErrorHardwareNotAvailable);
}
}
}
void DevicePluginNanoleaf::onConnectionChanged(bool connected)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
device->setStateValue(lightPanelsConnectedStateTypeId, connected);
}
void DevicePluginNanoleaf::onControllerInfoReceived(const Nanoleaf::ControllerInfo &controllerInfo)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
//qCDebug(dcNanoleaf()) << "Controller Info received" << controllerInfo.name << controllerInfo.firmwareVersion;
device->setParamValue(lightPanelsDeviceFirmwareVersionParamTypeId, controllerInfo.firmwareVersion);
}
void DevicePluginNanoleaf::onPowerReceived(bool power)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
//qCDebug(dcNanoleaf()) << "Power received" << power;
device->setStateValue(lightPanelsPowerStateTypeId, power);
}
void DevicePluginNanoleaf::onBrightnessReceived(int percentage)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
//qCDebug(dcNanoleaf()) << "Brightness received" << percentage;
device->setStateValue(lightPanelsBrightnessStateTypeId, percentage);
}
void DevicePluginNanoleaf::onColorReceived(QColor color)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
//qCDebug(dcNanoleaf()) << "Color received" << color.toRgb();
device->setStateValue(lightPanelsColorStateTypeId, color);
}
void DevicePluginNanoleaf::onColorModeReceived(Nanoleaf::ColorMode colorMode)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
switch (colorMode) {
case Nanoleaf::ColorMode::ColorTemperatureMode:
device->setStateValue(lightPanelsColorModeStateTypeId, tr("Color temperature"));
break;
case Nanoleaf::ColorMode::HueSaturationMode:
device->setStateValue(lightPanelsColorModeStateTypeId, tr("Hue/Saturation"));
break;
case Nanoleaf::ColorMode::EffectMode:
device->setStateValue(lightPanelsColorModeStateTypeId, tr("Effect"));
break;
}
}
void DevicePluginNanoleaf::onHueReceived(int hue)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
//qCDebug(dcNanoleaf()) << "Hue received" << hue;
QColor color = QColor(device->stateValue(lightPanelsColorStateTypeId).toString());
color.setHsv(hue, color.saturation(), color.value());
device->setStateValue(lightPanelsColorStateTypeId, color);
}
void DevicePluginNanoleaf::onSaturationReceived(int saturation)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
//qCDebug(dcNanoleaf()) << "Saturation received" << saturation;
QColor color = QColor(device->stateValue(lightPanelsColorStateTypeId).toString());
color.setHsv(color.hue(), saturation, color.value());
device->setStateValue(lightPanelsColorStateTypeId, color);
}
void DevicePluginNanoleaf::onEffectListReceived(const QStringList &effects)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
//qCDebug(dcNanoleaf()) << "Effect list received" << effects;
if (m_asyncBrowseResults.contains(nanoleaf)) {
BrowseResult *result = m_asyncBrowseResults.take(nanoleaf);
foreach (QString effect, effects) {
BrowserItem item;
item.setId(effect);
item.setBrowsable(false);
item.setExecutable(true);
item.setDisplayName(effect);
item.setDisabled(false);
result->addItem(item);
}
result->finish(Device::DeviceErrorNoError);
}
}
void DevicePluginNanoleaf::onColorTemperatureReceived(int kelvin)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
qCDebug(dcNanoleaf()) << "Color temperature received, Kelvin:" << kelvin << "Mired:" << (653-(kelvin/13));
//NOTE: this is just a rough estimation of the mired value
//Mired: 153 - 500
//Kelvin: 1200-6500
int mired = static_cast<int>(653-(kelvin/13));
device->setStateValue(lightPanelsColorTemperatureStateTypeId, mired);
}
void DevicePluginNanoleaf::onSelectedEffectReceived(const QString &effect)
{
Nanoleaf *nanoleaf = static_cast<Nanoleaf *>(sender());
Device *device = myDevices().findById(m_nanoleafConnections.key(nanoleaf));
if (!device)
return;
//qCDebug(dcNanoleaf()) << "Selected effect received" << effect;
device->setStateValue(lightPanelsEffectNameStateTypeId, QString(effect).remove('"').remove('*'));
}

View File

@ -0,0 +1,96 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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
* This project may also contain libraries licensed under the open source software license GNU GPL v.3.
* 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 DEVICEPLUGINNANOLEAF_H
#define DEVICEPLUGINNANOLEAF_H
#include "devices/deviceplugin.h"
#include "nanoleaf.h"
#include "plugintimer.h"
#include "network/networkaccessmanager.h"
#include "network/zeroconf/zeroconfservicebrowser.h"
#include <QHostAddress>
class DevicePluginNanoleaf: public DevicePlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginnanoleaf.json")
Q_INTERFACES(DevicePlugin)
public:
explicit DevicePluginNanoleaf();
void init() override;
void discoverDevices(DeviceDiscoveryInfo *info) override;
void startPairing(DevicePairingInfo *info) override;
void confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) override;
void setupDevice(DeviceSetupInfo *info) override;
void postSetupDevice(Device *device) override;
void deviceRemoved(Device *device) override;
void executeAction(DeviceActionInfo *info) override;
void browseDevice(BrowseResult *result) override;
void browserItem(BrowserItemResult *result) override;
void executeBrowserItem(BrowserActionInfo *info) override;
private:
ZeroConfServiceBrowser *m_zeroconfBrowser = nullptr;
PluginTimer *m_pluginTimer = nullptr;
QHash<DeviceId, Nanoleaf*> m_nanoleafConnections;
QHash<DeviceId, Nanoleaf*> m_unfinishedNanoleafConnections;
QHash<QUuid, DeviceActionInfo *> m_asyncActions;
QHash<Nanoleaf *, DevicePairingInfo *> m_unfinishedPairing;
QHash<Nanoleaf *, DeviceSetupInfo *> m_asyncDeviceSetup;
QHash<Nanoleaf *, BrowseResult *> m_asyncBrowseResults;
QHash<QUuid, BrowserActionInfo *> m_asyncBrowserItem;
Nanoleaf *createNanoleafConnection(const QHostAddress &address, int port);
public slots:
void onAuthTokenReceived(const QString &token);
void onAuthenticationStatusChanged(bool authenticated);
void onRequestExecuted(QUuid requestId, bool success);
void onConnectionChanged(bool connected);
void onControllerInfoReceived(const Nanoleaf::ControllerInfo &controllerInfo);
void onPowerReceived(bool power);
void onBrightnessReceived(int percentage);
void onColorReceived(QColor color);
void onColorModeReceived(Nanoleaf::ColorMode colorMode);
void onHueReceived(int hue);
void onSaturationReceived(int percentage);
void onEffectListReceived(const QStringList &effects);
void onColorTemperatureReceived(int kelvin);
void onSelectedEffectReceived(const QString &effect);
};
#endif // DEVICEPLUGINNANOLEAF_H

View File

@ -0,0 +1,139 @@
{
"name": "Nanoleaf",
"displayName": "Nanoleaf",
"id": "360867ec-1594-498d-8182-fbab1fe17489",
"vendors": [
{
"id": "3d7fdaa6-7896-419b-8be3-c90c42bcac7f",
"name": "nanoleaf",
"displayName": "Nanoleaf",
"deviceClasses": [
{
"id": "d44ee383-9fa5-4751-babd-1129ac20896a",
"name": "lightPanels",
"displayName": "Light panels",
"interfaces": ["colorlight", "colortemperaturelight", "alert", "connectable"],
"createMethods": ["discovery"],
"setupMethod": "pushButton",
"browsable": true,
"paramTypes": [
{
"id": "ff57079f-d5ab-4511-8a5c-0726e7b82af6",
"name": "address",
"displayName": "Address",
"type" : "QString"
},
{
"id": "ba4fd45b-990d-480a-859d-fff7ffba3ba4",
"name": "port",
"displayName": "Port",
"type" : "int",
"readOnly": true
},
{
"id": "353d3c71-0ad2-40d5-99f6-cc305e2073f1",
"name": "model",
"displayName": "Model",
"type" : "QString",
"readOnly": true
},
{
"id": "18be4a5f-e2c2-4070-bc3e-ea9fe64f2276",
"name": "serialNo",
"displayName": "Serial number",
"type" : "QString",
"readOnly": true
},
{
"id": "1b85eebe-3b1a-49a9-bddb-2175d6599b95",
"name": "firmwareVersion",
"displayName": "Firmware version",
"type" : "QString",
"readOnly": true
}
],
"actionTypes": [
{
"id": "47a6a1a1-fb90-4f24-be8c-b4dba0aaaa84",
"name": "alert",
"displayName": "Alert"
}
],
"stateTypes": [
{
"id": "a3102107-a825-4ec8-a9ec-b2c2a9fb5c89",
"name": "connected",
"displayName": "Reachable",
"displayNameEvent": "Reachable changed",
"defaultValue": false,
"type": "bool",
"cached": false
},
{
"id": "44bee9ec-513d-4834-991a-ee9ae69d9f2a",
"name": "power",
"displayName": "Power",
"displayNameEvent": "Power changed",
"displayNameAction": "Set power",
"type": "bool",
"defaultValue": false,
"writable": true
},
{
"id": "41248127-844b-40be-87e6-38aee48b6687",
"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": "d4a52cdc-93b2-44fc-a36c-ae65f1d98f2e",
"name": "color",
"displayName": "Color",
"displayNameEvent": "Color changed",
"displayNameAction": "Set color",
"type": "QColor",
"defaultValue": "#000000",
"writable": true
},
{
"id": "4e5d6460-d42e-4b7c-a8f3-6e953451c1ef",
"name": "brightness",
"displayName": "Brightness",
"displayNameEvent": "Brightness changed",
"displayNameAction": "Set brightness",
"type": "int",
"unit": "Percentage",
"defaultValue": 0,
"minValue": 0,
"maxValue": 100,
"writable": true
},
{
"id": "bdd2ea1e-9ef9-4967-9678-2c601b826199",
"name": "colorMode",
"displayName": "Color mode",
"displayNameEvent": "Color mode changed",
"type": "QString",
"defaultValue": "Color temperature"
},
{
"id": "57f9831e-1b98-41c1-a21c-6073ff327237",
"name": "effectName",
"displayName": "Effect name",
"displayNameEvent": "Effect name changed",
"type": "QString",
"defaultValue": "-"
}
]
}
]
}
]
}

826
nanoleaf/nanoleaf.cpp Normal file
View File

@ -0,0 +1,826 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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
* This project may also contain libraries licensed under the open source software license GNU GPL v.3.
* 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 "nanoleaf.h"
#include "extern-plugininfo.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QUrlQuery>
Nanoleaf::Nanoleaf(NetworkAccessManager *networkManager, const QHostAddress &address, int port, QObject *parent) :
QObject(parent),
m_networkManager(networkManager),
m_address(address),
m_port(port)
{
}
void Nanoleaf::setIpAddress(const QHostAddress &address)
{
m_address = address;
}
QHostAddress Nanoleaf::ipAddress()
{
return m_address;
}
void Nanoleaf::setPort(int port)
{
m_port = port;
}
int Nanoleaf::port()
{
return m_port;
}
void Nanoleaf::setAuthToken(const QString &token)
{
m_authToken = token;
}
QString Nanoleaf::authToken()
{
return m_authToken;
}
void Nanoleaf::addUser()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/new");
QNetworkRequest request;
request.setUrl(url);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->post(request, "");
//qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// Check HTTP status code
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
if (reply->error() == QNetworkReply::HostNotFoundError) {
emit connectionChanged(false);
}
if (status >= 400 && status <= 410) {
emit authenticationStatusChanged(false);
}
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
emit connectionChanged(true);
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
m_authToken = data.toVariant().toMap().value("auth_token").toString();
emit authTokenRecieved(m_authToken);
emit authenticationStatusChanged(true);
});
}
void Nanoleaf::deleteUser()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken);
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->deleteResource(request);
//qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
emit authenticationStatusChanged(false);
});
}
void Nanoleaf::getControllerInfo()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken);
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
//qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
emit authenticationStatusChanged(false);
return;
}
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
emit connectionChanged(true);
emit authenticationStatusChanged(true);
QVariantMap map = data.toVariant().toMap();
ControllerInfo info;
info.name = map.value("name").toString();
info.serialNumber = map.value("serialNo").toString();
info.model = map.value("model").toString();
info.manufacturer = map.value("manufacturer").toString();
info.firmwareVersion = map.value("firmwareVersion").toString();
emit controllerInfoReceived(info);
if (map.contains("state")) {
QVariantMap state = map.value("state").toMap();
if (state.contains("on")) {
emit powerReceived(state["on"].toMap().value("value").toBool());
}
if (state.contains("hue") && state.contains("sat") && state.contains("brightness")) {
int brightness = state["brightness"].toMap().value("value").toInt();
emit brightnessReceived(brightness);
int hue = state["hue"].toMap().value("value").toInt();
emit hueReceived(hue);
int sat = state["sat"].toMap().value("value").toInt();
emit saturationReceived(sat);
QColor color;
color.setHsv(hue, sat, brightness);
emit colorReceived(color);
}
if (state.contains("ct")) {
emit colorTemperatureReceived(state["ct"].toMap().value("value").toInt());
}
if (state.contains("colorMode")) {
QString colorModeString = state["colorMode"].toString();
if (colorModeString == "effect") {
emit colorModeReceived(ColorMode::EffectMode);
} else if (colorModeString == "hs") {
emit colorModeReceived(ColorMode::HueSaturationMode);
} else if (colorModeString == "ct") {
emit colorModeReceived(ColorMode::ColorTemperatureMode);
} else {
qCWarning(dcNanoleaf()) << "Unrecognized color mode";
}
}
}
if (map.contains("effects")) {
QVariantMap effects = map.value("effects").toMap();
emit selectedEffectReceived(effects.value("select").toString());
}
if (map.contains("panelLayout")) {
//QVariantMap panelLayout = map.value("panelLayout").toMap();
//emit panelLayoutReceived();
}
if (map.contains("rhythm")) {
//QVariantMap rhythm = map.value("rhythm").toMap();
//emit rhythmModulReceived(rhythm.value("select").toString());
}
});
}
void Nanoleaf::getPower()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken+"/state/on");
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
//qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
emit connectionChanged(false);
return;
}
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
bool power = data.toVariant().toMap().value("value").toBool();
emit connectionChanged(true);
emit powerReceived(power);
});
}
void Nanoleaf::getHue()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken+"/state/hue");
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
//qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
int hue = data.toVariant().toMap().value("value").toBool();
emit connectionChanged(true);
emit hueReceived(hue);
});
}
void Nanoleaf::getBrightness()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken+"/state/brightness");
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
int brightness = data.toVariant().toMap().value("value").toInt();
emit connectionChanged(true);
emit brightnessReceived(brightness);
});
}
void Nanoleaf::getSaturation()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken+"/state/sat");
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
int brightness = data.toVariant().toMap().value("value").toInt();
emit connectionChanged(true);
emit saturationReceived(brightness);
});
}
void Nanoleaf::getColorTemperature()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken+"/state/ct");
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
emit connectionChanged(false);
return;
}
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
int kelvin = data.toVariant().toMap().value("value").toInt();
emit connectionChanged(true);
emit colorTemperatureReceived(kelvin);
});
}
void Nanoleaf::getColorMode()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken+"/state/colorMode");
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
emit connectionChanged(false);
return;
}
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
emit connectionChanged(true);
QString colorModeString = data.toVariant().toMap().value("value").toString();
if (colorModeString == "effect") {
emit colorModeReceived(ColorMode::EffectMode);
} else if (colorModeString == "hs") {
emit colorModeReceived(ColorMode::HueSaturationMode);
} else if (colorModeString == "ct") {
emit colorModeReceived(ColorMode::ColorTemperatureMode);
} else {
qCWarning(dcNanoleaf()) << "Unrecognized color mode";
}
});
}
void Nanoleaf::registerForEvents()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken+"/events");
QUrlQuery query;
query.addQueryItem("id", "1,2,3,4");
url.setQuery(query);
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::readyRead, this, [reply, this] {
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
qCDebug(dcNanoleaf()) << "On event stream" << data.toJson();
});
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
emit connectionChanged(false);
return;
}
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
qCDebug(dcNanoleaf()) << "Event received" << data.toJson();
QVariantList events = data.toVariant().toList();
foreach (QVariant variant, events) {
QVariantMap event = variant.toMap();
switch (event["attr"].toInt()) {
case 1: //ON
emit powerReceived(event["value"].toBool());
break;
case 2: //Brightness
emit brightnessReceived(event["value"].toInt());
break;
case 3: //Hue
emit hueReceived(event["value"].toInt());
break;
case 4: //Saturation
emit saturationReceived(event["value"].toInt());
break;
case 5: //Color Temperature
emit colorTemperatureReceived(event["value"].toInt());
break;
case 6: { //colorMode
QString colorModeString = event["value"].toString();
if (colorModeString == "effect") {
emit colorModeReceived(ColorMode::EffectMode);
} else if (colorModeString == "hs") {
emit colorModeReceived(ColorMode::HueSaturationMode);
} else if (colorModeString == "ct") {
emit colorModeReceived(ColorMode::ColorTemperatureMode);
} else {
qCWarning(dcNanoleaf()) << "Unrecognized color mode";
}
break;
}
default:
qCWarning(dcNanoleaf()) << "Unrecognised Event received";
}
}
});
}
QUuid Nanoleaf::setPower(bool power)
{
QUuid requestId = QUuid::createUuid();
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath(QString("/api/v1/%1/state").arg(m_authToken));
QVariantMap map;
QVariantMap value;
value["value"] = power;
map.insert("on", value);
QJsonDocument body = QJsonDocument::fromVariant(map);
QNetworkRequest request;
request.setUrl(url);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->put(request, body.toJson());
//qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
emit requestExecuted(requestId, false);
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
emit connectionChanged(true);
emit requestExecuted(requestId, true);
});
return requestId;
}
QUuid Nanoleaf::setColor(QColor color)
{
QUuid requestId = setHue(color.hue());
setSaturation(static_cast<int>(color.saturation()/2.55)); //QColor saturation is 0-255
return requestId;
}
QUuid Nanoleaf::setHue(int hue)
{
QUuid requestId = QUuid::createUuid();
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath(QString("/api/v1/%1/state").arg(m_authToken));
QVariantMap map;
QVariantMap hueMap;
hueMap["value"] = hue;
map.insert("hue", hueMap);
QJsonDocument body = QJsonDocument::fromVariant(map);
QNetworkRequest request;
request.setUrl(url);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->put(request, body.toJson());
//qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
emit requestExecuted(requestId, false);
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
emit requestExecuted(requestId, true);
});
return requestId;
}
QUuid Nanoleaf::setBrightness(int percentage)
{
QUuid requestId = QUuid::createUuid();
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath(QString("/api/v1/%1/state").arg(m_authToken));
QVariantMap map;
QVariantMap value;
value["value"] = percentage;
map.insert("brightness", value);
QJsonDocument body = QJsonDocument::fromVariant(map);
QNetworkRequest request;
request.setUrl(url);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->put(request, body.toJson());
//qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
emit requestExecuted(requestId, false);
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
emit requestExecuted(requestId, true);
});
return requestId;
}
QUuid Nanoleaf::setSaturation(int percentage)
{
QUuid requestId = QUuid::createUuid();
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath(QString("/api/v1/%1/state/sat").arg(m_authToken));
QVariantMap map;
QVariantMap value;
value["value"] = percentage;
map.insert("sat", value);
QJsonDocument body = QJsonDocument::fromVariant(map);
QNetworkRequest request;
request.setUrl(url);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->put(request, body.toJson());
//qDebug(dcNanoleaf()) << "Sending request" << request.url() << body.toJson();
connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
emit requestExecuted(requestId, false);
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
emit requestExecuted(requestId, true);
});
return requestId;
}
QUuid Nanoleaf::setMired(int mired)
{
//NOTE: this is just a rough conversion between mired and kelvin
int kelvin = static_cast<int>((653-mired) * 13);
QUuid requestId = setKelvin(kelvin);
return requestId;
}
QUuid Nanoleaf::setKelvin(int kelvin)
{
QUuid requestId = QUuid::createUuid();
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath(QString("/api/v1/%1/state").arg(m_authToken));
QVariantMap map;
QVariantMap value;
value["value"] = kelvin;
map.insert("ct", value);
QJsonDocument body = QJsonDocument::fromVariant(map);
QNetworkRequest request;
request.setUrl(url);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->put(request, body.toJson());
qDebug(dcNanoleaf()) << "Sending request" << request.url() << body.toJson();
connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
emit requestExecuted(requestId, false);
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
emit requestExecuted(requestId, true);
});
return requestId;
}
void Nanoleaf::getEffects()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken+"/effects/effectsList");
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
emit connectionChanged(false);
return;
}
QJsonParseError error;
QJsonDocument data = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qDebug(dcNanoleaf()) << "Recieved invalide JSON object";
return;
}
QStringList effects;
foreach (QVariant effect, data.toVariant().toList()) {
effects.append(effect.toString());
}
emit connectionChanged(true);
emit effectListReceived(effects);
});
}
void Nanoleaf::getSelectedEffect()
{
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath("/api/v1/"+m_authToken+"/effects/select");
QNetworkRequest request;
request.setUrl(url);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, this, [reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
emit connectionChanged(false);
return;
}
QString effect = reply->readAll();
emit connectionChanged(true);
emit selectedEffectReceived(effect);
});
}
QUuid Nanoleaf::setEffect(const QString &effect)
{
QUuid requestId = QUuid::createUuid();
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath(QString("/api/v1/%1/effects").arg(m_authToken));
QVariantMap map;
map.insert("select", effect);
QJsonDocument body = QJsonDocument::fromVariant(map);
QNetworkRequest request;
request.setUrl(url);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->put(request, body.toJson());
qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
emit requestExecuted(requestId, false);
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
emit requestExecuted(requestId, true);
});
return requestId;
}
QUuid Nanoleaf::identify()
{
QUuid requestId = QUuid::createUuid();
QUrl url;
url.setHost(m_address.toString());
url.setPort(m_port);
url.setScheme("http");
url.setPath(QString("/api/v1/%1/identify").arg(m_authToken));
QNetworkRequest request;
request.setUrl(url);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
QNetworkReply *reply = m_networkManager->put(request, "");
qDebug(dcNanoleaf()) << "Sending request" << request.url();
connect(reply, &QNetworkReply::finished, this, [requestId, reply, this] {
reply->deleteLater();
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// Check HTTP status code
if (status < 200 || status > 204 || reply->error() != QNetworkReply::NoError) {
if (reply->error() == QNetworkReply::HostNotFoundError) {
emit connectionChanged(false);
}
if (status >= 400 && status <= 410) {
emit authenticationStatusChanged(false);
}
emit requestExecuted(requestId, false);
qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString();
return;
}
emit requestExecuted(requestId, true);
});
return requestId;
}

136
nanoleaf/nanoleaf.h Normal file
View File

@ -0,0 +1,136 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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
* This project may also contain libraries licensed under the open source software license GNU GPL v.3.
* 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 NANOLEAF_H
#define NANOLEAF_H
#include <QObject>
#include <QTimer>
#include <QUuid>
#include <QHostAddress>
#include <QColor>
#include "network/networkaccessmanager.h"
#include "devices/device.h"
class Nanoleaf : public QObject
{
Q_OBJECT
public:
struct ControllerInfo {
QString name;
QString serialNumber;
QString manufacturer;
QString firmwareVersion;
QString model;
};
enum ColorMode {
EffectMode,
HueSaturationMode,
ColorTemperatureMode
};
enum GestureID {
SingleTap = 0,
DoubleTap = 1,
SwipeUp = 2,
SwipeDown = 3,
SwipeLeft = 4,
SwipeRight = 5
};
explicit Nanoleaf(NetworkAccessManager *networkManager, const QHostAddress &address, int port = 16021, QObject *parent = nullptr);
void setIpAddress(const QHostAddress &address);
QHostAddress ipAddress();
void setPort(int port);
int port();
void setAuthToken(const QString &token);
QString authToken();
//AUTHORIZATION
void addUser();
void deleteUser();
//GET ALL PANEL INFORMATION
void getControllerInfo();
//STATES
void getPower();
void getHue();
void getBrightness();
void getSaturation();
void getColorTemperature();
void getColorMode();
void registerForEvents();
QUuid setPower(bool power);
QUuid setColor(QColor color);
QUuid setHue(int hue);
QUuid setBrightness(int percentage);
QUuid setSaturation(int percentage);
QUuid setMired(int mired);
QUuid setKelvin(int kelvin);
//EFFECTS
void getEffects();
void getSelectedEffect();
QUuid setEffect(const QString &effect);
QUuid identify();
private:
NetworkAccessManager *m_networkManager = nullptr;
QString m_authToken;
QHostAddress m_address;
int m_port;
signals:
void connectionChanged(bool connected);
void authenticationStatusChanged(bool authenticated);
void requestExecuted(QUuid requestId, bool success);
void controllerInfoReceived(const ControllerInfo &controllerInfo);
void authTokenRecieved(const QString &token);
void powerReceived(bool power);
void brightnessReceived(int percentage);
void colorModeReceived(ColorMode colorMode);
void hueReceived(int hue);
void saturationReceived(int percentage);
void effectListReceived(const QStringList &effects);
void colorReceived(QColor color);
void colorTemperatureReceived(int kelvin);
void selectedEffectReceived(const QString &effect);
//Only supported by Canvas
void touchEventReceived(GestureID gesture);
};
#endif // NANOLEAF_H

16
nanoleaf/nanoleaf.pro Normal file
View File

@ -0,0 +1,16 @@
include(../plugins.pri)
TARGET = $$qtLibraryTarget(nymea_devicepluginnanoleaf)
QT += network
SOURCES += \
devicepluginnanoleaf.cpp \
nanoleaf.cpp \
HEADERS += \
devicepluginnanoleaf.h \
nanoleaf.h \

View File

@ -0,0 +1,222 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>DevicePluginNanoleaf</name>
<message>
<location filename="../devicepluginnanoleaf.cpp" line="95"/>
<source>On the Nanoleaf controller, hold the on-off button for 5-7 seconds until the LED starts flashing.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../devicepluginnanoleaf.cpp" line="367"/>
<source>Color temperature</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../devicepluginnanoleaf.cpp" line="370"/>
<source>Hue/Saturation</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../devicepluginnanoleaf.cpp" line="373"/>
<source>Effect</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Nanoleaf</name>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="58"/>
<source>Address</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, Type: device, ID: {ff57079f-d5ab-4511-8a5c-0726e7b82af6})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="61"/>
<source>Alert</source>
<extracomment>The name of the ActionType ({47a6a1a1-fb90-4f24-be8c-b4dba0aaaa84}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="64"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="67"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="70"/>
<source>Brightness</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, ActionType: brightness, ID: {4e5d6460-d42e-4b7c-a8f3-6e953451c1ef})
----------
The name of the ParamType (DeviceClass: lightPanels, EventType: brightness, ID: {4e5d6460-d42e-4b7c-a8f3-6e953451c1ef})
----------
The name of the StateType ({4e5d6460-d42e-4b7c-a8f3-6e953451c1ef}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="73"/>
<source>Brightness changed</source>
<extracomment>The name of the EventType ({4e5d6460-d42e-4b7c-a8f3-6e953451c1ef}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="76"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="79"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="82"/>
<source>Color</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, ActionType: color, ID: {d4a52cdc-93b2-44fc-a36c-ae65f1d98f2e})
----------
The name of the ParamType (DeviceClass: lightPanels, EventType: color, ID: {d4a52cdc-93b2-44fc-a36c-ae65f1d98f2e})
----------
The name of the StateType ({d4a52cdc-93b2-44fc-a36c-ae65f1d98f2e}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="85"/>
<source>Color changed</source>
<extracomment>The name of the EventType ({d4a52cdc-93b2-44fc-a36c-ae65f1d98f2e}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="88"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="91"/>
<source>Color mode</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, EventType: colorMode, ID: {bdd2ea1e-9ef9-4967-9678-2c601b826199})
----------
The name of the StateType ({bdd2ea1e-9ef9-4967-9678-2c601b826199}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="94"/>
<source>Color mode changed</source>
<extracomment>The name of the EventType ({bdd2ea1e-9ef9-4967-9678-2c601b826199}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="97"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="100"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="103"/>
<source>Color temperature</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, ActionType: colorTemperature, ID: {41248127-844b-40be-87e6-38aee48b6687})
----------
The name of the ParamType (DeviceClass: lightPanels, EventType: colorTemperature, ID: {41248127-844b-40be-87e6-38aee48b6687})
----------
The name of the StateType ({41248127-844b-40be-87e6-38aee48b6687}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="106"/>
<source>Color temperature changed</source>
<extracomment>The name of the EventType ({41248127-844b-40be-87e6-38aee48b6687}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="109"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="112"/>
<source>Effect name</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, EventType: effectName, ID: {57f9831e-1b98-41c1-a21c-6073ff327237})
----------
The name of the StateType ({57f9831e-1b98-41c1-a21c-6073ff327237}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="115"/>
<source>Effect name changed</source>
<extracomment>The name of the EventType ({57f9831e-1b98-41c1-a21c-6073ff327237}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="118"/>
<source>Firmware version</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, Type: device, ID: {1b85eebe-3b1a-49a9-bddb-2175d6599b95})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="121"/>
<source>Light panels</source>
<extracomment>The name of the DeviceClass ({d44ee383-9fa5-4751-babd-1129ac20896a})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="124"/>
<source>Model</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, Type: device, ID: {353d3c71-0ad2-40d5-99f6-cc305e2073f1})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="127"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="130"/>
<source>Nanoleaf</source>
<extracomment>The name of the vendor ({3d7fdaa6-7896-419b-8be3-c90c42bcac7f})
----------
The name of the plugin Nanoleaf ({360867ec-1594-498d-8182-fbab1fe17489})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="133"/>
<source>Port</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, Type: device, ID: {ba4fd45b-990d-480a-859d-fff7ffba3ba4})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="136"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="139"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="142"/>
<source>Power</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, ActionType: power, ID: {44bee9ec-513d-4834-991a-ee9ae69d9f2a})
----------
The name of the ParamType (DeviceClass: lightPanels, EventType: power, ID: {44bee9ec-513d-4834-991a-ee9ae69d9f2a})
----------
The name of the StateType ({44bee9ec-513d-4834-991a-ee9ae69d9f2a}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="145"/>
<source>Power changed</source>
<extracomment>The name of the EventType ({44bee9ec-513d-4834-991a-ee9ae69d9f2a}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="148"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="151"/>
<source>Reachable</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, EventType: connected, ID: {a3102107-a825-4ec8-a9ec-b2c2a9fb5c89})
----------
The name of the StateType ({a3102107-a825-4ec8-a9ec-b2c2a9fb5c89}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="154"/>
<source>Reachable changed</source>
<extracomment>The name of the EventType ({a3102107-a825-4ec8-a9ec-b2c2a9fb5c89}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="157"/>
<source>Serial number</source>
<extracomment>The name of the ParamType (DeviceClass: lightPanels, Type: device, ID: {18be4a5f-e2c2-4070-bc3e-ea9fe64f2276})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="160"/>
<source>Set brightness</source>
<extracomment>The name of the ActionType ({4e5d6460-d42e-4b7c-a8f3-6e953451c1ef}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="163"/>
<source>Set color</source>
<extracomment>The name of the ActionType ({d4a52cdc-93b2-44fc-a36c-ae65f1d98f2e}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="166"/>
<source>Set color temperature</source>
<extracomment>The name of the ActionType ({41248127-844b-40be-87e6-38aee48b6687}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/nanoleaf/plugininfo.h" line="169"/>
<source>Set power</source>
<extracomment>The name of the ActionType ({44bee9ec-513d-4834-991a-ee9ae69d9f2a}) of DeviceClass lightPanels</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -28,6 +28,7 @@ PLUGIN_DIRS = \
lgsmarttv \
mailnotification \
mqttclient \
nanoleaf \
netatmo \
networkdetector \
onewire \