mirror of
https://github.com/hyperion-project/hyperion.ng.git
synced 2025-03-01 10:33:28 +00:00
Nanoleaf (#1647)
* 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:
@@ -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>
|
||||
|
@@ -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.",
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user