* Support Philips Hue APIv2 and refactoring

* Fix MDNSBrower - if timeout during host resolvment occurs

* Hue API v2 - Migrate database

* Fix macOS build

* Handle network timeout before any other error

* Address CodeQL findings

* Clean-up and Fixes

* Only getProperties, if username is available

* Option to layout by entertainment area center

* Fix Wizard

* Fix Nanoleaf, add user auth token wizard

* Nanoleaf fixes and enhancements

* Consider rotated panel layouts

* Corrections

* Layout corrections and filter for non LED panels

* Add LED test effect lightening up LEDs in sequence

* Align rotation value to 15 degree steps

* Align rotation value to 15 degree steps

* Skip non LED panels

* Fix Rotation and refactoring

---------

Co-authored-by: Paulchen-Panther <16664240+Paulchen-Panther@users.noreply.github.com>
This commit is contained in:
LordGrey 2023-10-15 17:04:51 +02:00 committed by GitHub
parent cd22d4454d
commit b1e68a3572
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 825 additions and 240 deletions

View File

@ -10,13 +10,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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 orientation
### Changed
### Fixed
- Fixed missing Include limits in QJsonSchemaChecker
- 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

View File

@ -46,6 +46,9 @@
</div>
</div>
<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">
<i class="fa fa-fw fa-save"></i><span data-i18n="wiz_identify">Identify/Test</span>
</button>

View File

@ -118,6 +118,8 @@
"conf_leds_layout_cl_vleddepth": "Vertical LED depth",
"conf_leds_layout_frame": "Classic Layout (LED Frame)",
"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_ma_cabling": "Cabling",
"conf_leds_layout_ma_direction": "Direction",
@ -566,8 +568,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",
@ -758,6 +760,8 @@
"edt_eff_ledlist": "LED List",
"edt_eff_ledtest_header": "LED Test",
"edt_eff_ledtest_header_desc": "Rotating output: Red, Green, Blue, White, Black",
"edt_eff_ledtest_seq_header": "LED Test - Sequence",
"edt_eff_ledtest_seq_header_desc": "Light up the LEDs in sequence",
"edt_eff_length": "Length",
"edt_eff_lightclock_header": "Light Clock",
"edt_eff_lightclock_header_desc": "A real clock as light! Adjust the colors of hours, minute, seconds. A optional 3/6/9/12 o'clock marker is also available. In case the clock is wrong, you need to check your system clock.",
@ -1121,8 +1125,15 @@
"wiz_identify": "Identify",
"wiz_identify_tip": "Identify configured device by lighting it up",
"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_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

@ -1050,7 +1050,7 @@ $(document).ready(function () {
// 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);
// led controller sepecific wizards
// LED controller specific wizards
$('#btn_wiz_holder').html("");
$('#btn_led_device_wiz').off();
@ -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 };
@ -1084,6 +1091,7 @@ $(document).ready(function () {
var colorOrderDefault = "rgb";
var filter = {};
$('#btn_layout_controller').hide();
$('#btn_test_controller').hide();
switch (ledType) {
@ -1341,6 +1349,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 +1367,8 @@ $(document).ready(function () {
break;
case "nanoleaf":
$('#btn_wiz_holder').show();
var token = conf_editor.getEditor("root.specificOptions.token").getValue();
if (token === "") {
return;
@ -1668,6 +1685,33 @@ $(document).ready(function () {
$("#leddevices").val(window.serverConfig.device.type);
$("#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
$("#btn_test_controller").off().on("click", function () {
var ledType = $("#leddevices").val();
@ -2147,6 +2191,7 @@ async function identify_device(type, params) {
}
function updateElements(ledType, key) {
var canLayout = false;
if (devicesProperties[ledType][key]) {
var hardwareLedCount = 1;
switch (ledType) {
@ -2165,18 +2210,11 @@ 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;
canLayout = true;
}
conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount);
break;
case "udpraw":
@ -2225,11 +2263,19 @@ function updateElements(ledType, key) {
}
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) {
$('#btn_submit_controller').attr('disabled', false);
}
}
else {
$('#btn_layout_controller').prop('disabled', true);
$('#btn_submit_controller').attr('disabled', true);
}
}
@ -2407,4 +2453,149 @@ function updateElementsWled(ledType, key) {
}
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", led: true, sideLengthX: 150, sideLengthY: 150 },
1: { name: "LightsRythm", led: false, sideLengthX: 0, sideLengthY: 0 },
2: { name: "Square", led: true, sideLengthX: 100, sideLengthY: 100 },
3: { name: "SquareControllerMaster", led: true, sideLengthX: 100, sideLengthY: 100 },
4: { name: "SquareControllerPassive", led: true, sideLengthX: 100, sideLengthY: 100 },
5: { name: "PowerSupply", led: true, sideLengthX: 100, sideLengthY: 100 },
7: { name: "ShapesHexagon", led: true, sideLengthX: 67, sideLengthY: 67 },
8: { name: "ShapesTriangle", led: true, sideLengthX: 134, sideLengthY: 134 },
9: { name: "ShapesMiniTriangle", led: true, sideLengthX: 67, sideLengthY: 67 },
12: { name: "ShapesController", led: false, sideLengthX: 0, sideLengthY: 0 },
14: { name: "ElementsHexagon", led: true, sideLengthX: 134, sideLengthY: 134 },
15: { name: "ElementsHexagonCorner", led: true, sideLengthX: 33.5, sideLengthY: 58 },
16: { name: "LinesConnector", led: false, sideLengthX: 11, sideLengthY: 11 },
17: { name: "LightLines", led: true, sideLengthX: 154, sideLengthY: 154 },
18: { name: "LightLinesSingleZone", led: true, sideLengthX: 77, sideLengthY: 77 },
19: { name: "ControllerCap", led: false, sideLengthX: 11, sideLengthY: 11 },
20: { name: "PowerConnector", led: false, sideLengthX: 11, sideLengthY: 11 },
999: { name: "Unknown", led: true, sideLengthX: 100, sideLengthY: 100 }
};
let { globalOrientation, layout } = panelLayout;
var degreesToRotate = 0;
if (globalOrientation) {
degreesToRotate = globalOrientation.value;
}
//Align rotation degree to 15 degree steps
const degreeSteps = 15;
var degreeRounded = ((Math.round(degreesToRotate / degreeSteps) * degreeSteps) + 360) % 360;
//Nanoleaf orientation is counter-clockwise
degreeRounded *= -1;
// Convert degrees to radians
const radians = (degreeRounded * Math.PI) / 180;
//Reduce the capture area
const areaSizeFactor = 0.5;
var panelDataXY = [...layout.positionData];
panelDataXY.forEach(panel => {
if (shapeTypes[panel.shapeType] == undefined) {
panel.shapeType = 999;
}
panel.shapeName = shapeTypes[panel.shapeType].name;
panel.led = shapeTypes[panel.shapeType].led;
panel.areaWidth = shapeTypes[panel.shapeType].sideLengthX * areaSizeFactor;
panel.areaHeight = shapeTypes[panel.shapeType].sideLengthY * areaSizeFactor;
if (radians !== 0) {
var rotatedXY = rotateCoordinates(panel.x, panel.y, radians);
panel.x = Math.round(rotatedXY.x);
panel.y = Math.round(rotatedXY.y);
}
panel.maxX = panel.x + panel.areaWidth;
panel.maxY = panel.y + panel.areaHeight;
});
var minX = panelDataXY[0].x;
var maxX = panelDataXY[0].x;
var minY = panelDataXY[0].y;
var maxY = panelDataXY[0].y;
panelDataXY.forEach(panel => {
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 = Math.abs(maxX - minX);
const height = Math.abs(maxY - minY);
const scaleX = 1 / width;
const scaleY = 1 / height;
var layoutObjects = [];
var i = 0;
sortByPanelCoordinates(panelDataXY, panelOrderTopDown, panelOrderLeftRight);
panelDataXY.forEach(panel => {
if (panel.led) {
let layoutObject = {
name: i + "-" + panel.panelId,
hmin: Math.min(1, Math.max(0, (panel.x - minX) * scaleX)),
hmax: Math.min(1, Math.max(0, (panel.x - minX + panel.areaWidth) * scaleX)),
//Nanoleaf corodinates start at bottom left, therefore reverse vertical positioning
vmax: (1 - Math.min(1, Math.max(0, (panel.y - minY) * scaleY))),
vmin: (1 - Math.min(1, Math.max(0, (panel.y - minY + panel.areaHeight) * 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);
}

11
effects/ledtest-seq.json Normal file
View File

@ -0,0 +1,11 @@
{
"name" : "Led Test - Sequence",
"script" : "ledtest-seq.py",
"args" :
{
"sleepTime" : 0.5,
"smoothing-custom-settings" : false,
"smoothing-time_ms" : 500,
"smoothing-updateFrequency" : 20.0
}
}

39
effects/ledtest-seq.py Normal file
View File

@ -0,0 +1,39 @@
import hyperion
import time
# Get parameters
sleepTime = float(hyperion.args.get('sleepTime', 0.5))
def TestRgb( iteration ):
switcher = {
0: (255, 0, 0),
1: (0, 255, 0),
2: (0, 0, 255),
}
return switcher.get(iteration, (127,127,127) )
ledData = bytearray(hyperion.ledCount * (0,0,0) )
i = 0
while not hyperion.abort():
if i < hyperion.ledCount:
j = i % 3
rgb = TestRgb( j )
ledData[3*i+0] = rgb[0]
ledData[3*i+1] = rgb[1]
ledData[3*i+2] = rgb[2]
i += 1
else:
if i == hyperion.ledCount:
ledData = bytearray(hyperion.ledCount * (0,0,0) )
i += 1
else:
i = 0
hyperion.setColor (ledData)
time.sleep(sleepTime)

View File

@ -0,0 +1,56 @@
{
"type":"object",
"script" : "ledtest-seq.py",
"title":"edt_eff_ledtest_seq_header",
"required":true,
"properties":{
"sleepTime": {
"type": "number",
"title":"edt_eff_sleeptime",
"default": 0.5,
"minimum" : 0.01,
"maximum": 1,
"step": 0.01,
"append" : "edt_append_s",
"propertyOrder" : 1
},
"smoothing-custom-settings" :
{
"type" : "boolean",
"title" : "edt_eff_smooth_custom",
"default" : false,
"propertyOrder" : 2
},
"smoothing-time_ms" :
{
"type" : "integer",
"title" : "edt_eff_smooth_time_ms",
"minimum" : 25,
"maximum": 600,
"default" : 200,
"append" : "edt_append_ms",
"options": {
"dependencies": {
"smoothing-custom-settings": true
}
},
"propertyOrder" : 3
},
"smoothing-updateFrequency" :
{
"type" : "number",
"title" : "edt_eff_smooth_updateFrequency",
"minimum" : 1.0,
"maximum" : 100.0,
"default" : 25.0,
"append" : "edt_append_hz",
"options": {
"dependencies": {
"smoothing-custom-settings": true
}
},
"propertyOrder" : 4
}
},
"additionalProperties": false
}

View File

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

View File

@ -4,6 +4,7 @@
//std includes
#include <sstream>
#include <iomanip>
#include <cmath>
// Qt includes
#include <QNetworkReply>
@ -21,88 +22,79 @@
// Constants
namespace {
const bool verbose = false;
const bool verbose3 = false;
const bool verbose = false;
const bool verbose3 = false;
// Configuration settings
const char CONFIG_HOST[] = "host";
const char CONFIG_AUTH_TOKEN[] = "token";
const char CONFIG_RESTORE_STATE[] = "restoreOriginalState";
const char CONFIG_BRIGHTNESS[] = "brightness";
const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness";
// Configuration settings
const char CONFIG_HOST[] = "host";
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 char CONFIG_PANEL_ORDER_TOP_DOWN[] = "panelOrderTopDown";
const char CONFIG_PANEL_ORDER_LEFT_RIGHT[] = "panelOrderLeftRight";
const bool DEFAULT_IS_RESTORE_STATE = true;
const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true;
const int BRI_MAX = 100;
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";
const char PANEL_ID[] = "panelId";
const char PANEL_POSITIONDATA[] = "positionData";
const char PANEL_SHAPE_TYPE[] = "shapeType";
const char PANEL_POS_X[] = "x";
const char PANEL_POS_Y[] = "y";
// Panel configuration settings
const char PANEL_GLOBALORIENTATION[] = "globalOrientation";
const char PANEL_GLOBALORIENTATION_VALUE[] = "value";
const char PANEL_LAYOUT[] = "layout";
const char PANEL_NUM[] = "numPanels";
const char PANEL_ID[] = "panelId";
const char PANEL_POSITIONDATA[] = "positionData";
const char PANEL_SHAPE_TYPE[] = "shapeType";
const char PANEL_POS_X[] = "x";
const char PANEL_POS_Y[] = "y";
// List of State Information
const char STATE_ON[] = "on";
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";
// List of State Information
const char STATE_ON[] = "on";
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";
const char DEV_DATA_MODEL[] = "model";
const char DEV_DATA_MANUFACTURER[] = "manufacturer";
const char DEV_DATA_FIRMWAREVERSION[] = "firmwareVersion";
// Device Data elements
const char DEV_DATA_NAME[] = "name";
const char DEV_DATA_MODEL[] = "model";
const char DEV_DATA_MANUFACTURER[] = "manufacturer";
const char DEV_DATA_FIRMWAREVERSION[] = "firmwareVersion";
// Nanoleaf Stream Control elements
const quint16 STREAM_CONTROL_DEFAULT_PORT = 60222;
// Nanoleaf Stream Control elements
const quint16 STREAM_CONTROL_DEFAULT_PORT = 60222;
// Nanoleaf OpenAPI URLs
const int API_DEFAULT_PORT = 16021;
const char API_BASE_PATH[] = "/api/v1/%1/";
const char API_ROOT[] = "";
const char API_EXT_MODE_STRING_V2[] = "{\"write\" : {\"command\" : \"display\", \"animType\" : \"extControl\", \"extControlVersion\" : \"v2\"}}";
const char API_STATE[] = "state";
const char API_PANELLAYOUT[] = "panelLayout";
const char API_EFFECT[] = "effects";
// Nanoleaf OpenAPI URLs
const int API_DEFAULT_PORT = 16021;
const char API_BASE_PATH[] = "/api/v1/%1/";
const char API_ROOT[] = "";
const char API_EXT_MODE_STRING_V2[] = "{\"write\" : {\"command\" : \"display\", \"animType\" : \"extControl\", \"extControlVersion\" : \"v2\"}}";
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";
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
const int STREAM_FRAME_PANEL_NUM_SIZE = 2;
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_NANOLEAF[] = "nanoleaf:nl*";
const char SSDP_LIGHTPANELS[] = "nanoleaf_aurora:light";
const double ROTATION_STEPS_DEGREE = 15.0;
// Nanoleaf ssdp services
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
// Nanoleaf Panel Shapetypes
enum SHAPETYPES {
TRIANGLE = 0,
RHYTM = 1,
SQUARE = 2,
CONTROL_SQUARE_PRIMARY = 3,
CONTROL_SQUARE_PASSIVE = 4,
POWER_SUPPLY= 5,
HEXAGON_SHAPES = 7,
TRIANGE_SHAPES = 8,
MINI_TRIANGE_SHAPES = 8,
SHAPES_CONTROLLER = 12
};
// Nanoleaf external control versions
enum EXTCONTROLVERSIONS {
EXTCTRLVER_V1 = 1,
@ -111,18 +103,16 @@ enum EXTCONTROLVERSIONS {
LedDeviceNanoleaf::LedDeviceNanoleaf(const QJsonObject& deviceConfig)
: ProviderUdp(deviceConfig)
, _restApi(nullptr)
, _apiPort(API_DEFAULT_PORT)
, _topDown(true)
, _leftRight(true)
, _startPos(0)
, _endPos(0)
, _extControlVersion(EXTCTRLVER_V2)
, _panelLedCount(0)
, _restApi(nullptr)
, _apiPort(API_DEFAULT_PORT)
, _topDown(true)
, _leftRight(true)
, _extControlVersion(EXTCTRLVER_V2)
, _panelLedCount(0)
{
#ifdef ENABLE_MDNS
QMetaObject::invokeMethod(&MdnsBrowser::getInstance(), "browseForServiceType",
Qt::QueuedConnection, Q_ARG(QByteArray, MdnsServiceRegister::getServiceType(_activeDeviceType)));
Qt::QueuedConnection, Q_ARG(QByteArray, MdnsServiceRegister::getServiceType(_activeDeviceType)));
#endif
}
@ -139,7 +129,7 @@ LedDeviceNanoleaf::~LedDeviceNanoleaf()
bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig)
{
bool isInitOK {false};
bool isInitOK{ false };
// Overwrite non supported/required features
setLatchTime(0);
@ -150,9 +140,9 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig)
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
_hostName = deviceConfig[CONFIG_HOST].toString();
@ -164,36 +154,66 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig)
_isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE);
_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, "Overwrite Brightn.: %d", _isBrightnessOverwrite);
Debug(_log, "Set Brightness to : %d", _brightness);
// Read panel organisation configuration
if (deviceConfig[CONFIG_PANEL_ORDER_TOP_DOWN].isString())
{
_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);
_topDown = deviceConfig[CONFIG_PANEL_ORDER_TOP_DOWN].toString("top2down") == "top2down";
_leftRight = deviceConfig[CONFIG_PANEL_ORDER_LEFT_RIGHT].toString("left2right") == "left2right";
isInitOK = true;
}
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);
if (hasLEDs(static_cast<SHAPETYPES>(panelshapeType)))
{
++hwLedCount;
}
else
{
DebugIf(verbose, _log, "Rhythm/Shape/Lines Controller panel skipped.");
}
}
return hwLedCount;
}
bool LedDeviceNanoleaf::hasLEDs(const SHAPETYPES& panelshapeType) const
{
bool hasLED {true};
// Skip non LED panel types
switch (panelshapeType)
{
case SHAPES_CONTROLLER:
case LINES_CONECTOR:
case CONTROLLER_CAP:
case POWER_CONNECTOR:
case RHYTM:
DebugIf(verbose, _log, "Rhythm/Shape/Lines Controller panel skipped.");
hasLED = false;
break;
default:
break;
}
return hasLED;
}
bool LedDeviceNanoleaf::initLedsConfiguration()
{
bool isInitOK = true;
@ -206,7 +226,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
if (response.error())
{
QString errorReason = QString("Getting device details failed with error: '%1'").arg(response.getErrorReason());
this->setInError ( errorReason );
this->setInError(errorReason);
isInitOK = false;
}
else
@ -225,37 +245,71 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
// Get panel details from /panelLayout/layout
QJsonObject jsonPanelLayout = jsonAllPanelInfo[API_PANELLAYOUT].toObject();
const QJsonObject globalOrientation = jsonPanelLayout[PANEL_GLOBALORIENTATION].toObject();
int orientation = globalOrientation[PANEL_GLOBALORIENTATION_VALUE].toInt();
int degreesToRotate {orientation};
bool isRotated {false};
if (degreesToRotate > 0)
{
isRotated = true;
int degreeRounded = static_cast<int>(round(degreesToRotate / ROTATION_STEPS_DEGREE) * ROTATION_STEPS_DEGREE);
degreesToRotate = (degreeRounded +360) % 360;
}
//Nanoleaf orientation is counter-clockwise
degreesToRotate *= -1;
double radians = (degreesToRotate * std::acos(-1)) / 180;
DebugIf(verbose, _log, "globalOrientation: %d, degreesToRotate: %d, radians: %0.2f", orientation, degreesToRotate, radians);
QJsonObject jsonLayout = jsonPanelLayout[PANEL_LAYOUT].toObject();
_panelLedCount = getHwLedCount(jsonLayout);
_devConfig["hardwareLedCount"] = _panelLedCount;
int panelNum = jsonLayout[PANEL_NUM].toInt();
const QJsonArray positionData = jsonLayout[PANEL_POSITIONDATA].toArray();
std::map<int, std::map<int, int>> panelMap;
// Loop over all children.
for(const QJsonValue & value : positionData)
for (const QJsonValue& value : positionData)
{
QJsonObject panelObj = value.toObject();
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 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);
// Skip Rhythm and Shapes controller panels
if (panelshapeType != RHYTM && panelshapeType != SHAPES_CONTROLLER)
int panelX;
int panelY;
if (isRotated)
{
panelMap[panelY][panelX] = panelId;
panelX = static_cast<int>(round(posX * cos(radians) - posY * sin(radians)));
panelY = static_cast<int>(round(posX * sin(radians) + posY * cos(radians)));
}
else
{ // Reset non support/required features
Info(_log, "Rhythm/Shape Controller panel skipped.");
{
panelX = posX;
panelY = posY;
}
if (hasLEDs(static_cast<SHAPETYPES>(panelshapeType)))
{
panelMap[panelY][panelX] = panelId;
DebugIf(verbose, _log, "Use Panel [%d] (%d,%d) - Type: [%d]", panelId, panelX, panelY, panelshapeType);
}
else
{
DebugIf(verbose, _log, "Skip Panel [%d] (%d,%d) - Type: [%d]", panelId, panelX, panelY, panelshapeType);
}
}
// Travers panels top down
_panelIds.clear();
for (auto posY = panelMap.crbegin(); posY != panelMap.crend(); ++posY)
{
// Sort panels left to right
@ -263,7 +317,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
{
for (auto posX = posY->second.cbegin(); posX != posY->second.cend(); ++posX)
{
DebugIf(verbose3, _log, "panelMap[%d][%d]=%d", posY->first, posX->first, posX->second);
DebugIf(verbose, _log, "panelMap[%d][%d]=%d", posY->first, posX->first, posX->second);
if (_topDown)
{
@ -280,7 +334,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
// Sort panels right to left
for (auto posX = posY->second.crbegin(); posX != posY->second.crend(); ++posX)
{
DebugIf(verbose3, _log, "panelMap[%d][%d]=%d", posY->first, posX->first, posX->second);
DebugIf(verbose, _log, "panelMap[%d][%d]=%d", posY->first, posX->first, posX->second);
if (_topDown)
{
@ -294,27 +348,22 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
}
}
this->_panelLedCount = _panelIds.size();
_devConfig["hardwareLedCount"] = _panelLedCount;
Debug(_log, "PanelsNum : %d", panelNum);
Debug(_log, "PanelLedCount : %d", _panelLedCount);
Debug(_log, "Sort Top>Down : %d", _topDown);
Debug(_log, "Sort Left>Right: %d", _leftRight);
DebugIf(verbose, _log, "PanelMap size : %d", panelMap.size());
DebugIf(verbose, _log, "PanelIds count : %d", _panelIds.size());
// Check. if enough panels were found.
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)
{
QString errorReason = QString("Not enough panels [%1] for configured LEDs [%2] found!")
.arg(_panelLedCount)
.arg(configuredLedCount);
this->setInError(errorReason);
.arg(_panelLedCount)
.arg(configuredLedCount);
this->setInError(errorReason, false);
isInitOK = false;
}
else
@ -324,15 +373,16 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
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)
//Check that panel count matches working list created for processing
if (_panelLedCount != _panelIds.size())
{
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);
QString errorReason = QString("Number of available panels [%1] do not match panel-ID look-up list [%2]!")
.arg(_panelLedCount)
.arg(_panelIds.size());
this->setInError(errorReason, false);
isInitOK = false;
}
}
}
return isInitOK;
@ -340,7 +390,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
bool LedDeviceNanoleaf::openRestAPI()
{
bool isInitOK {true};
bool isInitOK{ true };
if (_restApi == nullptr)
{
@ -360,7 +410,7 @@ int LedDeviceNanoleaf::open()
if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort))
{
if ( openRestAPI() )
if (openRestAPI())
{
// Read LedDevice configuration and validate against device configuration
if (initLedsConfiguration())
@ -415,7 +465,7 @@ QJsonObject LedDeviceNanoleaf::discover(const QJsonObject& /*params*/)
MdnsServiceRegister::getServiceType(_activeDeviceType),
MdnsServiceRegister::getServiceNameFilter(_activeDeviceType),
DEFAULT_DISCOVER_TIMEOUT
);
);
#else
QString discoveryMethod("ssdp");
deviceList = discover();
@ -424,25 +474,25 @@ QJsonObject LedDeviceNanoleaf::discover(const QJsonObject& /*params*/)
devicesDiscovered.insert("discoveryMethod", discoveryMethod);
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;
}
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;
_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) );
Info(_log, "Get properties for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName));
if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort))
{
if ( openRestAPI() )
if (openRestAPI())
{
QString filter = params["filter"].toString("");
_restApi->setPath(filter);
@ -453,7 +503,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());
@ -463,21 +520,19 @@ QJsonObject LedDeviceNanoleaf::getProperties(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("");
_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) );
Info(_log, "Identify %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName));
if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort))
{
if ( openRestAPI() )
if (openRestAPI())
{
_restApi->setPath("identify");
// Perform request
_restApi->setPath(API_IDENTIFY);
httpResponse response = _restApi->put();
if (response.error())
{
@ -487,6 +542,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;
@ -496,12 +581,12 @@ bool LedDeviceNanoleaf::powerOn()
{
QJsonObject newState;
QJsonObject onValue { {STATE_VALUE, true} };
QJsonObject onValue{ {STATE_VALUE, true} };
newState.insert(STATE_ON, onValue);
if ( _isBrightnessOverwrite)
if (_isBrightnessOverwrite)
{
QJsonObject briValue { {STATE_VALUE, _brightness} };
QJsonObject briValue{ {STATE_VALUE, _brightness} };
newState.insert(STATE_BRI, briValue);
}
@ -511,9 +596,10 @@ bool LedDeviceNanoleaf::powerOn()
if (response.error())
{
QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason());
this->setInError ( errorReason );
this->setInError(errorReason);
on = false;
} else {
}
else {
on = true;
}
@ -529,7 +615,7 @@ bool LedDeviceNanoleaf::powerOff()
{
QJsonObject newState;
QJsonObject onValue { {STATE_VALUE, false} };
QJsonObject onValue{ {STATE_VALUE, false} };
newState.insert(STATE_ON, onValue);
//Power-off the Nanoleaf device physically
@ -538,7 +624,7 @@ bool LedDeviceNanoleaf::powerOff()
if (response.error())
{
QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason());
this->setInError ( errorReason );
this->setInError(errorReason);
off = false;
}
}
@ -549,12 +635,12 @@ bool LedDeviceNanoleaf::storeState()
{
bool rc = true;
if ( _isRestoreOrigState )
if (_isRestoreOrigState)
{
_restApi->setPath(API_STATE);
httpResponse response = _restApi->get();
if ( response.error() )
if (response.error())
{
QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason());
setInError(errorReason);
@ -563,7 +649,7 @@ bool LedDeviceNanoleaf::storeState()
else
{
_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();
if (!isOn.isEmpty())
@ -579,7 +665,7 @@ bool LedDeviceNanoleaf::storeState()
_originalColorMode = _originalStateProperties[STATE_COLORMODE].toString();
switch(COLOR_MODES.indexOf(_originalColorMode)) {
switch (COLOR_MODES.indexOf(_originalColorMode)) {
case 0:
{
// hs
@ -611,7 +697,7 @@ bool LedDeviceNanoleaf::storeState()
_restApi->setPath(API_EFFECT);
httpResponse responseEffects = _restApi->get();
if ( responseEffects.error() )
if (responseEffects.error())
{
QString errorReason = QString("Storing device state failed with error: '%1'").arg(responseEffects.getErrorReason());
setInError(errorReason);
@ -620,7 +706,7 @@ bool LedDeviceNanoleaf::storeState()
else
{
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();
_originalIsDynEffect = _originalEffect == "*Dynamic*" || _originalEffect == "*Solid*";
}
@ -641,21 +727,21 @@ bool LedDeviceNanoleaf::restoreState()
{
bool rc = true;
if ( _isRestoreOrigState )
if (_isRestoreOrigState)
{
QJsonObject newState;
switch(COLOR_MODES.indexOf(_originalColorMode)) {
switch (COLOR_MODES.indexOf(_originalColorMode)) {
case 0:
{ // hs
QJsonObject hueValue { {STATE_VALUE, _originalHue} };
QJsonObject hueValue{ {STATE_VALUE, _originalHue} };
newState.insert(STATE_HUE, hueValue);
QJsonObject satValue { {STATE_VALUE, _originalSat} };
QJsonObject satValue{ {STATE_VALUE, _originalSat} };
newState.insert(STATE_SAT, satValue);
break;
}
case 1:
{ // ct
QJsonObject ctValue { {STATE_VALUE, _originalCt} };
QJsonObject ctValue{ {STATE_VALUE, _originalCt} };
newState.insert(STATE_CT, ctValue);
break;
}
@ -667,37 +753,38 @@ bool LedDeviceNanoleaf::restoreState()
newEffect[API_EFFECT_SELECT] = _originalEffect;
_restApi->setPath(API_EFFECT);
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;
}
break;
}
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;
}
if (!_originalIsDynEffect)
{
QJsonObject briValue { {STATE_VALUE, _originalBri} };
QJsonObject briValue{ {STATE_VALUE, _originalBri} };
newState.insert(STATE_BRI, briValue);
}
QJsonObject onValue { {STATE_VALUE, _originalIsOn} };
QJsonObject onValue{ {STATE_VALUE, _originalIsOn} };
newState.insert(STATE_ON, onValue);
_restApi->setPath(API_STATE);
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;
}
}
@ -722,7 +809,7 @@ bool LedDeviceNanoleaf::changeToExternalControlMode(QJsonDocument& resp)
if (response.error())
{
QString errorReason = QString("Change to external control mode failed with error: '%1'").arg(response.getErrorReason());
this->setInError ( errorReason );
this->setInError(errorReason);
}
else
{
@ -758,29 +845,24 @@ int LedDeviceNanoleaf::write(const std::vector<ColorRgb>& ledValues)
ColorRgb color;
//Maintain LED counter independent from PanelCounter
int ledCounter = 0;
for (int panelCounter = 0; panelCounter < _panelLedCount; panelCounter++)
for (int panelCounter = 0; panelCounter < _panelLedCount; ++panelCounter)
{
// Set panelID
int panelID = _panelIds[panelCounter];
qToBigEndian<quint16>(static_cast<quint16>(panelID), udpbuffer.data() + i);
i += 2;
// Set panels configured
if (panelCounter >= _startPos && panelCounter <= _endPos) {
color = static_cast<ColorRgb>(ledValues.at(ledCounter));
++ledCounter;
// Set panel's color LEDs
if (panelCounter < this->getLedCount()) {
color = static_cast<ColorRgb>(ledValues.at(panelCounter));
}
else
{
// Set panels not configured to black
color = ColorRgb::BLACK;
DebugIf(verbose3, _log, "[%d] >= panelLedCount [%d] => Set to BLACK", panelCounter, _panelLedCount);
DebugIf(verbose3, _log, "[%u] >= panelLedCount [%u] => Set to BLACK", panelCounter, _panelLedCount);
}
// Set panelID
qToBigEndian<quint16>(static_cast<quint16>(panelID), udpbuffer.data() + i);
i += 2;
// Set panel's color LEDs
udpbuffer[i++] = static_cast<char>(color.red);
udpbuffer[i++] = static_cast<char>(color.green);
udpbuffer[i++] = static_cast<char>(color.blue);
@ -799,7 +881,7 @@ int LedDeviceNanoleaf::write(const std::vector<ColorRgb>& ledValues)
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, "packet: [%s]", QSTRING_CSTR(toHex(udpbuffer, 64)));
Debug(_log, "packet: [%s]", QSTRING_CSTR(toHex(udpbuffer, 64)));
}
retVal = writeBytes(udpbuffer);

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:
///
@ -147,6 +161,27 @@ protected:
private:
// Nanoleaf Panel Shapetypes
enum SHAPETYPES {
TRIANGLE = 0,
RHYTM = 1,
SQUARE = 2,
CONTROL_SQUARE_PRIMARY = 3,
CONTROL_SQUARE_PASSIVE = 4,
POWER_SUPPLY = 5,
HEXAGON_SHAPES = 7,
TRIANGE_SHAPES = 8,
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
};
///
/// @brief Initialise the access to the REST-API wrapper
///
@ -182,6 +217,20 @@ 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;
///
/// @brief Check, if panelshape type has LEDs
///
/// @return True, if panel shape type has LEDs
///
bool hasLEDs(const SHAPETYPES& panelshapeType) const;
///REST-API wrapper
ProviderRestApi* _restApi;
int _apiPort;
@ -189,8 +238,6 @@ private:
bool _topDown;
bool _leftRight;
int _startPos;
int _endPos;
//Nanoleaf device details
QString _deviceModel;

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

View File

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