Nanoleaf Updates (#1299)

* Discover additional Nanoleaf devices

* Fix Nanoleaf not turning on

* Added LGTM configuration file

* Allow to pass QJsonObject as payload for put

* Nanoleaf - Support Restore State & Overwrite Brightness

* Removed because this is already included

Co-authored-by: Markus <16664240+Paulchen-Panther@users.noreply.github.com>
This commit is contained in:
LordGrey 2021-08-31 10:55:49 +02:00 committed by GitHub
parent 12cdd1d58a
commit f0bd38d473
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 301 additions and 50 deletions

View File

@ -21,11 +21,18 @@ const bool verbose3 = false;
const char CONFIG_ADDRESS[] = "host"; const char CONFIG_ADDRESS[] = "host";
//const char CONFIG_PORT[] = "port"; //const char CONFIG_PORT[] = "port";
const char CONFIG_AUTH_TOKEN[] = "token"; const char CONFIG_AUTH_TOKEN[] = "token";
const char CONFIG_RESTORE_STATE[] = "restoreOriginalState";
const char CONFIG_BRIGHTNESS[] = "brightness";
const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness";
const char CONFIG_PANEL_ORDER_TOP_DOWN[] = "panelOrderTopDown"; const char CONFIG_PANEL_ORDER_TOP_DOWN[] = "panelOrderTopDown";
const char CONFIG_PANEL_ORDER_LEFT_RIGHT[] = "panelOrderLeftRight"; const char CONFIG_PANEL_ORDER_LEFT_RIGHT[] = "panelOrderLeftRight";
const char CONFIG_PANEL_START_POS[] = "panelStartPos"; const char CONFIG_PANEL_START_POS[] = "panelStartPos";
const bool DEFAULT_IS_RESTORE_STATE = true;
const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true;
const int BRI_MAX = 100;
// Panel configuration settings // Panel configuration settings
const char PANEL_LAYOUT[] = "layout"; const char PANEL_LAYOUT[] = "layout";
const char PANEL_NUM[] = "numPanels"; const char PANEL_NUM[] = "numPanels";
@ -38,9 +45,13 @@ const char PANEL_POS_Y[] = "y";
// List of State Information // List of State Information
const char STATE_ON[] = "on"; const char STATE_ON[] = "on";
const char STATE_ONOFF_VALUE[] = "value"; const char STATE_BRI[] = "brightness";
const char STATE_VALUE_TRUE[] = "true"; const char STATE_HUE[] = "hue";
const char STATE_VALUE_FALSE[] = "false"; const char STATE_SAT[] = "sat";
const char STATE_CT[] = "ct";
const char STATE_COLORMODE[] = "colorMode";
const QStringList COLOR_MODES {"hs", "ct", "effect"};
const char STATE_VALUE[] = "value";
// Device Data elements // Device Data elements
const char DEV_DATA_NAME[] = "name"; const char DEV_DATA_NAME[] = "name";
@ -49,10 +60,7 @@ const char DEV_DATA_MANUFACTURER[] = "manufacturer";
const char DEV_DATA_FIRMWAREVERSION[] = "firmwareVersion"; const char DEV_DATA_FIRMWAREVERSION[] = "firmwareVersion";
// Nanoleaf Stream Control elements // Nanoleaf Stream Control elements
//const char STREAM_CONTROL_IP[] = "streamControlIpAddr"; const quint16 STREAM_CONTROL_DEFAULT_PORT = 60222;
const char STREAM_CONTROL_PORT[] = "streamControlPort";
//const char STREAM_CONTROL_PROTOCOL[] = "streamControlProtocol";
const quint16 STREAM_CONTROL_DEFAULT_PORT = 60222; //Fixed port for Canvas;
// Nanoleaf OpenAPI URLs // Nanoleaf OpenAPI URLs
const int API_DEFAULT_PORT = 16021; const int API_DEFAULT_PORT = 16021;
@ -64,6 +72,8 @@ const char API_STATE[] = "state";
const char API_PANELLAYOUT[] = "panelLayout"; const char API_PANELLAYOUT[] = "panelLayout";
const char API_EFFECT[] = "effects"; const char API_EFFECT[] = "effects";
const char API_EFFECT_SELECT[] = "select";
//Nanoleaf Control data stream //Nanoleaf Control data stream
const int STREAM_FRAME_PANEL_NUM_SIZE = 2; const int STREAM_FRAME_PANEL_NUM_SIZE = 2;
const int STREAM_FRAME_PANEL_INFO_SIZE = 8; const int STREAM_FRAME_PANEL_INFO_SIZE = 8;
@ -71,7 +81,7 @@ const int STREAM_FRAME_PANEL_INFO_SIZE = 8;
// Nanoleaf ssdp services // Nanoleaf ssdp services
const char SSDP_ID[] = "ssdp:all"; const char SSDP_ID[] = "ssdp:all";
const char SSDP_FILTER_HEADER[] = "ST"; const char SSDP_FILTER_HEADER[] = "ST";
const char SSDP_CANVAS[] = "nanoleaf:nl29"; const char SSDP_NANOLEAF[] = "nanoleaf:nl*";
const char SSDP_LIGHTPANELS[] = "nanoleaf_aurora:light"; const char SSDP_LIGHTPANELS[] = "nanoleaf_aurora:light";
} //End of constants } //End of constants
@ -87,7 +97,7 @@ enum SHAPETYPES {
TRIANGE_SHAPES = 8, TRIANGE_SHAPES = 8,
MINI_TRIANGE_SHAPES = 8, MINI_TRIANGE_SHAPES = 8,
SHAPES_CONTROLLER = 12 SHAPES_CONTROLLER = 12
}; };
// Nanoleaf external control versions // Nanoleaf external control versions
enum EXTCONTROLVERSIONS { enum EXTCONTROLVERSIONS {
@ -143,6 +153,14 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig)
Debug(_log, "RewriteTime : %d", this->getRewriteTime()); Debug(_log, "RewriteTime : %d", this->getRewriteTime());
Debug(_log, "LatchTime : %d", this->getLatchTime()); Debug(_log, "LatchTime : %d", this->getLatchTime());
_isRestoreOrigState = _devConfig[CONFIG_RESTORE_STATE].toBool(DEFAULT_IS_RESTORE_STATE);
_isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE);
_brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX);
Debug(_log, "RestoreOrigState : %d", _isRestoreOrigState);
Debug(_log, "Overwrite Brightn.: %d", _isBrightnessOverwrite);
Debug(_log, "Set Brightness to : %d", _brightness);
// Read panel organisation configuration // Read panel organisation configuration
if (deviceConfig[CONFIG_PANEL_ORDER_TOP_DOWN].isString()) if (deviceConfig[CONFIG_PANEL_ORDER_TOP_DOWN].isString())
{ {
@ -364,24 +382,13 @@ int LedDeviceNanoleaf::open()
int retval = -1; int retval = -1;
_isDeviceReady = false; _isDeviceReady = false;
QJsonDocument responseDoc; if (ProviderUdp::open() == 0)
if (changeToExternalControlMode(responseDoc))
{ {
// Resolve port for Light Panels // Everything is OK, device is ready
QJsonObject jsonStreamControllInfo = responseDoc.object(); _isDeviceReady = true;
if (!jsonStreamControllInfo.isEmpty()) retval = 0;
{
//Set default streaming port
_port = static_cast<uchar>(jsonStreamControllInfo[STREAM_CONTROL_PORT].toInt());
}
if (ProviderUdp::open() == 0)
{
// Everything is OK, device is ready
_isDeviceReady = true;
retval = 0;
}
} }
return retval; return retval;
} }
@ -392,7 +399,7 @@ QJsonArray LedDeviceNanoleaf::discover()
SSDPDiscover discover; SSDPDiscover discover;
// Search for Canvas and Light-Panels // Search for Canvas and Light-Panels
QString searchTargetFilter = QString("%1|%2").arg(SSDP_CANVAS, SSDP_LIGHTPANELS); QString searchTargetFilter = QString("%1|%2").arg(SSDP_NANOLEAF, SSDP_LIGHTPANELS);
discover.setSearchFilter(searchTargetFilter, SSDP_FILTER_HEADER); discover.setSearchFilter(searchTargetFilter, SSDP_FILTER_HEADER);
QString searchTarget = SSDP_ID; QString searchTarget = SSDP_ID;
@ -508,15 +515,29 @@ bool LedDeviceNanoleaf::powerOn()
{ {
if (changeToExternalControlMode()) if (changeToExternalControlMode())
{ {
QJsonObject newState;
QJsonObject onValue { {STATE_VALUE, true} };
newState.insert(STATE_ON, onValue);
if ( _isBrightnessOverwrite)
{
QJsonObject briValue { {STATE_VALUE, _brightness} };
newState.insert(STATE_BRI, briValue);
}
//Power-on Nanoleaf device //Power-on Nanoleaf device
_restApi->setPath(API_STATE); _restApi->setPath(API_STATE);
httpResponse response = _restApi->put(getOnOffRequest(true)); httpResponse response = _restApi->put(newState);
if (response.error()) if (response.error())
{ {
QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason()); QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason());
this->setInError ( errorReason ); this->setInError ( errorReason );
on = false; on = false;
} else {
on = true;
} }
} }
} }
return on; return on;
@ -527,9 +548,14 @@ bool LedDeviceNanoleaf::powerOff()
bool off = true; bool off = true;
if (_isDeviceReady) if (_isDeviceReady)
{ {
QJsonObject newState;
QJsonObject onValue { {STATE_VALUE, false} };
newState.insert(STATE_ON, onValue);
//Power-off the Nanoleaf device physically //Power-off the Nanoleaf device physically
_restApi->setPath(API_STATE); _restApi->setPath(API_STATE);
httpResponse response = _restApi->put(getOnOffRequest(false)); httpResponse response = _restApi->put(newState);
if (response.error()) if (response.error())
{ {
QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason()); QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason());
@ -540,10 +566,163 @@ bool LedDeviceNanoleaf::powerOff()
return off; return off;
} }
QString LedDeviceNanoleaf::getOnOffRequest(bool isOn) const bool LedDeviceNanoleaf::storeState()
{ {
QString state = isOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE; bool rc = true;
return QString("{\"%1\":{\"%2\":%3}}").arg(STATE_ON, STATE_ONOFF_VALUE, state);
if ( _isRestoreOrigState )
{
_restApi->setPath(API_STATE);
httpResponse response = _restApi->get();
if ( response.error() )
{
QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason());
setInError(errorReason);
rc = false;
}
else
{
_originalStateProperties = response.getBody().object();
DebugIf(verbose, _log, "state: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() );
QJsonObject isOn = _originalStateProperties.value(STATE_ON).toObject();
if (!isOn.isEmpty())
{
_originalIsOn = isOn[STATE_VALUE].toBool();
}
QJsonObject bri = _originalStateProperties.value(STATE_BRI).toObject();
if (!bri.isEmpty())
{
_originalBri = bri[STATE_VALUE].toInt();
}
_originalColorMode = _originalStateProperties[STATE_COLORMODE].toString();
switch(COLOR_MODES.indexOf(_originalColorMode)) {
case 0:
{
// hs
QJsonObject hue = _originalStateProperties.value(STATE_HUE).toObject();
if (!hue.isEmpty())
{
_originalHue = hue[STATE_VALUE].toInt();
}
QJsonObject sat = _originalStateProperties.value(STATE_SAT).toObject();
if (!sat.isEmpty())
{
_originalSat = sat[STATE_VALUE].toInt();
}
break;
}
case 1:
{
// ct
QJsonObject ct = _originalStateProperties.value(STATE_CT).toObject();
if (!ct.isEmpty())
{
_originalCt = ct[STATE_VALUE].toInt();
}
break;
}
case 2:
{
// effect
_restApi->setPath(API_EFFECT);
httpResponse response = _restApi->get();
if ( response.error() )
{
QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason());
setInError(errorReason);
rc = false;
}
else
{
QJsonObject effects = response.getBody().object();
DebugIf(verbose, _log, "effects: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() );
_originalEffect = effects[API_EFFECT_SELECT].toString();
_originalIsDynEffect = _originalEffect == "*Dynamic*" || _originalEffect == "*Solid*";
}
break;
}
default:
QString errorReason = QString("Unknown ColorMode: '%1'").arg(_originalColorMode);
setInError(errorReason);
rc = false;
break;
}
}
}
return rc;
}
bool LedDeviceNanoleaf::restoreState()
{
bool rc = true;
if ( _isRestoreOrigState )
{
QJsonObject newState;
switch(COLOR_MODES.indexOf(_originalColorMode)) {
case 0:
{ // hs
QJsonObject hueValue { {STATE_VALUE, _originalHue} };
newState.insert(STATE_HUE, hueValue);
QJsonObject satValue { {STATE_VALUE, _originalSat} };
newState.insert(STATE_SAT, satValue);
break;
}
case 1:
{ // ct
QJsonObject ctValue { {STATE_VALUE, _originalCt} };
newState.insert(STATE_CT, ctValue);
break;
}
case 2:
{ // effect
if (!_originalIsDynEffect)
{
QJsonObject newEffect;
newEffect[API_EFFECT_SELECT] = _originalEffect;
_restApi->setPath(API_EFFECT);
httpResponse response = _restApi->put(newEffect);
if ( response.error() )
{
Warning (_log, "%s restoring effect failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason()));
}
} else {
Warning (_log, "%s restoring effect failed with error: Cannot restore dynamic or solid effect. Turning device off", QSTRING_CSTR(_activeDeviceType));
_originalIsOn = false;
}
break;
}
default:
Warning (_log, "%s restoring failed with error: Unknown ColorMode", QSTRING_CSTR(_activeDeviceType));
rc = false;
}
if (!_originalIsDynEffect)
{
QJsonObject briValue { {STATE_VALUE, _originalBri} };
newState.insert(STATE_BRI, briValue);
}
QJsonObject onValue { {STATE_VALUE, _originalIsOn} };
newState.insert(STATE_ON, onValue);
_restApi->setPath(API_STATE);
httpResponse response = _restApi->put(newState);
if ( response.error() )
{
Warning (_log, "%s restoring state failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason()));
rc = false;
}
}
return rc;
} }
bool LedDeviceNanoleaf::changeToExternalControlMode() bool LedDeviceNanoleaf::changeToExternalControlMode()
@ -571,7 +750,6 @@ bool LedDeviceNanoleaf::changeToExternalControlMode(QJsonDocument& resp)
resp = response.getBody(); resp = response.getBody();
success = true; success = true;
} }
return success; return success;
} }

View File

@ -126,6 +126,25 @@ protected:
/// ///
bool powerOff() override; bool powerOff() override;
///
/// @brief Store the device's original state.
///
/// Save the device's state before hyperion color streaming starts allowing to restore state during switchOff().
///
/// @return True if success
///
bool storeState() override;
///
/// @brief Restore the device's original state.
///
/// Restore the device's state as before hyperion color streaming started.
/// This includes the on/off state of the device.
///
/// @return True, if success
///
bool restoreState() override;
private: private:
/// ///
@ -159,14 +178,6 @@ private:
/// @return True, if success /// @return True, if success
bool changeToExternalControlMode(QJsonDocument& resp); bool changeToExternalControlMode(QJsonDocument& resp);
///
/// @brief Get command to power Nanoleaf device on or off
///
/// @param isOn True, if to switch on device
/// @return Command to switch device on/off
///
QString getOnOffRequest(bool isOn) const;
/// ///
/// @brief Discover Nanoleaf devices available (for configuration). /// @brief Discover Nanoleaf devices available (for configuration).
/// Nanoleaf specific ssdp discovery /// Nanoleaf specific ssdp discovery
@ -197,6 +208,21 @@ private:
/// Array of the panel ids. /// Array of the panel ids.
QVector<int> _panelIds; QVector<int> _panelIds;
QJsonObject _originalStateProperties;
bool _isBrightnessOverwrite;
int _brightness;
QString _originalColorMode;
bool _originalIsOn;
int _originalHue;
int _originalSat;
int _originalCt;
int _originalBri;
QString _originalEffect;
bool _originalIsDynEffect {false};
}; };
#endif // LEDEVICENANOLEAF_H #endif // LEDEVICENANOLEAF_H

View File

@ -5,6 +5,7 @@
#include <QEventLoop> #include <QEventLoop>
#include <QNetworkReply> #include <QNetworkReply>
#include <QByteArray> #include <QByteArray>
#include <QJsonObject>
//std includes //std includes
#include <iostream> #include <iostream>
@ -154,16 +155,21 @@ httpResponse ProviderRestApi::get(const QUrl &url)
return response; return response;
} }
httpResponse ProviderRestApi::put(const QString &body) httpResponse ProviderRestApi::put(const QJsonObject &body)
{ {
return put( getUrl(), body ); return put( getUrl(), QJsonDocument(body).toJson(QJsonDocument::Compact));
} }
httpResponse ProviderRestApi::put(const QUrl &url, const QString &body) httpResponse ProviderRestApi::put(const QString &body)
{
return put( getUrl(), body.toUtf8() );
}
httpResponse ProviderRestApi::put(const QUrl &url, const QByteArray &body)
{ {
// Perform request // Perform request
QNetworkRequest request(url); QNetworkRequest request(url);
QNetworkReply* reply = _networkManager->put(request, body.toUtf8()); QNetworkReply* reply = _networkManager->put(request, body);
// Connect requestFinished signal to quit slot of the loop. // Connect requestFinished signal to quit slot of the loop.
QEventLoop loop; QEventLoop loop;
QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QEventLoop::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
@ -178,7 +184,7 @@ httpResponse ProviderRestApi::put(const QUrl &url, const QString &body)
{ {
if(reply->error() != QNetworkReply::NoError) if(reply->error() != QNetworkReply::NoError)
{ {
Debug(_log, "PUT: [%s] [%s]", QSTRING_CSTR( url.toString() ), QSTRING_CSTR( body ) ); Debug(_log, "PUT: [%s] [%s]", QSTRING_CSTR( url.toString() ),body.constData() );
} }
response = getResponse(reply); response = getResponse(reply);
} }

View File

@ -192,6 +192,13 @@ public:
/// ///
httpResponse get(const QUrl &url); httpResponse get(const QUrl &url);
/// @brief Execute PUT request
///
/// @param[in] body The body of the request in JSON
/// @return Response The body of the response in JSON
///
httpResponse put(const QJsonObject &body);
/// ///
/// @brief Execute PUT request /// @brief Execute PUT request
/// ///
@ -207,7 +214,7 @@ public:
/// @param[in] body The body of the request in JSON /// @param[in] body The body of the request in JSON
/// @return Response The body of the response in JSON /// @return Response The body of the response in JSON
/// ///
httpResponse put(const QUrl &url, const QString &body = ""); httpResponse put(const QUrl &url, const QByteArray &body);
/// ///
/// @brief Execute POST request /// @brief Execute POST request

View File

@ -28,13 +28,47 @@
"options": { "options": {
"infoText": "edt_dev_auth_key_title_info" "infoText": "edt_dev_auth_key_title_info"
}, },
"propertyOrder": 3
},
"restoreOriginalState": {
"type": "boolean",
"format": "checkbox",
"title": "edt_dev_spec_restoreOriginalState_title",
"default": true,
"required": true,
"options": {
"infoText": "edt_dev_spec_restoreOriginalState_title_info"
},
"propertyOrder": 4 "propertyOrder": 4
}, },
"overwriteBrightness": {
"type": "boolean",
"format": "checkbox",
"title": "edt_dev_spec_brightnessOverwrite_title",
"default": true,
"required": true,
"access": "advanced",
"propertyOrder": 5
},
"brightness": {
"type": "integer",
"title": "edt_dev_spec_brightness_title",
"default": 100,
"minimum": 1,
"maximum": 100,
"options": {
"dependencies": {
"overwriteBrightness": true
}
},
"access": "advanced",
"propertyOrder": 6
},
"title": { "title": {
"type": "object", "type": "object",
"title": "edt_dev_spec_panelorganisation_title", "title": "edt_dev_spec_panelorganisation_title",
"access": "advanced", "access": "advanced",
"propertyOrder": 5 "propertyOrder": 7
}, },
"panelOrderTopDown": { "panelOrderTopDown": {
"type": "integer", "type": "integer",
@ -48,7 +82,7 @@
"minimum": 0, "minimum": 0,
"maximum": 1, "maximum": 1,
"access": "advanced", "access": "advanced",
"propertyOrder": 6 "propertyOrder": 8
}, },
"panelOrderLeftRight": { "panelOrderLeftRight": {
"type": "integer", "type": "integer",
@ -62,7 +96,7 @@
"minimum": 0, "minimum": 0,
"maximum": 1, "maximum": 1,
"access": "advanced", "access": "advanced",
"propertyOrder": 7 "propertyOrder": 9
}, },
"panelStartPos": { "panelStartPos": {
"type": "integer", "type": "integer",
@ -71,7 +105,7 @@
"minimum": 0, "minimum": 0,
"default": 0, "default": 0,
"access": "advanced", "access": "advanced",
"propertyOrder": 8 "propertyOrder": 10
} }
}, },
"additionalProperties": true "additionalProperties": true