diff --git a/debian/control b/debian/control index 1840e055..2cbfbb1d 100644 --- a/debian/control +++ b/debian/control @@ -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}, @@ -845,6 +860,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, diff --git a/debian/nymea-plugin-nanoleaf.install.in b/debian/nymea-plugin-nanoleaf.install.in new file mode 100644 index 00000000..c196777d --- /dev/null +++ b/debian/nymea-plugin-nanoleaf.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_devicepluginnanoleaf.so diff --git a/nanoleaf/README.md b/nanoleaf/README.md new file mode 100644 index 00000000..55a87ec8 --- /dev/null +++ b/nanoleaf/README.md @@ -0,0 +1 @@ +# Nanoleaf diff --git a/nanoleaf/devicepluginnanoleaf.cpp b/nanoleaf/devicepluginnanoleaf.cpp new file mode 100644 index 00000000..e2e19cfc --- /dev/null +++ b/nanoleaf/devicepluginnanoleaf.cpp @@ -0,0 +1,183 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2020 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "devicepluginnanoleaf.h" +#include "plugininfo.h" + +#include "network/zeroconf/zeroconfservicebrowser.h" +#include "platform/platformzeroconfcontroller.h" + +#include +#include + +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()) { + if (info->deviceClassId() == lightPanelsDeviceClassId) { + + } + //TODO skip duplicated devices + + 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) { + 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("Please press the button")); +} + +void DevicePluginNanoleaf::confirmPairing(DevicePairingInfo *info, const QString &username, const QString &secret) +{ + Q_UNUSED(username) + Q_UNUSED(secret) + Nanoleaf *nanoleaf = new Nanoleaf(hardwareManager()->networkManager(), QHostAddress(info->params().paramValue(lightPanelsDeviceAddressParamTypeId).toString()), info->params().paramValue(lightPanelsDevicePortParamTypeId).toInt(), this); + nanoleaf->addUser(); + info->finish(Device::DeviceErrorNoError); +} + +void DevicePluginNanoleaf::setupDevice(DeviceSetupInfo *info) +{ + Device *device = info->device(); + if(device->deviceClassId() == lightPanelsDeviceClassId) { + return info->finish(Device::DeviceErrorNoError); + } +} + +void DevicePluginNanoleaf::postSetupDevice(Device *device) +{ + if (device->deviceClassId() == lightPanelsDeviceClassId) { + //TODO get all the information and set the device states + } + + if(!m_pluginTimer) { + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(5); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { + + }); + } +} + +void DevicePluginNanoleaf::deviceRemoved(Device *device) +{ + if(device->deviceClassId() == lightPanelsDeviceClassId) { + + } + + 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 (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->setHue(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->setColorTemperature(colorTemperature); + connect(info, &DeviceActionInfo::aborted,[requestId, this](){m_asyncActions.remove(requestId);}); + m_asyncActions.insert(requestId, info); + } + } +} + +void DevicePluginNanoleaf::getDeviceStates(Nanoleaf *nanoleaf) +{ + nanoleaf->getPower(); + nanoleaf->getHue(); + nanoleaf->getColorMode(); + nanoleaf->getBrightness(); + nanoleaf->getSaturation(); + nanoleaf->getColorTemperature(); +} + diff --git a/nanoleaf/devicepluginnanoleaf.h b/nanoleaf/devicepluginnanoleaf.h new file mode 100644 index 00000000..cdc8d063 --- /dev/null +++ b/nanoleaf/devicepluginnanoleaf.h @@ -0,0 +1,64 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2020 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#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 + +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; + + +private: + ZeroConfServiceBrowser *m_zeroconfBrowser = nullptr; + PluginTimer *m_pluginTimer = nullptr; + QHash m_nanoleafConnections; + QHash m_asyncActions; + + void getDeviceStates(Nanoleaf *nanoleaf); +}; + +#endif // DEVICEPLUGINNANOLEAF_H diff --git a/nanoleaf/devicepluginnanoleaf.json b/nanoleaf/devicepluginnanoleaf.json new file mode 100644 index 00000000..86ae6b86 --- /dev/null +++ b/nanoleaf/devicepluginnanoleaf.json @@ -0,0 +1,115 @@ +{ + "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", "connectable"], + "createMethods": ["discovery"], + "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 + } + ], + "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 brigtness", + "type": "int", + "unit": "Percentage", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100, + "writable": true + + } + ] + } + ] + } + ] +} diff --git a/nanoleaf/nanoleaf.cpp b/nanoleaf/nanoleaf.cpp new file mode 100644 index 00000000..d385efa4 --- /dev/null +++ b/nanoleaf/nanoleaf.cpp @@ -0,0 +1,213 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2020 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "nanoleaf.h" +#include "extern-plugininfo.h" + +#include +#include +#include +#include + +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::addUser() +{ + QUrl url; + url.setHost(m_address.toString()); + url.setPort(m_port); + url.setPath("/api/v1/new"); + + QNetworkRequest request; + request.setUrl(url); + 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 || reply->error() != QNetworkReply::NoError) { + if (reply->error() == QNetworkReply::HostNotFoundError) { + emit connectionChanged(false); + } + if (status == 400 || status == 401) { + emit authenticationStatusChanged(false); + } + qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString(); + return; + } + emit connectionChanged(true); + emit authenticationStatusChanged(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(); + }); +} + +void Nanoleaf::deleteUser() +{ + QUrl url; + url.setHost(m_address.toString()); + url.setPort(m_port); + 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 != 204 || reply->error() != QNetworkReply::NoError) { + qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString(); + return; + } + }); +} + +void Nanoleaf::getPower() +{ + +} + +void Nanoleaf::getHue() +{ + +} + +void Nanoleaf::getBrightness() +{ + +} + +void Nanoleaf::getSaturation() +{ + +} + +void Nanoleaf::getColorTemperature() +{ + +} + +void Nanoleaf::getColorMode() +{ + +} + +QUuid Nanoleaf::setPower(bool power) +{ + QUuid requestId = QUuid::createUuid(); + QUrl url; + url.setHost(m_address.toString()); + url.setPort(m_port); + url.setPath(QString("/api/v1/%1/state/on").arg(m_authToken)); + + QVariantMap map; + QVariantMap value; + value["value"] = power; + map.insert("on", value); + QJsonDocument body = QJsonDocument::fromVariant(map); + + QNetworkRequest request; + request.setUrl(url); + 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 != 204 || reply->error() != QNetworkReply::NoError) { + emit requestedExecuted(requestId, false); + qCWarning(dcNanoleaf()) << "Request error:" << status << reply->errorString(); + return; + } + emit requestedExecuted(requestId, true); + }); + return requestId; +} + +QUuid Nanoleaf::setHue(QColor color) +{ + Q_UNUSED(color); + QUuid requestId = QUuid::createUuid(); + return requestId; +} + +QUuid Nanoleaf::setBrightness(int percentage) +{ + Q_UNUSED(percentage); + QUuid requestId = QUuid::createUuid(); + return requestId; +} + +QUuid Nanoleaf::setSaturation(int percentage) +{ + Q_UNUSED(percentage); + QUuid requestId = QUuid::createUuid(); + return requestId; +} + +QUuid Nanoleaf::setColorTemperature(int mired) +{ + Q_UNUSED(mired); + QUuid requestId = QUuid::createUuid(); + return requestId; +} + + + diff --git a/nanoleaf/nanoleaf.h b/nanoleaf/nanoleaf.h new file mode 100644 index 00000000..250d0a97 --- /dev/null +++ b/nanoleaf/nanoleaf.h @@ -0,0 +1,98 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2020 Bernhard Trinnes * + * * + * This file is part of nymea. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 library; If not, see * + * . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef NANOLEAF_H +#define NANOLEAF_H + +#include +#include +#include +#include +#include + +#include "network/networkaccessmanager.h" +#include "devices/device.h" + +class Nanoleaf : public QObject +{ + Q_OBJECT +public: + + 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(); + + //AUTHORIZATION + void addUser(); + void deleteUser(); + + //GET ALL PANEL INFORMATION + + //STATES + void getPower(); + void getHue(); + void getBrightness(); + void getSaturation(); + void getColorTemperature(); + void getColorMode(); + + QUuid setPower(bool power); + QUuid setHue(QColor color); + QUuid setBrightness(int percentage); + QUuid setSaturation(int percentage); + QUuid setColorTemperature(int mired); + + + //EFFECTS + + //PANEL LAYOUT + + //IDENTIFY + + + //EXTERNAL CONTROL + + + //RHYTHM + +private: + NetworkAccessManager *m_networkManager = nullptr; + QString m_authToken; + QHostAddress m_address; + int m_port; + +signals: + void connectionChanged(bool connected); + void authenticationStatusChanged(bool authenticated); + void requestedExecuted(QUuid requestId, bool success); + + void powerReceived(bool power); + void brightnessReceived(int percentage); + void colorModeReceived(); + void hueReceived(QColor color); + void saturationReceived(int percentage); +}; + +#endif // NANOLEAF_H diff --git a/nanoleaf/nanoleaf.pro b/nanoleaf/nanoleaf.pro new file mode 100644 index 00000000..9d3e6f6b --- /dev/null +++ b/nanoleaf/nanoleaf.pro @@ -0,0 +1,16 @@ +include(../plugins.pri) + +TARGET = $$qtLibraryTarget(nymea_devicepluginnanoleaf) + +QT += network + +SOURCES += \ + devicepluginnanoleaf.cpp \ + nanoleaf.cpp \ + +HEADERS += \ + devicepluginnanoleaf.h \ + nanoleaf.h \ + + + diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 9e1177a0..24e1cb3a 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -28,6 +28,7 @@ PLUGIN_DIRS = \ lgsmarttv \ mailnotification \ mqttclient \ + nanoleaf \ netatmo \ networkdetector \ onewire \