diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index d8518fd2..5f1781dc 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -566,8 +566,8 @@ "edt_conf_webc_port_title": "HTTP Port", "edt_conf_webc_sslport_expl": "Port for the WebServer, RPC and WebSocket HTTPS connections", "edt_conf_webc_sslport_title": "HTTPS Port", - "edt_dev_auth_key_title": "Authentication Token", - "edt_dev_auth_key_title_info": "Authentication Token required to acccess the device", + "edt_dev_auth_key_title": "Authorization Token", + "edt_dev_auth_key_title_info": "Authorization Token required to acccess the device", "edt_dev_enum_sub_min_cool_adjust": "Subtract cool white", "edt_dev_enum_sub_min_warm_adjust": "Subtract warm white", "edt_dev_enum_subtract_minimum": "Subtract minimum", @@ -1123,6 +1123,11 @@ "wiz_identify_light": "Identify $1", "wiz_ids_disabled": "Deactivated", "wiz_ids_entire": "Whole picture", + "wiz_nanoleaf_failure_auth_token": "Please press the Nanoleaf Power On/Off button within 30 seconds", + "wiz_nanoleaf_failure_auth_token_t": "User authorization token generating timeout", + "wiz_nanoleaf_press_onoff_button": "Please press the Power On/Off button on your Nanoleaf device for 5-7 seconds", + "wiz_nanoleaf_user_auth_intro": "The wizard supports you in generating a user authorization token required to allowing Hyperion to access the device.", + "wiz_nanoleaf_user_auth_title": "Authorization Token Generating Wizard", "wiz_noLights": "No $1 found! Please get the lights connected to the network or configure them manually.", "wiz_pos": "Position/State", "wiz_rgb_expl": "The color dot switches every x seconds the color (red, green), at the same time your LEDs switch the color too. Answer the questions at the bottom to check/correct your byte order.", diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 2b0304c5..c59b7ad1 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -1060,6 +1060,13 @@ $(document).ready(function () { var hue_title = 'wiz_hue_title'; changeWizard(data, hue_title, startWizardPhilipsHue); } + else if (ledType == "nanoleaf") { + var ledWizardType = ledType; + var data = { type: ledWizardType }; + var nanoleaf_user_auth_title = 'wiz_nanoleaf_user_auth_title'; + changeWizard(data, nanoleaf_user_auth_title, startWizardNanoleafUserAuth); + $('#btn_wiz_holder').hide(); + } else if (ledType == "atmoorb") { var ledWizardType = (this.checked) ? "atmoorb" : ledType; var data = { type: ledWizardType }; @@ -1341,6 +1348,13 @@ $(document).ready(function () { if (host === "") { conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(1); + switch (ledType) { + + case "nanoleaf": + $('#btn_wiz_holder').hide(); + break; + default: + } } else { let params = {}; @@ -1352,6 +1366,8 @@ $(document).ready(function () { break; case "nanoleaf": + $('#btn_wiz_holder').show(); + var token = conf_editor.getEditor("root.specificOptions.token").getValue(); if (token === "") { return; @@ -2165,15 +2181,8 @@ function updateElements(ledType, key) { case "nanoleaf": var ledProperties = devicesProperties[ledType][key]; - if (ledProperties && ledProperties.panelLayout.layout) { - //Identify non-LED type panels, e.g. Rhythm (1) and Shapes Controller (12) - var nonLedNum = 0; - for (const panel of ledProperties.panelLayout.layout.positionData) { - if (panel.shapeType === 1 || panel.shapeType === 12) { - nonLedNum++; - } - } - hardwareLedCount = ledProperties.panelLayout.layout.numPanels - nonLedNum; + if (ledProperties) { + hardwareLedCount = ledProperties.ledCount; } conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index 79fc9182..ace73805 100755 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -2187,3 +2187,90 @@ async function identify_atmoorb_device(orbId) { } } +//**************************** +// Nanoleaf Token Wizard +//**************************** +var lights = null; +function startWizardNanoleafUserAuth(e) { + //create html + var nanoleaf_user_auth_title = 'wiz_nanoleaf_user_auth_title'; + var nanoleaf_user_auth_intro = 'wiz_nanoleaf_user_auth_intro'; + + $('#wiz_header').html('' + $.i18n(nanoleaf_user_auth_title)); + $('#wizp1_body').html('

' + $.i18n(nanoleaf_user_auth_title) + '

' + $.i18n(nanoleaf_user_auth_intro) + '

'); + + $('#wizp1_footer').html(''); + + $('#wizp3_body').html('' + $.i18n('wiz_nanoleaf_press_onoff_button') + '


'); + + if (getStorage("darkMode") == "on") + $('#wizard_logo').attr("src", 'img/hyperion/logo_negativ.png'); + + //open modal + $("#wizard_modal").modal({ backdrop: "static", keyboard: false, show: true }); + + //listen for continue + $('#btn_wiz_cont').off().on('click', function () { + createNanoleafUserAuthorization(); + $('#wizp1').toggle(false); + $('#wizp3').toggle(true); + }); +} + +function createNanoleafUserAuthorization() { + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + + let params = { host: host }; + + var retryTime = 30; + var retryInterval = 2; + + var UserInterval = setInterval(function () { + + $('#wizp1').toggle(false); + $('#wizp3').toggle(true); + + (async () => { + + retryTime -= retryInterval; + $("#connectionTime").html(retryTime); + if (retryTime <= 0) { + abortConnection(UserInterval); + clearInterval(UserInterval); + + showNotification('warning', $.i18n('wiz_nanoleaf_failure_auth_token'), $.i18n('wiz_nanoleaf_failure_auth_token_t')) + + resetWizard(true); + } + else { + const res = await requestLedDeviceAddAuthorization('nanoleaf', params); + if (res && !res.error) { + var response = res.info; + + if (jQuery.isEmptyObject(response)) { + debugMessage(retryTime + ": Power On/Off button not pressed or device not reachable"); + } else { + $('#wizp1').toggle(false); + $('#wizp3').toggle(false); + + var token = response.auth_token; + if (token != 'undefined') { + conf_editor.getEditor("root.specificOptions.token").setValue(token); + } + clearInterval(UserInterval); + resetWizard(true); + } + } else { + $('#wizp1').toggle(false); + $('#wizp3').toggle(false); + clearInterval(UserInterval); + resetWizard(true); + } + } + })(); + + }, retryInterval * 1000); +} + diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp index f895cba6..19971350 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp @@ -75,7 +75,8 @@ const char API_EXT_MODE_STRING_V2[] = "{\"write\" : {\"command\" : \"display\", const char API_STATE[] = "state"; const char API_PANELLAYOUT[] = "panelLayout"; const char API_EFFECT[] = "effects"; - +const char API_IDENTIFY[] = "identify"; +const char API_ADD_USER[] = "new"; const char API_EFFECT_SELECT[] = "select"; //Nanoleaf Control data stream @@ -99,8 +100,15 @@ enum SHAPETYPES { POWER_SUPPLY= 5, HEXAGON_SHAPES = 7, TRIANGE_SHAPES = 8, - MINI_TRIANGE_SHAPES = 8, - SHAPES_CONTROLLER = 12 + MINI_TRIANGE_SHAPES = 9, + SHAPES_CONTROLLER = 12, + ELEMENTS_HEXAGONS = 14, + ELEMENTS_HEXAGONS_CORNER = 15, + LINES_CONECTOR = 16, + LIGHT_LINES = 17, + LIGHT_LINES_SINGLZONE = 18, + CONTROLLER_CAP = 19, + POWER_CONNECTOR = 20 }; // Nanoleaf external control versions @@ -194,6 +202,32 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig) return isInitOK; } +int LedDeviceNanoleaf::getHwLedCount(const QJsonObject& jsonLayout) const +{ + int hwLedCount {0}; + + const QJsonArray positionData = jsonLayout[PANEL_POSITIONDATA].toArray(); + for(const QJsonValue & value : positionData) + { + QJsonObject panelObj = value.toObject(); + int panelId = panelObj[PANEL_ID].toInt(); + int panelshapeType = panelObj[PANEL_SHAPE_TYPE].toInt(); + + DebugIf(verbose,_log, "Panel [%d] - Type: [%d]", panelId, panelshapeType); + + // Skip Rhythm and Shapes controller panels + if (panelshapeType != RHYTM && panelshapeType != SHAPES_CONTROLLER) + { + ++hwLedCount; + } + else + { // Reset non support/required features + DebugIf(verbose, _log, "Rhythm/Shape Controller panel skipped."); + } + } + return hwLedCount; +} + bool LedDeviceNanoleaf::initLedsConfiguration() { bool isInitOK = true; @@ -227,6 +261,8 @@ bool LedDeviceNanoleaf::initLedsConfiguration() QJsonObject jsonPanelLayout = jsonAllPanelInfo[API_PANELLAYOUT].toObject(); QJsonObject jsonLayout = jsonPanelLayout[PANEL_LAYOUT].toObject(); + _panelLedCount = getHwLedCount(jsonLayout); + int panelNum = jsonLayout[PANEL_NUM].toInt(); const QJsonArray positionData = jsonLayout[PANEL_POSITIONDATA].toArray(); @@ -256,6 +292,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration() } // Travers panels top down + _panelIds.clear(); for (auto posY = panelMap.crbegin(); posY != panelMap.crend(); ++posY) { // Sort panels left to right @@ -294,7 +331,6 @@ bool LedDeviceNanoleaf::initLedsConfiguration() } } - this->_panelLedCount = _panelIds.size(); _devConfig["hardwareLedCount"] = _panelLedCount; Debug(_log, "PanelsNum : %d", panelNum); @@ -314,7 +350,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration() QString errorReason = QString("Not enough panels [%1] for configured LEDs [%2] found!") .arg(_panelLedCount) .arg(configuredLedCount); - this->setInError(errorReason); + this->setInError(errorReason, false); isInitOK = false; } else @@ -330,7 +366,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration() QString errorReason = QString("Start panel [%1] out of range. Start panel position can be max [%2] given [%3] panel available!") .arg(_startPos).arg(_panelLedCount - configuredLedCount).arg(_panelLedCount); - this->setInError(errorReason); + this->setInError(errorReason, false); isInitOK = false; } } @@ -436,7 +472,7 @@ QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params) _hostName = params[CONFIG_HOST].toString(""); _apiPort = API_DEFAULT_PORT; - _authToken = params["token"].toString(""); + _authToken = params[CONFIG_AUTH_TOKEN].toString(""); Info(_log, "Get properties for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName) ); @@ -453,7 +489,14 @@ QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params) { Warning(_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); } - properties.insert("properties", response.getBody().object()); + QJsonObject propertiesDetails = response.getBody().object(); + if (!propertiesDetails.isEmpty()) + { + QJsonObject jsonLayout = propertiesDetails.value(API_PANELLAYOUT).toObject().value(PANEL_LAYOUT).toObject(); + _panelLedCount = getHwLedCount(jsonLayout); + propertiesDetails.insert("ledCount", getHwLedCount(jsonLayout)); + } + properties.insert("properties", propertiesDetails); } DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); @@ -466,8 +509,8 @@ void LedDeviceNanoleaf::identify(const QJsonObject& params) DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); _hostName = params[CONFIG_HOST].toString(""); - _apiPort = API_DEFAULT_PORT;if (NetUtils::resolveHostToAddress(_log, _hostName, _address)) - _authToken = params["token"].toString(""); + _apiPort = API_DEFAULT_PORT; + _authToken = params[CONFIG_AUTH_TOKEN].toString(""); Info(_log, "Identify %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName) ); @@ -475,9 +518,7 @@ void LedDeviceNanoleaf::identify(const QJsonObject& params) { if ( openRestAPI() ) { - _restApi->setPath("identify"); - - // Perform request + _restApi->setPath(API_IDENTIFY); httpResponse response = _restApi->put(); if (response.error()) { @@ -487,6 +528,36 @@ void LedDeviceNanoleaf::identify(const QJsonObject& params) } } +QJsonObject LedDeviceNanoleaf::addAuthorization(const QJsonObject& params) +{ + Debug(_log, "params: [%s]", QJsonDocument(params).toJson(QJsonDocument::Compact).constData()); + QJsonObject responseBody; + + _hostName = params[CONFIG_HOST].toString(""); + _apiPort = API_DEFAULT_PORT; + + Info(_log, "Generate user authorization token for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName) ); + + if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) + { + if ( openRestAPI() ) + { + _restApi->setBasePath(QString(API_BASE_PATH).arg(API_ADD_USER)); + httpResponse response = _restApi->post(); + if (response.error()) + { + Warning(_log, "%s generating user authorization token failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); + } + else + { + Debug(_log, "Generated user authorization token: \"%s\"", QSTRING_CSTR(response.getBody().object().value("auth_token").toString()) ); + responseBody = response.getBody().object(); + } + } + } + return responseBody; +} + bool LedDeviceNanoleaf::powerOn() { bool on = false; diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h index a1df1f80..a0a59094 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.h @@ -87,6 +87,20 @@ public: /// void identify(const QJsonObject& params) override; + /// @brief Add an API-token to the Nanoleaf device + /// + /// Following parameters are required + /// @code + /// { + /// "host" : "hostname or IP", + /// } + ///@endcode + /// + /// @param[in] params Parameters to query device + /// @return A JSON structure holding the authorization keys + /// + QJsonObject addAuthorization(const QJsonObject& params) override; + protected: /// @@ -182,6 +196,13 @@ private: /// QJsonArray discover(); + /// + /// @brief Get number of panels that can be used as LEds. + /// + /// @return Number of usable LED panels + /// + int getHwLedCount(const QJsonObject& jsonLayout) const; + ///REST-API wrapper ProviderRestApi* _restApi; int _apiPort; diff --git a/libsrc/leddevice/dev_net/ProviderRestApi.cpp b/libsrc/leddevice/dev_net/ProviderRestApi.cpp index 359a4ea5..e2d07475 100644 --- a/libsrc/leddevice/dev_net/ProviderRestApi.cpp +++ b/libsrc/leddevice/dev_net/ProviderRestApi.cpp @@ -318,7 +318,6 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) } else { - qDebug() << "httpStatusCode: "<< httpStatusCode; if (httpStatusCode > 0) { QString httpReason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); QString advise; @@ -327,7 +326,7 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply) advise = "Check Request Body"; break; case HttpStatusCode::UnAuthorized: - advise = "Check Authentication Token (API Key)"; + advise = "Check Authorization Token (API Key)"; break; case HttpStatusCode::Forbidden: advise = "No permission to access the given resource";