nymea-plugins/lifx/lifxcloud.cpp

369 lines
14 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-plugins.
*
* nymea-plugins is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-plugins 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
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#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) {
qCDebug(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) {
qCDebug(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 = std::rand();
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 = std::rand();
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 = std::rand();
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;
}