Fix Nanoleaf, add user auth token wizard

This commit is contained in:
LordGrey 2023-10-02 17:43:02 +02:00
parent d7666987e9
commit 4a5f89845c
6 changed files with 218 additions and 26 deletions

View File

@ -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.",

View File

@ -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);

View File

@ -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('<i class="fa fa-magic fa-fw"></i>' + $.i18n(nanoleaf_user_auth_title));
$('#wizp1_body').html('<h4 style="font-weight:bold;text-transform:uppercase;">' + $.i18n(nanoleaf_user_auth_title) + '</h4><p>' + $.i18n(nanoleaf_user_auth_intro) + '</p>');
$('#wizp1_footer').html('<button type="button" class="btn btn-primary" id="btn_wiz_cont"><i class="fa fa-fw fa-check"></i>'
+ $.i18n('general_btn_continue') + '</button><button type="button" class="btn btn-danger" data-dismiss="modal"><i class="fa fa-fw fa-close"></i>'
+ $.i18n('general_btn_cancel') + '</button>');
$('#wizp3_body').html('<span>' + $.i18n('wiz_nanoleaf_press_onoff_button') + '</span> <br /><br /><center><span id="connectionTime"></span><br /><i class="fa fa-cog fa-spin" style="font-size:100px"></i></center>');
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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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";