This commit is contained in:
LordGrey 2023-10-05 20:31:59 +00:00 committed by GitHub
commit 25e9e4ac1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 626 additions and 227 deletions

View File

@ -10,13 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Nanoleaf: Wizard to generate user authorization token allowing users to configure the device via a single window
- Nanoleaf: Generation of a default layout per device's configuration, including oriantation
### Changed ### Changed
### Fixed ### Fixed
- Fixed missing Include limits in QJsonSchemaChecker - Fixed missing Include limits in QJsonSchemaChecker
- Fixed dependencies for deb packages in Debian Bookworm - Fixed dependencies for deb packages in Debian Bookworm
- Nanoleaf: "Panel numbering sequence" was not configurable any longer
- Nanoleaf: Number of panels increased during retries (#1643)
## Removed ### Removed
- Nanoleaf: Removed "Start Position" in favour of the general Blacklist feature provided
## [2.0.15](https://github.com/hyperion-project/hyperion.ng/releases/tag/2.0.15) - 2023-02 ## [2.0.15](https://github.com/hyperion-project/hyperion.ng/releases/tag/2.0.15) - 2023-02

View File

@ -46,6 +46,9 @@
</div> </div>
</div> </div>
<div class="panel-footer" style="text-align:right"> <div class="panel-footer" style="text-align:right">
<button id='btn_layout_controller' class="btn btn-primary" disabled data-toggle="tooltip" data-placement="top" title="Generate a layout for the configured device">
<i class="fa fa-fw fa-save"></i><span data-i18n="wiz_layout">Generate Layout</span>
</button>
<button id='btn_test_controller' class="btn btn-primary" disabled data-toggle="tooltip" data-placement="top" title="Identify configured device by lighting it up"> <button id='btn_test_controller' class="btn btn-primary" disabled data-toggle="tooltip" data-placement="top" title="Identify configured device by lighting it up">
<i class="fa fa-fw fa-save"></i><span data-i18n="wiz_identify">Identify/Test</span> <i class="fa fa-fw fa-save"></i><span data-i18n="wiz_identify">Identify/Test</span>
</button> </button>

View File

@ -118,6 +118,8 @@
"conf_leds_layout_cl_vleddepth": "Vertical LED depth", "conf_leds_layout_cl_vleddepth": "Vertical LED depth",
"conf_leds_layout_frame": "Classic Layout (LED Frame)", "conf_leds_layout_frame": "Classic Layout (LED Frame)",
"conf_leds_layout_generatedconf": "Generated/Current LED Configuration", "conf_leds_layout_generatedconf": "Generated/Current LED Configuration",
"conf_leds_layout_generation_success": "LED Layout generated sucessfully",
"conf_leds_layout_generation_error": "LED Layout was not generated",
"conf_leds_layout_intro": "You also need an LED layout, which reflects your LED positions. The classic layout is the usually used TV frame, but we also support LED matrix (LED walls) creation. The view on this layout is ALWAYS from the FRONT of your TV.", "conf_leds_layout_intro": "You also need an LED layout, which reflects your LED positions. The classic layout is the usually used TV frame, but we also support LED matrix (LED walls) creation. The view on this layout is ALWAYS from the FRONT of your TV.",
"conf_leds_layout_ma_cabling": "Cabling", "conf_leds_layout_ma_cabling": "Cabling",
"conf_leds_layout_ma_direction": "Direction", "conf_leds_layout_ma_direction": "Direction",
@ -566,8 +568,8 @@
"edt_conf_webc_port_title": "HTTP Port", "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_expl": "Port for the WebServer, RPC and WebSocket HTTPS connections",
"edt_conf_webc_sslport_title": "HTTPS Port", "edt_conf_webc_sslport_title": "HTTPS Port",
"edt_dev_auth_key_title": "Authentication Token", "edt_dev_auth_key_title": "Authorization Token",
"edt_dev_auth_key_title_info": "Authentication Token required to acccess the device", "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_cool_adjust": "Subtract cool white",
"edt_dev_enum_sub_min_warm_adjust": "Subtract warm white", "edt_dev_enum_sub_min_warm_adjust": "Subtract warm white",
"edt_dev_enum_subtract_minimum": "Subtract minimum", "edt_dev_enum_subtract_minimum": "Subtract minimum",
@ -1121,8 +1123,15 @@
"wiz_identify": "Identify", "wiz_identify": "Identify",
"wiz_identify_tip": "Identify configured device by lighting it up", "wiz_identify_tip": "Identify configured device by lighting it up",
"wiz_identify_light": "Identify $1", "wiz_identify_light": "Identify $1",
"wiz_layout": "Generate Layout",
"wiz_layout_tip": "Generate a layout for the configured device",
"wiz_ids_disabled": "Deactivated", "wiz_ids_disabled": "Deactivated",
"wiz_ids_entire": "Whole picture", "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_noLights": "No $1 found! Please get the lights connected to the network or configure them manually.",
"wiz_pos": "Position/State", "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.", "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

@ -1050,7 +1050,7 @@ $(document).ready(function () {
// change save button state based on validation result // change save button state based on validation result
conf_editor.validate().length || window.readOnlyMode ? $('#btn_submit_controller').prop('disabled', true) : $('#btn_submit_controller').prop('disabled', false); conf_editor.validate().length || window.readOnlyMode ? $('#btn_submit_controller').prop('disabled', true) : $('#btn_submit_controller').prop('disabled', false);
// led controller sepecific wizards // LED controller specific wizards
$('#btn_wiz_holder').html(""); $('#btn_wiz_holder').html("");
$('#btn_led_device_wiz').off(); $('#btn_led_device_wiz').off();
@ -1060,6 +1060,13 @@ $(document).ready(function () {
var hue_title = 'wiz_hue_title'; var hue_title = 'wiz_hue_title';
changeWizard(data, hue_title, startWizardPhilipsHue); 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") { else if (ledType == "atmoorb") {
var ledWizardType = (this.checked) ? "atmoorb" : ledType; var ledWizardType = (this.checked) ? "atmoorb" : ledType;
var data = { type: ledWizardType }; var data = { type: ledWizardType };
@ -1084,6 +1091,7 @@ $(document).ready(function () {
var colorOrderDefault = "rgb"; var colorOrderDefault = "rgb";
var filter = {}; var filter = {};
$('#btn_layout_controller').hide();
$('#btn_test_controller').hide(); $('#btn_test_controller').hide();
switch (ledType) { switch (ledType) {
@ -1341,6 +1349,13 @@ $(document).ready(function () {
if (host === "") { if (host === "") {
conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(1); conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(1);
switch (ledType) {
case "nanoleaf":
$('#btn_wiz_holder').hide();
break;
default:
}
} }
else { else {
let params = {}; let params = {};
@ -1352,6 +1367,8 @@ $(document).ready(function () {
break; break;
case "nanoleaf": case "nanoleaf":
$('#btn_wiz_holder').show();
var token = conf_editor.getEditor("root.specificOptions.token").getValue(); var token = conf_editor.getEditor("root.specificOptions.token").getValue();
if (token === "") { if (token === "") {
return; return;
@ -1668,6 +1685,33 @@ $(document).ready(function () {
$("#leddevices").val(window.serverConfig.device.type); $("#leddevices").val(window.serverConfig.device.type);
$("#leddevices").trigger("change"); $("#leddevices").trigger("change");
// Generate layout for LED-Device
$("#btn_layout_controller").off().on("click", function () {
var ledType = $("#leddevices").val();
var isGenerated = false;
switch (ledType) {
case "nanoleaf":
var host = conf_editor.getEditor("root.specificOptions.host").getValue();
var ledDeviceProperties = devicesProperties[ledType][host];
if (ledDeviceProperties) {
var panelOrderTopDown = conf_editor.getEditor("root.specificOptions.panelOrderTopDown").getValue() === "top2down";
var panelOrderLeftRight = conf_editor.getEditor("root.specificOptions.panelOrderLeftRight").getValue() === "left2right";
var ledArray = nanoleafGeneratelayout(ledDeviceProperties.panelLayout, panelOrderTopDown, panelOrderLeftRight);
aceEdt.set(ledArray);
isGenerated = true;
}
break;
default:
}
if (isGenerated) {
showInfoDialog('success', "", $.i18n('conf_leds_layout_generation_success'));
} else {
showInfoDialog('error', "", $.i18n('conf_leds_layout_generation_error'));
}
});
// Identify/ Test LED-Device // Identify/ Test LED-Device
$("#btn_test_controller").off().on("click", function () { $("#btn_test_controller").off().on("click", function () {
var ledType = $("#leddevices").val(); var ledType = $("#leddevices").val();
@ -2147,6 +2191,7 @@ async function identify_device(type, params) {
} }
function updateElements(ledType, key) { function updateElements(ledType, key) {
var canLayout = false;
if (devicesProperties[ledType][key]) { if (devicesProperties[ledType][key]) {
var hardwareLedCount = 1; var hardwareLedCount = 1;
switch (ledType) { switch (ledType) {
@ -2165,18 +2210,11 @@ function updateElements(ledType, key) {
case "nanoleaf": case "nanoleaf":
var ledProperties = devicesProperties[ledType][key]; var ledProperties = devicesProperties[ledType][key];
if (ledProperties && ledProperties.panelLayout.layout) { if (ledProperties) {
//Identify non-LED type panels, e.g. Rhythm (1) and Shapes Controller (12) hardwareLedCount = ledProperties.ledCount;
var nonLedNum = 0; canLayout = true;
for (const panel of ledProperties.panelLayout.layout.positionData) {
if (panel.shapeType === 1 || panel.shapeType === 12) {
nonLedNum++;
}
}
hardwareLedCount = ledProperties.panelLayout.layout.numPanels - nonLedNum;
} }
conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount);
break; break;
case "udpraw": case "udpraw":
@ -2225,11 +2263,19 @@ function updateElements(ledType, key) {
} }
if (!conf_editor.validate().length) { if (!conf_editor.validate().length) {
if (canLayout) {
$("#btn_layout_controller").show();
$('#btn_layout_controller').prop('disabled', false);
} else {
$('#btn_layout_controller').hide();
}
if (!window.readOnlyMode) { if (!window.readOnlyMode) {
$('#btn_submit_controller').attr('disabled', false); $('#btn_submit_controller').attr('disabled', false);
} }
} }
else { else {
$('#btn_layout_controller').prop('disabled', true);
$('#btn_submit_controller').attr('disabled', true); $('#btn_submit_controller').attr('disabled', true);
} }
} }
@ -2407,4 +2453,136 @@ function updateElementsWled(ledType, key) {
} }
showInputOptionForItem(conf_editor, "root.specificOptions.segments", "switchOffOtherSegments", showAdditionalOptions); showInputOptionForItem(conf_editor, "root.specificOptions.segments", "switchOffOtherSegments", showAdditionalOptions);
} }
function sortByPanelCoordinates(arr, topToBottom, leftToRight) {
arr.sort((a, b) => {
//Nanoleaf corodinates start at bottom left, therefore reverse topToBottom
if (!topToBottom) {
if (a.y === b.y) {
if (leftToRight) {
return a.x - b.x;
} else {
return b.x - a.x;
}
} else {
return a.y - b.y;
}
}
else {
if (a.y === b.y) {
if (leftToRight) {
return a.x - b.x;
} else {
return b.x - a.x;
}
} else {
return b.y - a.y;
}
}
});
}
function rotateCoordinates(x, y, radians) {
var rotatedX = x * Math.cos(radians) - y * Math.sin(radians);
var rotatedY = x * Math.sin(radians) + y * Math.cos(radians);
return { x: rotatedX, y: rotatedY };
}
function nanoleafGeneratelayout(panelLayout, panelOrderTopDown, panelOrderLeftRight) {
// Dictionary for Nanoleaf shape types
let shapeTypes = {
0: { name: "LightsTriangle", sideLengthX: 150, sideLengthY: 150 },
1: { name: "LightsRythm", sideLengthX: 0, sideLengthY: 0 },
2: { name: "Square", sideLengthX: 100, sideLengthY: 100 },
3: { name: "SquareControllerMaster", sideLengthX: 100, sideLengthY: 100 },
4: { name: "SquareControllerPassive", sideLengthX: 100, sideLengthY: 100 },
5: { name: "PowerSupply", sideLengthX: 100, sideLengthY: 100 },
7: { name: "ShapesHexagon", sideLengthX: 67, sideLengthY: 67 },
8: { name: "ShapesTriangle", sideLengthX: 134, sideLengthY: 134 },
9: { name: "ShapesMiniTriangle", sideLengthX: 67, sideLengthY: 67 },
12: { name: "ShapesController", sideLengthX: 0, sideLengthY: 0 },
14: { name: "ElementsHexagon", sideLengthX: 134, sideLengthY: 134 },
15: { name: "ElementsHexagonCorner", sideLengthX: 33.5, sideLengthY: 58 },
16: { name: "LinesConnector", sideLengthX: 11, sideLengthY: 11 },
17: { name: "LightLines", sideLengthX: 154, sideLengthY: 154 },
18: { name: "LightLinesSingleZone", sideLengthX: 77, sideLengthY: 77 },
19: { name: "ControllerCap", sideLengthX: 11, sideLengthY: 11 },
20: { name: "PowerConnector", sideLengthX: 11, sideLengthY: 11 },
999: { name: "Unknown", sideLengthX: 100, sideLengthY: 100 }
};
let { globalOrientation, layout } = panelLayout;
var positionData = layout.positionData;
var degreesToRotate = 0;
if (globalOrientation) {
degreesToRotate = globalOrientation.value;
}
// Convert degrees to radians
var radians = (degreesToRotate * Math.PI) / 180;
var rotatedCoordinates = rotateCoordinates(positionData[0].x, positionData[0].y, radians);
var minX = rotatedCoordinates.x;
var maxX = rotatedCoordinates.x;
var minY = rotatedCoordinates.y;
var maxY = rotatedCoordinates.y;
//Define capture rectangle per panel
var factorPartial = 0.25;
positionData.forEach(panel => {
if (shapeTypes[panel.shapeType] == undefined) {
panel.shapeType = 999;
}
if (radians !== 0) {
rotatedCoordinates = rotateCoordinates(panel.x, panel.y, radians);
panel.x = rotatedCoordinates.x;
panel.y = rotatedCoordinates.y;
}
panel.maxX = panel.x + shapeTypes[panel.shapeType].sideLengthX * factorPartial;
panel.maxY = panel.y + shapeTypes[panel.shapeType].sideLengthY * factorPartial;
panel.shapeName = shapeTypes[panel.shapeType].name;
if (panel.maxX > maxX) {
maxX = panel.maxX;
}
if (panel.x < minX) {
minX = panel.x;
}
if (panel.maxY > maxY) {
maxY = panel.maxY;
}
if (panel.y < minY) {
minY = panel.y
}
});
const width = (maxX - minX);
const height = (maxY - minY);
const scaleX = 1 / width;
const scaleY = 1 / height;
var layoutObjects = [];
var i = 0;
sortByPanelCoordinates(positionData, panelOrderTopDown, panelOrderLeftRight);
positionData.forEach(panel => {
// Skip non-LED elements
if (panel.sideLengthX !== 0) {
let layoutObject = {
name: i + "-" + panel.panelId,
hmin: (panel.x - minX) * scaleX,
hmax: (panel.maxX - minX) * scaleX,
//Nanoleaf corodinates start at bottom left, therefore reverse vertical positioning
vmin: 1 - ((panel.maxY - minY) * scaleY),
vmax: 1 - ((panel.y - minY) * scaleY)
};
layoutObjects.push(JSON.parse(JSON.stringify(layoutObject)));
++i;
}
});
return layoutObjects;
}

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

@ -21,19 +21,19 @@ using namespace semver;
// Constants // Constants
namespace { namespace {
const char DEFAULT_VERSION[] = "2.0.0-alpha.8"; const char DEFAULT_VERSION[] = "2.0.0-alpha.8";
} //End of constants } //End of constants
QJsonObject SettingsManager::schemaJson; QJsonObject SettingsManager::schemaJson;
SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonlyMode) SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonlyMode)
: QObject(parent) : QObject(parent)
, _log(Logger::getInstance("SETTINGSMGR", "I"+QString::number(instance))) , _log(Logger::getInstance("SETTINGSMGR", "I" + QString::number(instance)))
, _instance(instance) , _instance(instance)
, _sTable(new SettingsTable(instance, this)) , _sTable(new SettingsTable(instance, this))
, _configVersion(DEFAULT_VERSION) , _configVersion(DEFAULT_VERSION)
, _previousVersion(DEFAULT_VERSION) , _previousVersion(DEFAULT_VERSION)
, _readonlyMode(readonlyMode) , _readonlyMode(readonlyMode)
{ {
_sTable->setReadonlyMode(_readonlyMode); _sTable->setReadonlyMode(_readonlyMode);
// get schema // get schema
@ -739,12 +739,12 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config)
{ {
QString type = newDeviceConfig["type"].toString(); QString type = newDeviceConfig["type"].toString();
const QStringList serialDevices {"adalight", "dmx", "atmo", "sedu", "tpm2", "karate"}; const QStringList serialDevices{ "adalight", "dmx", "atmo", "sedu", "tpm2", "karate" };
if ( serialDevices.contains(type )) if (serialDevices.contains(type))
{ {
if (!newDeviceConfig.contains("rateList")) if (!newDeviceConfig.contains("rateList"))
{ {
newDeviceConfig["rateList"] = "CUSTOM"; newDeviceConfig["rateList"] = "CUSTOM";
migrated = true; migrated = true;
} }
} }
@ -791,7 +791,8 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config)
if (newDeviceConfig.contains("type")) if (newDeviceConfig.contains("type"))
{ {
QString type = newDeviceConfig["type"].toString(); QString type = newDeviceConfig["type"].toString();
if ( type == "philipshue")
if (type == "philipshue")
{ {
if (newDeviceConfig.contains("groupId")) if (newDeviceConfig.contains("groupId"))
{ {
@ -805,7 +806,7 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config)
if (newDeviceConfig.contains("lightIds")) if (newDeviceConfig.contains("lightIds"))
{ {
QJsonArray lightIds = newDeviceConfig.value( "lightIds").toArray(); QJsonArray lightIds = newDeviceConfig.value("lightIds").toArray();
// Iterate through the JSON array and update integer values to strings // Iterate through the JSON array and update integer values to strings
for (int i = 0; i < lightIds.size(); ++i) { for (int i = 0; i < lightIds.size(); ++i) {
QJsonValue value = lightIds.at(i); QJsonValue value = lightIds.at(i);
@ -820,6 +821,69 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config)
} }
} }
if (type == "nanoleaf")
{
if (newDeviceConfig.contains("panelStartPos"))
{
newDeviceConfig.remove("panelStartPos");
migrated = true;
}
if (newDeviceConfig.contains("panelOrderTopDown"))
{
QString panelOrderTopDown;
if (newDeviceConfig["panelOrderTopDown"].isDouble())
{
panelOrderTopDown = QString(newDeviceConfig["panelOrderTopDown"].toInt());
}
else
{
panelOrderTopDown = newDeviceConfig["panelOrderTopDown"].toString();
}
if (newDeviceConfig["panelOrderTopDown"] == "0")
{
newDeviceConfig["panelOrderTopDown"] = "top2down";
migrated = true;
}
else
{
if (newDeviceConfig["panelOrderTopDown"] == "1")
{
newDeviceConfig["panelOrderTopDown"] = "bottom2up";
migrated = true;
}
}
}
if (newDeviceConfig.contains("panelOrderLeftRight"))
{
QString panelOrderLeftRight;
if (newDeviceConfig["panelOrderLeftRight"].isDouble())
{
panelOrderLeftRight = QString(newDeviceConfig["panelOrderLeftRight"].toInt());
}
else
{
panelOrderLeftRight = newDeviceConfig["panelOrderLeftRight"].toString();
}
if (newDeviceConfig["panelOrderLeftRight"] == "0")
{
newDeviceConfig["panelOrderLeftRight"] = "left2right";
migrated = true;
}
else
{
if (newDeviceConfig["panelOrderLeftRight"] == "1")
{
newDeviceConfig["panelOrderTopDown"] = "right2left";
migrated = true;
}
}
}
}
} }
if (migrated) if (migrated)

View File

@ -4,6 +4,7 @@
//std includes //std includes
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
#include <cmath>
// Qt includes // Qt includes
#include <QNetworkReply> #include <QNetworkReply>
@ -21,72 +22,74 @@
// Constants // Constants
namespace { namespace {
const bool verbose = false; const bool verbose = false;
const bool verbose3 = false; const bool verbose3 = false;
// Configuration settings // Configuration settings
const char CONFIG_HOST[] = "host"; const char CONFIG_HOST[] = "host";
const char CONFIG_AUTH_TOKEN[] = "token"; const char CONFIG_AUTH_TOKEN[] = "token";
const char CONFIG_RESTORE_STATE[] = "restoreOriginalState"; const char CONFIG_RESTORE_STATE[] = "restoreOriginalState";
const char CONFIG_BRIGHTNESS[] = "brightness"; const char CONFIG_BRIGHTNESS[] = "brightness";
const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness"; 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 bool DEFAULT_IS_RESTORE_STATE = true; const bool DEFAULT_IS_RESTORE_STATE = true;
const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true; const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true;
const int BRI_MAX = 100; const int BRI_MAX = 100;
// Panel configuration settings // Panel configuration settings
const char PANEL_LAYOUT[] = "layout"; const char PANEL_GLOBALORIENTATION[] = "globalOrientation";
const char PANEL_NUM[] = "numPanels"; const char PANEL_GLOBALORIENTATION_VALUE[] = "value";
const char PANEL_ID[] = "panelId"; const char PANEL_LAYOUT[] = "layout";
const char PANEL_POSITIONDATA[] = "positionData"; const char PANEL_NUM[] = "numPanels";
const char PANEL_SHAPE_TYPE[] = "shapeType"; const char PANEL_ID[] = "panelId";
const char PANEL_POS_X[] = "x"; const char PANEL_POSITIONDATA[] = "positionData";
const char PANEL_POS_Y[] = "y"; const char PANEL_SHAPE_TYPE[] = "shapeType";
const char PANEL_POS_X[] = "x";
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_BRI[] = "brightness"; const char STATE_BRI[] = "brightness";
const char STATE_HUE[] = "hue"; const char STATE_HUE[] = "hue";
const char STATE_SAT[] = "sat"; const char STATE_SAT[] = "sat";
const char STATE_CT[] = "ct"; const char STATE_CT[] = "ct";
const char STATE_COLORMODE[] = "colorMode"; const char STATE_COLORMODE[] = "colorMode";
const QStringList COLOR_MODES {"hs", "ct", "effect"}; const QStringList COLOR_MODES{ "hs", "ct", "effect" };
const char STATE_VALUE[] = "value"; const char STATE_VALUE[] = "value";
// Device Data elements // Device Data elements
const char DEV_DATA_NAME[] = "name"; const char DEV_DATA_NAME[] = "name";
const char DEV_DATA_MODEL[] = "model"; const char DEV_DATA_MODEL[] = "model";
const char DEV_DATA_MANUFACTURER[] = "manufacturer"; 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 quint16 STREAM_CONTROL_DEFAULT_PORT = 60222; const quint16 STREAM_CONTROL_DEFAULT_PORT = 60222;
// Nanoleaf OpenAPI URLs // Nanoleaf OpenAPI URLs
const int API_DEFAULT_PORT = 16021; const int API_DEFAULT_PORT = 16021;
const char API_BASE_PATH[] = "/api/v1/%1/"; const char API_BASE_PATH[] = "/api/v1/%1/";
const char API_ROOT[] = ""; const char API_ROOT[] = "";
const char API_EXT_MODE_STRING_V2[] = "{\"write\" : {\"command\" : \"display\", \"animType\" : \"extControl\", \"extControlVersion\" : \"v2\"}}"; const char API_EXT_MODE_STRING_V2[] = "{\"write\" : {\"command\" : \"display\", \"animType\" : \"extControl\", \"extControlVersion\" : \"v2\"}}";
const char API_STATE[] = "state"; 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_IDENTIFY[] = "identify";
const char API_ADD_USER[] = "new";
const char API_EFFECT_SELECT[] = "select";
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;
//Nanoleaf Control data stream // Nanoleaf ssdp services
const int STREAM_FRAME_PANEL_NUM_SIZE = 2; const char SSDP_ID[] = "ssdp:all";
const int STREAM_FRAME_PANEL_INFO_SIZE = 8; const char SSDP_FILTER_HEADER[] = "ST";
const char SSDP_NANOLEAF[] = "nanoleaf:nl*";
// Nanoleaf ssdp services const char SSDP_LIGHTPANELS[] = "nanoleaf_aurora:light";
const char SSDP_ID[] = "ssdp:all";
const char SSDP_FILTER_HEADER[] = "ST";
const char SSDP_NANOLEAF[] = "nanoleaf:nl*";
const char SSDP_LIGHTPANELS[] = "nanoleaf_aurora:light";
} //End of constants } //End of constants
// Nanoleaf Panel Shapetypes // Nanoleaf Panel Shapetypes
@ -96,11 +99,18 @@ enum SHAPETYPES {
SQUARE = 2, SQUARE = 2,
CONTROL_SQUARE_PRIMARY = 3, CONTROL_SQUARE_PRIMARY = 3,
CONTROL_SQUARE_PASSIVE = 4, CONTROL_SQUARE_PASSIVE = 4,
POWER_SUPPLY= 5, POWER_SUPPLY = 5,
HEXAGON_SHAPES = 7, HEXAGON_SHAPES = 7,
TRIANGE_SHAPES = 8, TRIANGE_SHAPES = 8,
MINI_TRIANGE_SHAPES = 8, MINI_TRIANGE_SHAPES = 9,
SHAPES_CONTROLLER = 12 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 // Nanoleaf external control versions
@ -111,18 +121,16 @@ enum EXTCONTROLVERSIONS {
LedDeviceNanoleaf::LedDeviceNanoleaf(const QJsonObject& deviceConfig) LedDeviceNanoleaf::LedDeviceNanoleaf(const QJsonObject& deviceConfig)
: ProviderUdp(deviceConfig) : ProviderUdp(deviceConfig)
, _restApi(nullptr) , _restApi(nullptr)
, _apiPort(API_DEFAULT_PORT) , _apiPort(API_DEFAULT_PORT)
, _topDown(true) , _topDown(true)
, _leftRight(true) , _leftRight(true)
, _startPos(0) , _extControlVersion(EXTCTRLVER_V2)
, _endPos(0) , _panelLedCount(0)
, _extControlVersion(EXTCTRLVER_V2)
, _panelLedCount(0)
{ {
#ifdef ENABLE_MDNS #ifdef ENABLE_MDNS
QMetaObject::invokeMethod(&MdnsBrowser::getInstance(), "browseForServiceType", QMetaObject::invokeMethod(&MdnsBrowser::getInstance(), "browseForServiceType",
Qt::QueuedConnection, Q_ARG(QByteArray, MdnsServiceRegister::getServiceType(_activeDeviceType))); Qt::QueuedConnection, Q_ARG(QByteArray, MdnsServiceRegister::getServiceType(_activeDeviceType)));
#endif #endif
} }
@ -139,7 +147,7 @@ LedDeviceNanoleaf::~LedDeviceNanoleaf()
bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig) bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig)
{ {
bool isInitOK {false}; bool isInitOK{ false };
// Overwrite non supported/required features // Overwrite non supported/required features
setLatchTime(0); setLatchTime(0);
@ -150,9 +158,9 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig)
Info(_log, "Device Nanoleaf does not require rewrites. Refresh time is ignored."); Info(_log, "Device Nanoleaf does not require rewrites. Refresh time is ignored.");
} }
DebugIf(verbose,_log, "deviceConfig: [%s]", QString(QJsonDocument(_devConfig).toJson(QJsonDocument::Compact)).toUtf8().constData()); DebugIf(verbose, _log, "deviceConfig: [%s]", QString(QJsonDocument(_devConfig).toJson(QJsonDocument::Compact)).toUtf8().constData());
if ( ProviderUdp::init(deviceConfig) ) if (ProviderUdp::init(deviceConfig))
{ {
//Set hostname as per configuration and default port //Set hostname as per configuration and default port
_hostName = deviceConfig[CONFIG_HOST].toString(); _hostName = deviceConfig[CONFIG_HOST].toString();
@ -164,36 +172,46 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig)
_isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE); _isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE);
_brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX); _brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX);
Debug(_log, "Hostname/IP : %s", QSTRING_CSTR(_hostName) ); Debug(_log, "Hostname/IP : %s", QSTRING_CSTR(_hostName));
Debug(_log, "RestoreOrigState : %d", _isRestoreOrigState); Debug(_log, "RestoreOrigState : %d", _isRestoreOrigState);
Debug(_log, "Overwrite Brightn.: %d", _isBrightnessOverwrite); Debug(_log, "Overwrite Brightn.: %d", _isBrightnessOverwrite);
Debug(_log, "Set Brightness to : %d", _brightness); Debug(_log, "Set Brightness to : %d", _brightness);
// Read panel organisation configuration // Read panel organisation configuration
if (deviceConfig[CONFIG_PANEL_ORDER_TOP_DOWN].isString()) _topDown = deviceConfig[CONFIG_PANEL_ORDER_TOP_DOWN].toString("top2down") == "top2down";
{ _leftRight = deviceConfig[CONFIG_PANEL_ORDER_LEFT_RIGHT].toString("left2right") == "left2right";
_topDown = deviceConfig[CONFIG_PANEL_ORDER_TOP_DOWN].toString().toInt() == 0;
}
else
{
_topDown = deviceConfig[CONFIG_PANEL_ORDER_TOP_DOWN].toInt() == 0;
}
if (deviceConfig[CONFIG_PANEL_ORDER_LEFT_RIGHT].isString())
{
_leftRight = deviceConfig[CONFIG_PANEL_ORDER_LEFT_RIGHT].toString().toInt() == 0;
}
else
{
_leftRight = deviceConfig[CONFIG_PANEL_ORDER_LEFT_RIGHT].toInt() == 0;
}
_startPos = deviceConfig[CONFIG_PANEL_START_POS].toInt(0);
isInitOK = true; isInitOK = true;
} }
return isInitOK; 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 LedDeviceNanoleaf::initLedsConfiguration()
{ {
bool isInitOK = true; bool isInitOK = true;
@ -206,7 +224,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
if (response.error()) if (response.error())
{ {
QString errorReason = QString("Getting device details failed with error: '%1'").arg(response.getErrorReason()); QString errorReason = QString("Getting device details failed with error: '%1'").arg(response.getErrorReason());
this->setInError ( errorReason ); this->setInError(errorReason);
isInitOK = false; isInitOK = false;
} }
else else
@ -225,24 +243,44 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
// Get panel details from /panelLayout/layout // Get panel details from /panelLayout/layout
QJsonObject jsonPanelLayout = jsonAllPanelInfo[API_PANELLAYOUT].toObject(); QJsonObject jsonPanelLayout = jsonAllPanelInfo[API_PANELLAYOUT].toObject();
const QJsonObject globalOrientation = jsonPanelLayout[PANEL_GLOBALORIENTATION].toObject();
double radians = (globalOrientation[PANEL_GLOBALORIENTATION_VALUE].toInt() * std::acos(-1)) / 180;
QJsonObject jsonLayout = jsonPanelLayout[PANEL_LAYOUT].toObject(); QJsonObject jsonLayout = jsonPanelLayout[PANEL_LAYOUT].toObject();
_panelLedCount = getHwLedCount(jsonLayout);
_devConfig["hardwareLedCount"] = _panelLedCount;
int panelNum = jsonLayout[PANEL_NUM].toInt(); int panelNum = jsonLayout[PANEL_NUM].toInt();
const QJsonArray positionData = jsonLayout[PANEL_POSITIONDATA].toArray(); const QJsonArray positionData = jsonLayout[PANEL_POSITIONDATA].toArray();
std::map<int, std::map<int, int>> panelMap; std::map<int, std::map<int, int>> panelMap;
// Loop over all children. // Loop over all children.
for(const QJsonValue & value : positionData) for (const QJsonValue& value : positionData)
{ {
QJsonObject panelObj = value.toObject(); QJsonObject panelObj = value.toObject();
int panelId = panelObj[PANEL_ID].toInt(); int panelId = panelObj[PANEL_ID].toInt();
int panelX = panelObj[PANEL_POS_X].toInt();
int panelY = panelObj[PANEL_POS_Y].toInt();
int panelshapeType = panelObj[PANEL_SHAPE_TYPE].toInt(); int panelshapeType = panelObj[PANEL_SHAPE_TYPE].toInt();
int posX = panelObj[PANEL_POS_X].toInt();
int posY = panelObj[PANEL_POS_Y].toInt();
DebugIf(verbose,_log, "Panel [%d] (%d,%d) - Type: [%d]", panelId, panelX, panelY, panelshapeType); int panelX;
int panelY;
if (radians != 0)
{
panelX = static_cast<int>(posX * cos(radians) - posY * sin(radians));
panelY = static_cast<int>(posX * sin(radians) + posY * cos(radians));
}
else
{
panelX = posX;
panelY = posY;
}
DebugIf(verbose, _log, "Panel [%d] (%d,%d) - Type: [%d]", panelId, panelX, panelY, panelshapeType);
// Skip Rhythm and Shapes controller panels // Skip Rhythm and Shapes controller panels
if (panelshapeType != RHYTM && panelshapeType != SHAPES_CONTROLLER) if (panelshapeType != RHYTM && panelshapeType != SHAPES_CONTROLLER)
@ -256,6 +294,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
} }
// Travers panels top down // Travers panels top down
_panelIds.clear();
for (auto posY = panelMap.crbegin(); posY != panelMap.crend(); ++posY) for (auto posY = panelMap.crbegin(); posY != panelMap.crend(); ++posY)
{ {
// Sort panels left to right // Sort panels left to right
@ -294,27 +333,19 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
} }
} }
this->_panelLedCount = _panelIds.size();
_devConfig["hardwareLedCount"] = _panelLedCount;
Debug(_log, "PanelsNum : %d", panelNum); Debug(_log, "PanelsNum : %d", panelNum);
Debug(_log, "PanelLedCount : %d", _panelLedCount); Debug(_log, "PanelLedCount : %d", _panelLedCount);
Debug(_log, "Sort Top>Down : %d", _topDown);
Debug(_log, "Sort Left>Right: %d", _leftRight);
// Check. if enough panels were found. // Check. if enough panels were found.
int configuredLedCount = this->getLedCount(); int configuredLedCount = this->getLedCount();
_endPos = _startPos + configuredLedCount - 1;
Debug(_log, "Sort Top>Down : %d", _topDown);
Debug(_log, "Sort Left>Right: %d", _leftRight);
Debug(_log, "Start Panel Pos: %d", _startPos);
Debug(_log, "End Panel Pos : %d", _endPos);
if (_panelLedCount < configuredLedCount) if (_panelLedCount < configuredLedCount)
{ {
QString errorReason = QString("Not enough panels [%1] for configured LEDs [%2] found!") QString errorReason = QString("Not enough panels [%1] for configured LEDs [%2] found!")
.arg(_panelLedCount) .arg(_panelLedCount)
.arg(configuredLedCount); .arg(configuredLedCount);
this->setInError(errorReason); this->setInError(errorReason, false);
isInitOK = false; isInitOK = false;
} }
else else
@ -323,16 +354,6 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
{ {
Info(_log, "%s: More panels [%d] than configured LEDs [%d].", QSTRING_CSTR(this->getActiveDeviceType()), _panelLedCount, configuredLedCount); Info(_log, "%s: More panels [%d] than configured LEDs [%d].", QSTRING_CSTR(this->getActiveDeviceType()), _panelLedCount, configuredLedCount);
} }
// Check, if start position + number of configured LEDs is greater than number of panels available
if (_endPos >= _panelLedCount)
{
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);
isInitOK = false;
}
} }
} }
return isInitOK; return isInitOK;
@ -340,7 +361,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
bool LedDeviceNanoleaf::openRestAPI() bool LedDeviceNanoleaf::openRestAPI()
{ {
bool isInitOK {true}; bool isInitOK{ true };
if (_restApi == nullptr) if (_restApi == nullptr)
{ {
@ -360,7 +381,7 @@ int LedDeviceNanoleaf::open()
if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort))
{ {
if ( openRestAPI() ) if (openRestAPI())
{ {
// Read LedDevice configuration and validate against device configuration // Read LedDevice configuration and validate against device configuration
if (initLedsConfiguration()) if (initLedsConfiguration())
@ -415,7 +436,7 @@ QJsonObject LedDeviceNanoleaf::discover(const QJsonObject& /*params*/)
MdnsServiceRegister::getServiceType(_activeDeviceType), MdnsServiceRegister::getServiceType(_activeDeviceType),
MdnsServiceRegister::getServiceNameFilter(_activeDeviceType), MdnsServiceRegister::getServiceNameFilter(_activeDeviceType),
DEFAULT_DISCOVER_TIMEOUT DEFAULT_DISCOVER_TIMEOUT
); );
#else #else
QString discoveryMethod("ssdp"); QString discoveryMethod("ssdp");
deviceList = discover(); deviceList = discover();
@ -424,25 +445,25 @@ QJsonObject LedDeviceNanoleaf::discover(const QJsonObject& /*params*/)
devicesDiscovered.insert("discoveryMethod", discoveryMethod); devicesDiscovered.insert("discoveryMethod", discoveryMethod);
devicesDiscovered.insert("devices", deviceList); devicesDiscovered.insert("devices", deviceList);
DebugIf(verbose,_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); DebugIf(verbose, _log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData());
return devicesDiscovered; return devicesDiscovered;
} }
QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params) QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params)
{ {
DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());
QJsonObject properties; QJsonObject properties;
_hostName = params[CONFIG_HOST].toString(""); _hostName = params[CONFIG_HOST].toString("");
_apiPort = API_DEFAULT_PORT; _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) ); Info(_log, "Get properties for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName));
if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort))
{ {
if ( openRestAPI() ) if (openRestAPI())
{ {
QString filter = params["filter"].toString(""); QString filter = params["filter"].toString("");
_restApi->setPath(filter); _restApi->setPath(filter);
@ -453,7 +474,14 @@ QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params)
{ {
Warning(_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); 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()); DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData());
@ -463,21 +491,19 @@ QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params)
void LedDeviceNanoleaf::identify(const QJsonObject& params) void LedDeviceNanoleaf::identify(const QJsonObject& params)
{ {
DebugIf(verbose,_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());
_hostName = params[CONFIG_HOST].toString(""); _hostName = params[CONFIG_HOST].toString("");
_apiPort = API_DEFAULT_PORT;if (NetUtils::resolveHostToAddress(_log, _hostName, _address)) _apiPort = API_DEFAULT_PORT;
_authToken = params["token"].toString(""); _authToken = params[CONFIG_AUTH_TOKEN].toString("");
Info(_log, "Identify %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName) ); Info(_log, "Identify %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName));
if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort))
{ {
if ( openRestAPI() ) if (openRestAPI())
{ {
_restApi->setPath("identify"); _restApi->setPath(API_IDENTIFY);
// Perform request
httpResponse response = _restApi->put(); httpResponse response = _restApi->put();
if (response.error()) if (response.error())
{ {
@ -487,6 +513,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 LedDeviceNanoleaf::powerOn()
{ {
bool on = false; bool on = false;
@ -496,12 +552,12 @@ bool LedDeviceNanoleaf::powerOn()
{ {
QJsonObject newState; QJsonObject newState;
QJsonObject onValue { {STATE_VALUE, true} }; QJsonObject onValue{ {STATE_VALUE, true} };
newState.insert(STATE_ON, onValue); newState.insert(STATE_ON, onValue);
if ( _isBrightnessOverwrite) if (_isBrightnessOverwrite)
{ {
QJsonObject briValue { {STATE_VALUE, _brightness} }; QJsonObject briValue{ {STATE_VALUE, _brightness} };
newState.insert(STATE_BRI, briValue); newState.insert(STATE_BRI, briValue);
} }
@ -511,9 +567,10 @@ bool LedDeviceNanoleaf::powerOn()
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 { }
else {
on = true; on = true;
} }
@ -529,7 +586,7 @@ bool LedDeviceNanoleaf::powerOff()
{ {
QJsonObject newState; QJsonObject newState;
QJsonObject onValue { {STATE_VALUE, false} }; QJsonObject onValue{ {STATE_VALUE, false} };
newState.insert(STATE_ON, onValue); newState.insert(STATE_ON, onValue);
//Power-off the Nanoleaf device physically //Power-off the Nanoleaf device physically
@ -538,7 +595,7 @@ bool LedDeviceNanoleaf::powerOff()
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());
this->setInError ( errorReason ); this->setInError(errorReason);
off = false; off = false;
} }
} }
@ -549,12 +606,12 @@ bool LedDeviceNanoleaf::storeState()
{ {
bool rc = true; bool rc = true;
if ( _isRestoreOrigState ) if (_isRestoreOrigState)
{ {
_restApi->setPath(API_STATE); _restApi->setPath(API_STATE);
httpResponse response = _restApi->get(); httpResponse response = _restApi->get();
if ( response.error() ) if (response.error())
{ {
QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason()); QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason());
setInError(errorReason); setInError(errorReason);
@ -563,7 +620,7 @@ bool LedDeviceNanoleaf::storeState()
else else
{ {
_originalStateProperties = response.getBody().object(); _originalStateProperties = response.getBody().object();
DebugIf(verbose, _log, "state: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); DebugIf(verbose, _log, "state: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData());
QJsonObject isOn = _originalStateProperties.value(STATE_ON).toObject(); QJsonObject isOn = _originalStateProperties.value(STATE_ON).toObject();
if (!isOn.isEmpty()) if (!isOn.isEmpty())
@ -579,7 +636,7 @@ bool LedDeviceNanoleaf::storeState()
_originalColorMode = _originalStateProperties[STATE_COLORMODE].toString(); _originalColorMode = _originalStateProperties[STATE_COLORMODE].toString();
switch(COLOR_MODES.indexOf(_originalColorMode)) { switch (COLOR_MODES.indexOf(_originalColorMode)) {
case 0: case 0:
{ {
// hs // hs
@ -611,7 +668,7 @@ bool LedDeviceNanoleaf::storeState()
_restApi->setPath(API_EFFECT); _restApi->setPath(API_EFFECT);
httpResponse responseEffects = _restApi->get(); httpResponse responseEffects = _restApi->get();
if ( responseEffects.error() ) if (responseEffects.error())
{ {
QString errorReason = QString("Storing device state failed with error: '%1'").arg(responseEffects.getErrorReason()); QString errorReason = QString("Storing device state failed with error: '%1'").arg(responseEffects.getErrorReason());
setInError(errorReason); setInError(errorReason);
@ -620,7 +677,7 @@ bool LedDeviceNanoleaf::storeState()
else else
{ {
QJsonObject effects = responseEffects.getBody().object(); QJsonObject effects = responseEffects.getBody().object();
DebugIf(verbose, _log, "effects: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); DebugIf(verbose, _log, "effects: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData());
_originalEffect = effects[API_EFFECT_SELECT].toString(); _originalEffect = effects[API_EFFECT_SELECT].toString();
_originalIsDynEffect = _originalEffect == "*Dynamic*" || _originalEffect == "*Solid*"; _originalIsDynEffect = _originalEffect == "*Dynamic*" || _originalEffect == "*Solid*";
} }
@ -641,21 +698,21 @@ bool LedDeviceNanoleaf::restoreState()
{ {
bool rc = true; bool rc = true;
if ( _isRestoreOrigState ) if (_isRestoreOrigState)
{ {
QJsonObject newState; QJsonObject newState;
switch(COLOR_MODES.indexOf(_originalColorMode)) { switch (COLOR_MODES.indexOf(_originalColorMode)) {
case 0: case 0:
{ // hs { // hs
QJsonObject hueValue { {STATE_VALUE, _originalHue} }; QJsonObject hueValue{ {STATE_VALUE, _originalHue} };
newState.insert(STATE_HUE, hueValue); newState.insert(STATE_HUE, hueValue);
QJsonObject satValue { {STATE_VALUE, _originalSat} }; QJsonObject satValue{ {STATE_VALUE, _originalSat} };
newState.insert(STATE_SAT, satValue); newState.insert(STATE_SAT, satValue);
break; break;
} }
case 1: case 1:
{ // ct { // ct
QJsonObject ctValue { {STATE_VALUE, _originalCt} }; QJsonObject ctValue{ {STATE_VALUE, _originalCt} };
newState.insert(STATE_CT, ctValue); newState.insert(STATE_CT, ctValue);
break; break;
} }
@ -667,37 +724,38 @@ bool LedDeviceNanoleaf::restoreState()
newEffect[API_EFFECT_SELECT] = _originalEffect; newEffect[API_EFFECT_SELECT] = _originalEffect;
_restApi->setPath(API_EFFECT); _restApi->setPath(API_EFFECT);
httpResponse response = _restApi->put(newEffect); httpResponse response = _restApi->put(newEffect);
if ( response.error() ) if (response.error())
{ {
Warning (_log, "%s restoring effect failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); 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. Device is switched off", QSTRING_CSTR(_activeDeviceType)); else {
Warning(_log, "%s restoring effect failed with error: Cannot restore dynamic or solid effect. Device is switched off", QSTRING_CSTR(_activeDeviceType));
_originalIsOn = false; _originalIsOn = false;
} }
break; break;
} }
default: default:
Warning (_log, "%s restoring failed with error: Unknown ColorMode", QSTRING_CSTR(_activeDeviceType)); Warning(_log, "%s restoring failed with error: Unknown ColorMode", QSTRING_CSTR(_activeDeviceType));
rc = false; rc = false;
} }
if (!_originalIsDynEffect) if (!_originalIsDynEffect)
{ {
QJsonObject briValue { {STATE_VALUE, _originalBri} }; QJsonObject briValue{ {STATE_VALUE, _originalBri} };
newState.insert(STATE_BRI, briValue); newState.insert(STATE_BRI, briValue);
} }
QJsonObject onValue { {STATE_VALUE, _originalIsOn} }; QJsonObject onValue{ {STATE_VALUE, _originalIsOn} };
newState.insert(STATE_ON, onValue); newState.insert(STATE_ON, onValue);
_restApi->setPath(API_STATE); _restApi->setPath(API_STATE);
httpResponse response = _restApi->put(newState); httpResponse response = _restApi->put(newState);
if ( response.error() ) if (response.error())
{ {
Warning (_log, "%s restoring state failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); Warning(_log, "%s restoring state failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason()));
rc = false; rc = false;
} }
} }
@ -722,7 +780,7 @@ bool LedDeviceNanoleaf::changeToExternalControlMode(QJsonDocument& resp)
if (response.error()) if (response.error())
{ {
QString errorReason = QString("Change to external control mode failed with error: '%1'").arg(response.getErrorReason()); QString errorReason = QString("Change to external control mode failed with error: '%1'").arg(response.getErrorReason());
this->setInError ( errorReason ); this->setInError(errorReason);
} }
else else
{ {
@ -762,25 +820,14 @@ int LedDeviceNanoleaf::write(const std::vector<ColorRgb>& ledValues)
int ledCounter = 0; int ledCounter = 0;
for (int panelCounter = 0; panelCounter < _panelLedCount; panelCounter++) for (int panelCounter = 0; panelCounter < _panelLedCount; panelCounter++)
{ {
int panelID = _panelIds[panelCounter];
// Set panels configured
if (panelCounter >= _startPos && panelCounter <= _endPos) {
color = static_cast<ColorRgb>(ledValues.at(ledCounter));
++ledCounter;
}
else
{
// Set panels not configured to black
color = ColorRgb::BLACK;
DebugIf(verbose3, _log, "[%d] >= panelLedCount [%d] => Set to BLACK", panelCounter, _panelLedCount);
}
// Set panelID // Set panelID
int panelID = _panelIds[panelCounter];
qToBigEndian<quint16>(static_cast<quint16>(panelID), udpbuffer.data() + i); qToBigEndian<quint16>(static_cast<quint16>(panelID), udpbuffer.data() + i);
i += 2; i += 2;
// Set panel's color LEDs // Set panel's color LEDs
color = static_cast<ColorRgb>(ledValues.at(ledCounter));
++ledCounter;
udpbuffer[i++] = static_cast<char>(color.red); udpbuffer[i++] = static_cast<char>(color.red);
udpbuffer[i++] = static_cast<char>(color.green); udpbuffer[i++] = static_cast<char>(color.green);
udpbuffer[i++] = static_cast<char>(color.blue); udpbuffer[i++] = static_cast<char>(color.blue);
@ -799,7 +846,7 @@ int LedDeviceNanoleaf::write(const std::vector<ColorRgb>& ledValues)
if (verbose3) if (verbose3)
{ {
Debug(_log, "UDP-Address [%s], UDP-Port [%u], udpBufferSize[%d], Bytes to send [%d]", QSTRING_CSTR(_address.toString()), _port, udpBufferSize, i); Debug(_log, "UDP-Address [%s], UDP-Port [%u], udpBufferSize[%d], Bytes to send [%d]", QSTRING_CSTR(_address.toString()), _port, udpBufferSize, i);
Debug( _log, "packet: [%s]", QSTRING_CSTR(toHex(udpbuffer, 64))); Debug(_log, "packet: [%s]", QSTRING_CSTR(toHex(udpbuffer, 64)));
} }
retVal = writeBytes(udpbuffer); retVal = writeBytes(udpbuffer);

View File

@ -87,6 +87,20 @@ public:
/// ///
void identify(const QJsonObject& params) override; 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: protected:
/// ///
@ -182,6 +196,13 @@ private:
/// ///
QJsonArray discover(); 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 ///REST-API wrapper
ProviderRestApi* _restApi; ProviderRestApi* _restApi;
int _apiPort; int _apiPort;
@ -189,8 +210,6 @@ private:
bool _topDown; bool _topDown;
bool _leftRight; bool _leftRight;
int _startPos;
int _endPos;
//Nanoleaf device details //Nanoleaf device details
QString _deviceModel; QString _deviceModel;

View File

@ -318,7 +318,6 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply)
} }
else else
{ {
qDebug() << "httpStatusCode: "<< httpStatusCode;
if (httpStatusCode > 0) { if (httpStatusCode > 0) {
QString httpReason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); QString httpReason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
QString advise; QString advise;
@ -327,7 +326,7 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply)
advise = "Check Request Body"; advise = "Check Request Body";
break; break;
case HttpStatusCode::UnAuthorized: case HttpStatusCode::UnAuthorized:
advise = "Check Authentication Token (API Key)"; advise = "Check Authorization Token (API Key)";
break; break;
case HttpStatusCode::Forbidden: case HttpStatusCode::Forbidden:
advise = "No permission to access the given resource"; advise = "No permission to access the given resource";

View File

@ -72,41 +72,28 @@
"propertyOrder": 7 "propertyOrder": 7
}, },
"panelOrderTopDown": { "panelOrderTopDown": {
"type": "integer", "type": "string",
"title": "edt_dev_spec_order_top_down_title", "title": "edt_dev_spec_order_top_down_title",
"enum": [ 0, 1 ], "enum": [ "top2down", "bottom2up" ],
"default": 0, "default": "top2down",
"required": true, "required": true,
"options": { "options": {
"enum_titles": [ "edt_conf_enum_top_down", "edt_conf_enum_bottom_up" ] "enum_titles": [ "edt_conf_enum_top_down", "edt_conf_enum_bottom_up" ]
}, },
"minimum": 0,
"maximum": 1,
"access": "advanced", "access": "advanced",
"propertyOrder": 8 "propertyOrder": 8
}, },
"panelOrderLeftRight": { "panelOrderLeftRight": {
"type": "integer", "type": "string",
"title": "edt_dev_spec_order_left_right_title", "title": "edt_dev_spec_order_left_right_title",
"enum": [ 0, 1 ], "enum": [ "left2right", "right2left" ],
"default": 0, "default": "left2right",
"required": true, "required": true,
"options": { "options": {
"enum_titles": [ "edt_conf_enum_left_right", "edt_conf_enum_right_left" ] "enum_titles": [ "edt_conf_enum_left_right", "edt_conf_enum_right_left" ]
}, },
"minimum": 0,
"maximum": 1,
"access": "advanced", "access": "advanced",
"propertyOrder": 9 "propertyOrder": 9
},
"panelStartPos": {
"type": "integer",
"title": "edt_dev_spec_panel_start_position",
"step": 1,
"minimum": 0,
"default": 0,
"access": "advanced",
"propertyOrder": 10
} }
}, },
"additionalProperties": true "additionalProperties": true