diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp index fdc50dcb..7b7b987e 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp @@ -21,11 +21,18 @@ const bool verbose3 = false; const char CONFIG_ADDRESS[] = "host"; //const char CONFIG_PORT[] = "port"; 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_LEFT_RIGHT[] = "panelOrderLeftRight"; 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 const char PANEL_LAYOUT[] = "layout"; const char PANEL_NUM[] = "numPanels"; @@ -38,9 +45,13 @@ const char PANEL_POS_Y[] = "y"; // List of State Information const char STATE_ON[] = "on"; -const char STATE_ONOFF_VALUE[] = "value"; -const char STATE_VALUE_TRUE[] = "true"; -const char STATE_VALUE_FALSE[] = "false"; +const char STATE_BRI[] = "brightness"; +const char STATE_HUE[] = "hue"; +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 const char DEV_DATA_NAME[] = "name"; @@ -49,10 +60,7 @@ const char DEV_DATA_MANUFACTURER[] = "manufacturer"; const char DEV_DATA_FIRMWAREVERSION[] = "firmwareVersion"; // Nanoleaf Stream Control elements -//const char STREAM_CONTROL_IP[] = "streamControlIpAddr"; -const char STREAM_CONTROL_PORT[] = "streamControlPort"; -//const char STREAM_CONTROL_PROTOCOL[] = "streamControlProtocol"; -const quint16 STREAM_CONTROL_DEFAULT_PORT = 60222; //Fixed port for Canvas; +const quint16 STREAM_CONTROL_DEFAULT_PORT = 60222; // Nanoleaf OpenAPI URLs const int API_DEFAULT_PORT = 16021; @@ -64,6 +72,8 @@ const char API_STATE[] = "state"; const char API_PANELLAYOUT[] = "panelLayout"; const char API_EFFECT[] = "effects"; +const char API_EFFECT_SELECT[] = "select"; + //Nanoleaf Control data stream const int STREAM_FRAME_PANEL_NUM_SIZE = 2; const int STREAM_FRAME_PANEL_INFO_SIZE = 8; @@ -71,7 +81,7 @@ const int STREAM_FRAME_PANEL_INFO_SIZE = 8; // Nanoleaf ssdp services const char SSDP_ID[] = "ssdp:all"; 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"; } //End of constants @@ -87,7 +97,7 @@ enum SHAPETYPES { TRIANGE_SHAPES = 8, MINI_TRIANGE_SHAPES = 8, SHAPES_CONTROLLER = 12 - }; +}; // Nanoleaf external control versions enum EXTCONTROLVERSIONS { @@ -143,6 +153,14 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig) Debug(_log, "RewriteTime : %d", this->getRewriteTime()); 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 if (deviceConfig[CONFIG_PANEL_ORDER_TOP_DOWN].isString()) { @@ -364,24 +382,13 @@ int LedDeviceNanoleaf::open() int retval = -1; _isDeviceReady = false; - QJsonDocument responseDoc; - if (changeToExternalControlMode(responseDoc)) + if (ProviderUdp::open() == 0) { - // Resolve port for Light Panels - QJsonObject jsonStreamControllInfo = responseDoc.object(); - if (!jsonStreamControllInfo.isEmpty()) - { - //Set default streaming port - _port = static_cast(jsonStreamControllInfo[STREAM_CONTROL_PORT].toInt()); - } - - if (ProviderUdp::open() == 0) - { - // Everything is OK, device is ready - _isDeviceReady = true; - retval = 0; - } + // Everything is OK, device is ready + _isDeviceReady = true; + retval = 0; } + return retval; } @@ -392,7 +399,7 @@ QJsonArray LedDeviceNanoleaf::discover() SSDPDiscover discover; // 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); QString searchTarget = SSDP_ID; @@ -508,15 +515,29 @@ bool LedDeviceNanoleaf::powerOn() { 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 _restApi->setPath(API_STATE); - httpResponse response = _restApi->put(getOnOffRequest(true)); + httpResponse response = _restApi->put(newState); if (response.error()) { QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason()); this->setInError ( errorReason ); on = false; + } else { + on = true; } + } } return on; @@ -527,9 +548,14 @@ bool LedDeviceNanoleaf::powerOff() bool off = true; if (_isDeviceReady) { + QJsonObject newState; + + QJsonObject onValue { {STATE_VALUE, false} }; + newState.insert(STATE_ON, onValue); + //Power-off the Nanoleaf device physically _restApi->setPath(API_STATE); - httpResponse response = _restApi->put(getOnOffRequest(false)); + httpResponse response = _restApi->put(newState); if (response.error()) { QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason()); @@ -540,10 +566,163 @@ bool LedDeviceNanoleaf::powerOff() return off; } -QString LedDeviceNanoleaf::getOnOffRequest(bool isOn) const +bool LedDeviceNanoleaf::storeState() { - QString state = isOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE; - return QString("{\"%1\":{\"%2\":%3}}").arg(STATE_ON, STATE_ONOFF_VALUE, state); + bool rc = true; + + 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() @@ -571,7 +750,6 @@ bool LedDeviceNanoleaf::changeToExternalControlMode(QJsonDocument& resp) resp = response.getBody(); success = true; } - return success; } diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h index ab736165..17126db9 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h @@ -126,6 +126,25 @@ protected: /// 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: /// @@ -159,14 +178,6 @@ private: /// @return True, if success 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). /// Nanoleaf specific ssdp discovery @@ -197,6 +208,21 @@ private: /// Array of the panel ids. QVector _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 diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.cpp b/libsrc/leddevice/dev_net/ProviderRestApi.cpp index d625b667..94ed1a36 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.cpp +++ b/libsrc/leddevice/dev_net/ProviderRestApi.cpp @@ -5,6 +5,7 @@ #include #include #include +#include //std includes #include @@ -154,16 +155,21 @@ httpResponse ProviderRestApi::get(const QUrl &url) 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 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. QEventLoop loop; 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) { - 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); } diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.h b/libsrc/leddevice/dev_net/ProviderRestApi.h index d59709d1..79052333 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.h +++ b/libsrc/leddevice/dev_net/ProviderRestApi.h @@ -192,6 +192,13 @@ public: /// 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 /// @@ -207,7 +214,7 @@ public: /// @param[in] body The body of the request 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 diff --git a/libsrc/leddevice/schemas/schema-nanoleaf.json b/libsrc/leddevice/schemas/schema-nanoleaf.json index ec3a81d3..fae3bf24 100644 --- a/libsrc/leddevice/schemas/schema-nanoleaf.json +++ b/libsrc/leddevice/schemas/schema-nanoleaf.json @@ -28,13 +28,47 @@ "options": { "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 }, + "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": { "type": "object", "title": "edt_dev_spec_panelorganisation_title", "access": "advanced", - "propertyOrder": 5 + "propertyOrder": 7 }, "panelOrderTopDown": { "type": "integer", @@ -48,7 +82,7 @@ "minimum": 0, "maximum": 1, "access": "advanced", - "propertyOrder": 6 + "propertyOrder": 8 }, "panelOrderLeftRight": { "type": "integer", @@ -62,7 +96,7 @@ "minimum": 0, "maximum": 1, "access": "advanced", - "propertyOrder": 7 + "propertyOrder": 9 }, "panelStartPos": { "type": "integer", @@ -71,7 +105,7 @@ "minimum": 0, "default": 0, "access": "advanced", - "propertyOrder": 8 + "propertyOrder": 10 } }, "additionalProperties": true