Merge remote-tracking branch 'origin/master' into updates

This commit is contained in:
LordGrey 2023-10-15 18:31:00 +02:00
commit b7945d4aa7
30 changed files with 3421 additions and 1175 deletions

View File

@ -36,7 +36,7 @@ jobs:
name: Setup APT build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set APT matrix
id: apt-ppa
run: |
@ -54,7 +54,7 @@ jobs:
matrix: ${{ fromJson(needs.setup.outputs.apt-matrix) }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }}
submodules: true
@ -107,12 +107,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.head_sha || github.event.client_payload.head_sha }}
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v5.3.0
uses: crazy-max/ghaction-import-gpg@v6.0.0
with:
gpg_private_key: ${{ secrets.APT_GPG }}

View File

@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: recursive

View File

@ -13,7 +13,7 @@ jobs:
if: github.repository_owner == 'hyperion-project'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0
@ -48,7 +48,7 @@ jobs:
if: github.repository_owner == 'hyperion-project'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Check if commit has changed
id: build-necessary
run: |
@ -66,7 +66,7 @@ jobs:
if: ${{ needs.check.outputs.build-nightly == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set nightly matrix
id: nightly-ppa
run: |
@ -84,7 +84,7 @@ jobs:
matrix: ${{ fromJson(needs.setup.outputs.nightly-matrix) }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
@ -135,10 +135,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v5.3.0
uses: crazy-max/ghaction-import-gpg@v6.0.0
with:
gpg_private_key: ${{ secrets.APT_GPG }}

View File

@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: recursive
@ -76,7 +76,7 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: recursive
@ -125,7 +125,7 @@ jobs:
QT_VERSION: 5.15.2
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: recursive

View File

@ -33,7 +33,7 @@ jobs:
platform: amlogic
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
@ -62,7 +62,7 @@ jobs:
name: macOS
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
@ -97,7 +97,7 @@ jobs:
QT_VERSION: 5.15.2
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: recursive
@ -162,7 +162,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# Generate environment variables
- name: Generate environment variables from .version and tag

View File

@ -10,7 +10,7 @@ jobs:
steps:
# Dispatch event to build new HyperBian image
- name: Dispatch HyperBian build
uses: peter-evans/repository-dispatch@v2.1.1
uses: peter-evans/repository-dispatch@v2.1.2
if: ${{ github.repository_owner == 'hyperion-project'}}
with:
repository: hyperion-project/HyperBian

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

@ -86,6 +86,8 @@
"conf_leds_layout_cl_bottomright": "Bottom Right (Corner)",
"conf_leds_layout_cl_cornergap": "Corner Gap",
"conf_leds_layout_cl_edgegap": "Edge Gap",
"conf_leds_layout_cl_entertainment": "Entertainment Area",
"conf_leds_layout_cl_entertainment_center": "Entertainment Area Center",
"conf_leds_layout_cl_gaglength": "Gap length",
"conf_leds_layout_cl_gappos": "gap position",
"conf_leds_layout_cl_hleddepth": "Horizontal LED depth",
@ -116,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",
@ -564,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",
@ -618,7 +622,7 @@
"edt_dev_spec_gpioBcm_title": "GPIO Pin",
"edt_dev_spec_gpioMap_title": "GPIO mapping",
"edt_dev_spec_gpioNumber_title": "GPIO number",
"edt_dev_spec_groupId_title": "Group ID",
"edt_dev_spec_groupId_title": "Group",
"edt_dev_spec_header_title": "Specific Settings",
"edt_dev_spec_interpolation_title": "Interpolation",
"edt_dev_spec_intervall_title": "Interval",
@ -683,6 +687,7 @@
"edt_dev_spec_transistionTime_title": "Transition time",
"edt_dev_spec_uid_title": "UID",
"edt_dev_spec_universe_title": "Universe",
"edt_dev_spec_useAPIv2_title": "Use API v2",
"edt_dev_spec_useEntertainmentAPI_title": "Use Hue Entertainment API",
"edt_dev_spec_useOrbSmoothing_title": "Use orb smoothing",
"edt_dev_spec_useRgbwProtocol_title": "Use RGBW protocol",
@ -755,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.",
@ -1087,7 +1094,7 @@
"wiz_cololight_noprops": "Not able to get device properties - Define Hardware LED count manually",
"wiz_cololight_title": "Cololight Wizard",
"wiz_guideyou": "The $1 will guide you through the settings. Just press the button!",
"wiz_hue_blinkblue": "Let ID $1 light up blue",
"wiz_hue_blinkblue": "Let it light up",
"wiz_hue_clientkey": "Clientkey",
"wiz_hue_create_user": "Create new User",
"wiz_hue_desc1": "1. Hyperion searches automatically for a Hue-Bridge, in case it cannot find one you need to provide the hostname or IP-address and push the reload button. <br> 2. Provide a user ID, if you do not have one create a new one.",
@ -1118,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

@ -327,7 +327,7 @@ $(document).ready(function () {
var saveOptions = conf_editor_screen.getValue();
var instCaptOptions = window.serverConfig.instCapture;
instCaptOptions.systemEnable = true;
instCaptOptions.systemEnable = saveOptions.framegrabber.enable;
saveOptions.instCapture = instCaptOptions;
requestWriteConfig(saveOptions);
@ -679,7 +679,7 @@ $(document).ready(function () {
var saveOptions = conf_editor_video.getValue();
var instCaptOptions = window.serverConfig.instCapture;
instCaptOptions.v4lEnable = true;
instCaptOptions.v4lEnable = saveOptions.grabberV4L2.enable;
saveOptions.instCapture = instCaptOptions;
requestWriteConfig(saveOptions);
@ -805,7 +805,7 @@ $(document).ready(function () {
const saveOptions = conf_editor_audio.getValue();
const instCaptOptions = window.serverConfig.instCapture;
instCaptOptions.audioEnable = true;
instCaptOptions.audioEnable = saveOptions.grabberAudio.enable;
saveOptions.instCapture = instCaptOptions;
requestWriteConfig(saveOptions);

View File

@ -1021,11 +1021,6 @@ $(document).ready(function () {
var generalOptions = window.serverSchema.properties.device;
var ledType = $(this).val();
// philipshueentertainment backward fix
if (ledType == "philipshueentertainment")
ledType = "philipshue";
var specificOptions = window.serverSchema.properties.alldevices[ledType];
conf_editor = createJsonEditor('editor_container_leddevice', {
@ -1055,18 +1050,22 @@ $(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();
if (ledType == "philipshue") {
$('#root_specificOptions_useEntertainmentAPI').on("change", function () {
var ledWizardType = (this.checked) ? "philipshueentertainment" : ledType;
var ledWizardType = ledType;
var data = { type: ledWizardType };
var hue_title = (this.checked) ? 'wiz_hue_e_title' : 'wiz_hue_title';
var hue_title = 'wiz_hue_title';
changeWizard(data, hue_title, startWizardPhilipsHue);
});
$("#root_specificOptions_useEntertainmentAPI").trigger("change");
}
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;
@ -1092,6 +1091,7 @@ $(document).ready(function () {
var colorOrderDefault = "rgb";
var filter = {};
$('#btn_layout_controller').hide();
$('#btn_test_controller').hide();
switch (ledType) {
@ -1349,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 = {};
@ -1360,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;
@ -1676,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();
@ -2155,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) {
@ -2173,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":
@ -2233,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);
}
}
@ -2415,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

@ -604,7 +604,7 @@ var lightPosTopLeft112 = { hmin: 0, hmax: 0.5, vmin: 0, vmax: 0.15 };
var lightPosTopLeft121 = { hmin: 0.5, hmax: 1, vmin: 0, vmax: 0.15 };
var lightPosTopLeftNewMid = { hmin: 0.25, hmax: 0.75, vmin: 0, vmax: 0.15 };
function assignLightPos(id, pos, name) {
function assignLightPos(pos, name) {
var i = null;
if (pos === "top")
@ -695,52 +695,50 @@ devicesProperties = {};
var hueIPs = [];
var hueIPsinc = 0;
var hueLights = null;
var hueGroups = null;
var hueLights = [];
var hueEntertainmentConfigs = [];
var hueEntertainmentServices = [];
var lightLocation = [];
var groupLights = [];
var groupChannels = [];
var groupLightsLocations = [];
var hueType = "philipshue";
var isAPIv2Ready = true;
var isEntertainmentReady = true;
function startWizardPhilipsHue(e) {
if (typeof e.data.type != "undefined") hueType = e.data.type;
//create html
var hue_title = 'wiz_hue_title';
var hue_intro1 = 'wiz_hue_intro1';
var hue_intro1 = 'wiz_hue_e_intro1';
var hue_desc1 = 'wiz_hue_desc1';
var hue_create_user = 'wiz_hue_create_user';
if (hueType == 'philipshueentertainment') {
hue_title = 'wiz_hue_e_title';
hue_intro1 = 'wiz_hue_e_intro1';
hue_desc1 = 'wiz_hue_e_desc1';
hue_create_user = 'wiz_hue_e_create_user';
}
$('#wiz_header').html('<i class="fa fa-magic fa-fw"></i>' + $.i18n(hue_title));
$('#wizp1_body').html('<h4 style="font-weight:bold;text-transform:uppercase;">' + $.i18n(hue_title) + '</h4><p>' + $.i18n(hue_intro1) + '</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>');
$('#wizp2_body').html('<div id="wh_topcontainer"></div>');
var hidePort = "hidden-lg";
if (storedAccess === 'expert') {
hidePort = "";
}
$('#wh_topcontainer').append('<p class="text-left" style="font-weight:bold">' + $.i18n(hue_desc1) + '</p>' +
var topContainer_html = '<p class="text-left" style="font-weight:bold">' + $.i18n(hue_desc1) + '</p>' +
'<div class="row">' +
'<div class="col-md-2">' +
' <p class="text-left">' + $.i18n('wiz_hue_ip') + '</p></div>' +
' <div class="col-md-7"><div class="input-group">' +
' <span class="input-group-addon" id="retry_bridge" style="cursor:pointer"><i class="fa fa-refresh"></i></span>' +
' <input type="text" class="input-group form-control" id="host" placeholder="' + $.i18n('wiz_hue_ip') + '"></div></div>' +
' <div class="col-md-3 ' + hidePort + '"><div class="input-group">' +
' <select id="hue_bridge_select" class="hue_bridge_sel_watch form-control">' + '</select>' + '</div></div>' +
' <div class="col-md-7"><div class="input-group">' +
' <span class="input-group-addon"><i class="fa fa-arrow-right"></i></span>' +
' <input type="text" class="input-group form-control" id="host" placeholder="' + $.i18n('wiz_hue_ip') + '"></div></div>';
if (storedAccess === 'expert') {
topContainer_html += '<div class="col-md-3"><div class="input-group">' +
'<span class="input-group-addon">:</span>' +
' <input type="text" class="input-group form-control" id="port" placeholder="' + $.i18n('edt_conf_general_port_title') + '"></div></div>' +
'</div><p><span style="font-weight:bold;color:red" id="wiz_hue_ipstate"></span><span style="font-weight:bold;" id="wiz_hue_discovered"></span></p>'
);
$('#wh_topcontainer').append();
$('#wh_topcontainer').append('<div class="form-group" id="usrcont" style="display:none"></div>');
'<input type="text" class="input-group form-control" id="port" placeholder="' + $.i18n('edt_conf_general_port_title') + '"></div></div>';
}
topContainer_html += '</div><p><span style="font-weight:bold;color:red" id="wiz_hue_ipstate"></span><span style="font-weight:bold;" id="wiz_hue_discovered"></span></p>';
topContainer_html += '<div class="form-group" id="usrcont" style="display:none"></div>';
$('#wh_topcontainer').append(topContainer_html);
$('#usrcont').append('<div class="row"><div class="col-md-2"><p class="text-left">' + $.i18n('wiz_hue_username') + '</p ></div>' +
'<div class="col-md-7">' +
@ -751,23 +749,18 @@ function startWizardPhilipsHue(e) {
'</div><input type="hidden" id="groupId">'
);
if (hueType == 'philipshueentertainment') {
$('#usrcont').append('<div class="row"><div class="col-md-2"><p class="text-left">' + $.i18n('wiz_hue_clientkey') +
$('#usrcont').append('<div id="hue_client_key_r" class="row"><div class="col-md-2"><p class="text-left">' + $.i18n('wiz_hue_clientkey') +
'</p></div><div class="col-md-7"><input class="form-control" id="clientkey" type="text"></div></div><br>');
}
$('#usrcont').append('<p><span style="font-weight:bold;color:red" id="wiz_hue_usrstate"></span><\p>' +
'<button type="button" class="btn btn-primary" style="display:none" id="wiz_hue_create_user"> <i class="fa fa-fw fa-plus"></i>' + $.i18n(hue_create_user) + '</button>');
if (hueType == 'philipshueentertainment') {
$('#wizp2_body').append('<div id="hue_grp_ids_t" style="display:none"><p class="text-left" style="font-weight:bold">' + $.i18n('wiz_hue_e_desc2') + '</p></div>');
createTable("gidsh", "gidsb", "hue_grp_ids_t");
$('.gidsh').append(createTableRow([$.i18n('edt_dev_spec_groupId_title'), $.i18n('wiz_hue_e_use_group')], true));
$('.gidsh').append(createTableRow([$.i18n('edt_dev_spec_groupId_title'), ""], true));
$('#wizp2_body').append('<div id="hue_ids_t" style="display:none"><p class="text-left" style="font-weight:bold" id="hue_id_headline">' + $.i18n('wiz_hue_e_desc3') + '</p></div>');
}
else {
$('#wizp2_body').append('<div id="hue_ids_t" style="display:none"><p class="text-left" style="font-weight:bold" id="hue_id_headline">' + $.i18n('wiz_hue_desc2') + '</p></div>');
}
createTable("lidsh", "lidsb", "hue_ids_t");
$('.lidsh').append(createTableRow([$.i18n('edt_dev_spec_lightid_title'), $.i18n('wiz_pos'), $.i18n('wiz_identify')], true));
$('#wizp2_footer').html('<button type="button" class="btn btn-primary" id="btn_wiz_save" style="display:none"><i class="fa fa-fw fa-save"></i>' + $.i18n('general_btn_save') + '</button><button type="button" class="btn btn-danger" id="btn_wiz_abort"><i class="fa fa-fw fa-close"></i>' + $.i18n('general_btn_cancel') + '</button>');
@ -793,14 +786,27 @@ function startWizardPhilipsHue(e) {
function checkHueBridge(cb, hueUser) {
var usr = (typeof hueUser != "undefined") ? hueUser : 'config';
if (usr == 'config') $('#wiz_hue_discovered').html("");
if (usr === 'config') {
$('#wiz_hue_discovered').html("");
}
if (hueIPs[hueIPsinc]) {
var host = hueIPs[hueIPsinc].host;
var port = hueIPs[hueIPsinc].port;
if (usr != '')
{
getProperties_hue_bridge(cb, decodeURIComponent(host), port, usr);
}
else
{
cb(false, usr);
}
if (isAPIv2Ready) {
$('#port').val(443);
}
}
}
function checkBridgeResult(reply, usr) {
@ -811,37 +817,51 @@ function checkBridgeResult(reply, usr) {
$('#port').val(hueIPs[hueIPsinc].port)
$('#usrcont').toggle(true);
checkHueBridge(checkUserResult, $('#user').val() ? $('#user').val() : "newdeveloper");
}
else {
//increment and check again
if (hueIPs.length - 1 > hueIPsinc) {
hueIPsinc++;
checkHueBridge(checkBridgeResult);
checkHueBridge(checkUserResult, $('#user').val());
}
else {
$('#usrcont').toggle(false);
$('#wiz_hue_ipstate').html($.i18n('wiz_hue_failure_ip'));
}
}
};
function checkUserResult(reply, usr) {
function checkUserResult(reply, username) {
$('#usrcont').toggle(true);
if (reply) {
$('#user').val(usr);
if (hueType == 'philipshueentertainment' && $('#clientkey').val() == "") {
var hue_create_user = 'wiz_hue_e_create_user';
if (!isEntertainmentReady) {
hue_create_user = 'wiz_hue_create_user';
$('#hue_client_key_r').toggle(false);
} else {
$('#hue_client_key_r').toggle(true);
}
$('#wiz_hue_create_user').text($.i18n(hue_create_user));
$('#wiz_hue_create_user').toggle(true);
if (reply) {
$('#user').val(username);
if (isEntertainmentReady && $('#clientkey').val() == "") {
$('#wiz_hue_usrstate').html($.i18n('wiz_hue_e_clientkey_needed'));
$('#wiz_hue_create_user').toggle(true);
} else {
$('#wiz_hue_usrstate').html("");
$('#wiz_hue_create_user').toggle(false);
if (hueType == 'philipshue') {
get_hue_lights();
}
if (hueType == 'philipshueentertainment') {
get_hue_groups();
if (isEntertainmentReady) {
$('#hue_id_headline').text($.i18n('wiz_hue_e_desc3'));
$('#hue_grp_ids_t').toggle(true);
get_hue_groups(username);
} else {
$('#hue_id_headline').text($.i18n('wiz_hue_desc2'));
$('#hue_grp_ids_t').toggle(false);
get_hue_lights(username);
}
}
}
@ -852,22 +872,73 @@ function checkUserResult(reply, usr) {
}
};
function useGroupId(id) {
$('#groupId').val(id);
function useGroupId(id, username) {
$('#groupId').val(hueEntertainmentConfigs[id].id);
if (isAPIv2Ready) {
var group = hueEntertainmentConfigs[id];
groupLights = [];
for (const light of group.light_services) {
groupLights.push(light.rid);
}
groupChannels = [];
for (const channel of group.channels) {
groupChannels.push(channel);
}
groupLightsLocations = [];
for (const location of group.locations.service_locations) {
groupLightsLocations.push(location);
}
} else {
//Ensure ligthIDs are strings
groupLights = hueGroups[id].lights.map(num => {
groupLights = hueEntertainmentConfigs[id].lights.map(num => {
return String(num);
});
groupLightsLocations = hueGroups[id].locations;
get_hue_lights();
var lightLocations = hueEntertainmentConfigs[id].locations;
for (var locationID in lightLocations) {
var lightLocation = {};
let position = {
x: lightLocations[locationID][0],
y: lightLocations[locationID][1],
z: lightLocations[locationID][2]
};
lightLocation.position = position;
groupLightsLocations.push(lightLocation);
}
}
get_hue_lights(username);
}
function updateBridgeDetails(properties) {
var ledDeviceProperties = properties.config;
if (!jQuery.isEmptyObject(ledDeviceProperties)) {
isEntertainmentReady = properties.isEntertainmentReady;
isAPIv2Ready = properties.isAPIv2Ready;
if (ledDeviceProperties.name && ledDeviceProperties.bridgeid && ledDeviceProperties.modelid) {
$('#wiz_hue_discovered').html(
"Bridge: " + ledDeviceProperties.name +
", Modelid: " + ledDeviceProperties.modelid +
", Firmware: " + ledDeviceProperties.swversion + "<br/>" +
"API-Version: " + ledDeviceProperties.apiversion +
", Entertainment: " + (isEntertainmentReady ? "&#10003;" : "-") +
", APIv2: " + (isAPIv2Ready ? "&#10003;" : "-")
);
}
}
}
async function discover_hue_bridges() {
$('#wiz_hue_ipstate').html($.i18n('edt_dev_spec_devices_discovery_inprogress'));
$('#wiz_hue_discovered').html("")
// $('#wiz_hue_discovered').html("")
const res = await requestLedDeviceDiscovery('philipshue');
if (res && !res.error) {
const r = res.info;
@ -903,11 +974,6 @@ async function discover_hue_bridges() {
port = device.port;
}
//Remap https port to http port until Hue-API v2 is supported
if (port == 443) {
port = 80;
}
if (host) {
if (!hueIPs.some(item => item.host === host)) {
@ -916,22 +982,39 @@ async function discover_hue_bridges() {
}
}
}
$('#wiz_hue_ipstate').html("");
$('#host').val(hueIPs[hueIPsinc].host)
$('#port').val(hueIPs[hueIPsinc].port)
$('#hue_bridge_select').html("");
for (var key in hueIPs) {
$('#hue_bridge_select').append(createSelOpt(key, hueIPs[key].host));
}
$('.hue_bridge_sel_watch').on("click", function () {
hueIPsinc = $(this).val();
var name = $("#hue_bridge_select option:selected").text();
$('#host').val(name);
$('#port').val(hueIPs[hueIPsinc].port)
var usr = $('#user').val();
if (usr != "") {
checkHueBridge(checkUserResult, usr);
} else {
checkHueBridge(checkBridgeResult);
}
});
$('.hue_bridge_sel_watch').click();
}
}
}
async function getProperties_hue_bridge(cb, hostAddress, port, username, resourceFilter) {
let params = { host: hostAddress, user: username, filter: resourceFilter };
let params = { host: hostAddress, username: username, filter: resourceFilter };
if (port !== 'undefined') {
params.port = parseInt(port);
}
@ -945,23 +1028,27 @@ async function getProperties_hue_bridge(cb, hostAddress, port, username, resourc
}
// Use device's properties, if properties in chache
if (devicesProperties[ledType][key]) {
if (devicesProperties[ledType][key] && devicesProperties[ledType][key][username]) {
updateBridgeDetails(devicesProperties[ledType][key]);
cb(true, username);
} else {
const res = await requestLedDeviceProperties(ledType, params);
if (res && !res.error) {
var ledDeviceProperties = res.info.properties;
if (!jQuery.isEmptyObject(ledDeviceProperties)) {
devicesProperties[ledType][key] = {};
devicesProperties[ledType][key][username] = ledDeviceProperties;
isAPIv2Ready = res.info.isAPIv2Ready;
devicesProperties[ledType][key].isAPIv2Ready = isAPIv2Ready;
isEntertainmentReady = res.info.isEntertainmentReady;
devicesProperties[ledType][key].isEntertainmentReady = isEntertainmentReady;
updateBridgeDetails(devicesProperties[ledType][key]);
if (username === "config") {
if (ledDeviceProperties.name && ledDeviceProperties.bridgeid && ledDeviceProperties.modelid) {
$('#wiz_hue_discovered').html("Bridge: " + ledDeviceProperties.name + ", Modelid: " + ledDeviceProperties.modelid + ", API-Version: " + ledDeviceProperties.apiversion);
cb(true);
}
} else {
devicesProperties[ledType][key] = ledDeviceProperties;
cb(true, username);
}
} else {
@ -973,12 +1060,12 @@ async function getProperties_hue_bridge(cb, hostAddress, port, username, resourc
}
}
async function identify_hue_device(hostAddress, port, username, id) {
async function identify_hue_device(hostAddress, port, username, name, id, id_v1) {
var disabled = $('#btn_wiz_save').is(':disabled');
// Take care that new record cannot be save during background process
$('#btn_wiz_save').prop('disabled', true);
let params = { host: decodeURIComponent(hostAddress), user: username, lightId: id };
let params = { host: decodeURIComponent(hostAddress), username: username, lightName: decodeURIComponent(name), lightId: id, lightId_v1: id_v1 };
if (port !== 'undefined') {
params.port = parseInt(port);
@ -1003,12 +1090,10 @@ function beginWizardHue() {
$('#user').val(usr);
}
if (hueType == 'philipshueentertainment') {
var clkey = eV("clientkey");
if (clkey != "") {
$('#clientkey').val(clkey);
}
}
//check if host is empty/reachable/search for bridge
if (eV("host") == "") {
@ -1022,13 +1107,13 @@ function beginWizardHue() {
$('#host').val(host);
var port = eV("port");
if (port == 0) {
$('#port').val(80);
}
else {
if (port > 0) {
$('#port').val(port);
}
hueIPs.unshift({ host: host, port: port });
else {
$('#port').val('');
}
hueIPs.push({ host: host, port: port });
if (usr != "") {
checkHueBridge(checkUserResult, usr);
@ -1038,18 +1123,18 @@ function beginWizardHue() {
}
$('#retry_bridge').off().on('click', function () {
var host = $('#host').val();
var port = parseInt($('#port').val());
if ($('#host').val() != "") {
if (host != "") {
hueIPs = [];
hueIPsinc = 0;
var port = $('#port').val();
if (isNaN(port) || port < 1 || port > 65535) {
port = 80;
$('#port').val(80);
var idx = hueIPs.findIndex(item => item.host === host && item.port === port);
if (idx === -1) {
hueIPs.push({ host: host, port: port });
hueIPsinc = hueIPs.length - 1;
} else {
hueIPsinc = idx;
}
hueIPs.push({ host: $('#host').val(), port: port });
}
else {
discover_hue_bridges();
@ -1064,29 +1149,177 @@ function beginWizardHue() {
});
$('#retry_usr').off().on('click', function () {
checkHueBridge(checkUserResult, $('#user').val() ? $('#user').val() : "newdeveloper");
checkHueBridge(checkUserResult, $('#user').val());
});
$('#wiz_hue_create_user').off().on('click', function () {
if ($('#host').val() != "") {
hueIPs.unshift({ host: $('#host').val(), port: $('#port').val() });
}
createHueUser();
});
function assignLightEntertainmentPos(isFocusCenter, position, name, id) {
var x = position.x;
var z = position.z;
if (isFocusCenter) {
// Map lights as in centered range -0.5 to 0.5
if (x < -0.5) {
x = -0.5;
} else if (x > 0.5) {
x = 0.5;
}
if (z < -0.5) {
z = -0.5;
} else if (z > 0.5) {
z = 0.5;
}
} else {
// Map lights as in full range -1 to 1
x /= 2;
z /= 2;
}
var h = x + 0.5;
var v = -z + 0.5;
var hmin = h - 0.05;
var hmax = h + 0.05;
var vmin = v - 0.05;
var vmax = v + 0.05;
let layoutObject = {
hmin: hmin < 0 ? 0 : hmin,
hmax: hmax > 1 ? 1 : hmax,
vmin: vmin < 0 ? 0 : vmin,
vmax: vmax > 1 ? 1 : vmax,
name: name
};
if (id) {
layoutObject.name += "_" + id;
}
return layoutObject;
}
function assignSegmentedLightPos(segment, position, name) {
var layoutObjects = [];
var segTotalLength = 0;
for (var key in segment) {
segTotalLength += segment[key].length;
}
var min;
var max;
var horizontal = true;
var layoutObject = assignLightPos(position, name);
if (position === "left" || position === "right") {
// vertical distribution
min = layoutObject.vmin;
max = layoutObject.vmax;
horizontal = false;
} else {
// horizontal distribution
min = layoutObject.hmin;
max = layoutObject.hmax;
}
var step = (max - min) / segTotalLength;
var start = min;
for (var key in segment) {
min = start;
max = round(start + segment[key].length * step);
if (horizontal) {
layoutObject.hmin = min;
layoutObject.hmax = max;
} else {
layoutObject.vmin = min;
layoutObject.vmax = max;
}
layoutObject.name = name + "_" + key;
layoutObjects.push(JSON.parse(JSON.stringify(layoutObject)));
start = max;
}
return layoutObjects;
}
$('#btn_wiz_save').off().on("click", function () {
var hueLedConfig = [];
var finalLightIds = [];
var channelNumber = 0;
//create hue led config
for (var key in hueLights) {
if (hueType == 'philipshueentertainment') {
if (groupLights.indexOf(key) == -1) continue;
for (var key in groupLights) {
var lightId = groupLights[key];
if ($('#hue_' + lightId).val() != "disabled") {
finalLightIds.push(lightId);
var lightName;
if (isAPIv2Ready) {
var light = hueLights.find(light => light.id === lightId);
lightName = light.metadata.name;
} else {
lightName = hueLights[lightId].name;
}
var position = $('#hue_' + lightId).val();
var lightIdx = groupLights.indexOf(lightId);
var lightLocation = groupLightsLocations[lightIdx];
var serviceID;
if (isAPIv2Ready) {
serviceID = lightLocation.service.rid;
}
if (position.startsWith("entertainment")) {
// Layout per entertainment area definition at bridge
var isFocusCenter = false;
if (position === "entertainment_center") {
isFocusCenter = true;
}
if (isAPIv2Ready) {
groupChannels.forEach((channel) => {
if (channel.members[0].service.rid === serviceID) {
var layoutObject = assignLightEntertainmentPos(isFocusCenter, channel.position, lightName, channel.channel_id);
hueLedConfig.push(JSON.parse(JSON.stringify(layoutObject)));
++channelNumber;
}
});
} else {
var layoutObject = assignLightEntertainmentPos(isFocusCenter, lightLocation.position, lightName);
hueLedConfig.push(JSON.parse(JSON.stringify(layoutObject)));
}
}
else {
// Layout per manual settings
var maxSegments = 1;
if (isAPIv2Ready) {
var service = hueEntertainmentServices.find(service => service.id === serviceID);
maxSegments = service.segments.max_segments;
}
if (maxSegments > 1) {
var segment = service.segments.segments;
var layoutObjects = assignSegmentedLightPos(segment, position, lightName);
hueLedConfig.push(...layoutObjects);
} else {
var layoutObject = assignLightPos(position, lightName);
hueLedConfig.push(JSON.parse(JSON.stringify(layoutObject)));
}
channelNumber += maxSegments;
}
if ($('#hue_' + key).val() != "disabled") {
finalLightIds.push(key);
var idx_content = assignLightPos(key, $('#hue_' + key).val(), hueLights[key].name);
hueLedConfig.push(JSON.parse(JSON.stringify(idx_content)));
}
}
@ -1121,7 +1354,7 @@ function beginWizardHue() {
d.brightnessFactor = parseFloat(eV("brightnessFactor", 1));
d.clientkey = $('#clientkey').val();
d.groupId = parseInt($('#groupId').val());
d.groupId = $('#groupId').val();
d.blackLightsTimeout = parseInt(eV("blackLightsTimeout", 5000));
d.brightnessMin = parseFloat(eV("brightnessMin", 0));
d.brightnessMax = parseFloat(eV("brightnessMax", 1));
@ -1134,8 +1367,16 @@ function beginWizardHue() {
d.enableAttempts = parseInt(conf_editor.getEditor("root.generalOptions.enableAttempts").getValue());
d.enableAttemptsInterval = parseInt(conf_editor.getEditor("root.generalOptions.enableAttemptsInterval").getValue());
if (hueType == 'philipshue') {
d.useEntertainmentAPI = false;
d.useEntertainmentAPI = isEntertainmentReady;
d.useAPIv2 = isAPIv2Ready;
if (isEntertainmentReady) {
d.hardwareLedCount = channelNumber;
if (window.serverConfig.device.type !== d.type) {
//smoothing on, if new device
sc.smoothing = { enable: true };
}
} else {
d.hardwareLedCount = finalLightIds.length;
d.verbose = false;
if (window.serverConfig.device.type !== d.type) {
@ -1144,15 +1385,6 @@ function beginWizardHue() {
}
}
if (hueType == 'philipshueentertainment') {
d.useEntertainmentAPI = true;
d.hardwareLedCount = groupLights.length;
if (window.serverConfig.device.type !== d.type) {
//smoothing on, if new device
sc.smoothing = { enable: true };
}
}
window.serverConfig.device = d;
requestWriteConfig(sc, true);
@ -1163,7 +1395,6 @@ function beginWizardHue() {
}
function createHueUser() {
var host = hueIPs[hueIPsinc].host;
var port = hueIPs[hueIPsinc].port;
@ -1208,7 +1439,8 @@ function createHueUser() {
conf_editor.getEditor("root.specificOptions.host").setValue(host);
conf_editor.getEditor("root.specificOptions.port").setValue(port);
}
if (hueType == 'philipshueentertainment') {
if (isEntertainmentReady) {
var clientkey = response.clientkey;
if (clientkey != 'undefined') {
$('#clientkey').val(clientkey);
@ -1230,37 +1462,52 @@ function createHueUser() {
}, retryInterval * 1000);
}
function get_hue_groups() {
function get_hue_groups(username) {
var host = hueIPs[hueIPsinc].host;
if (devicesProperties['philipshue'][host]) {
var ledProperties = devicesProperties['philipshue'][host];
if (devicesProperties['philipshue'][host] && devicesProperties['philipshue'][host][username]) {
var ledProperties = devicesProperties['philipshue'][host][username];
if (!jQuery.isEmptyObject(ledProperties)) {
hueGroups = ledProperties.groups;
if (Object.keys(hueGroups).length > 0) {
if (isAPIv2Ready) {
if (!jQuery.isEmptyObject(ledProperties.data)) {
if (Object.keys(ledProperties.data).length > 0) {
hueEntertainmentConfigs = ledProperties.data.filter(config => {
return config.type === "entertainment_configuration";
});
hueEntertainmentServices = ledProperties.data.filter(config => {
return (config.type === "entertainment" && config.renderer === true);
});
}
}
} else {
if (!jQuery.isEmptyObject(ledProperties.groups)) {
hueEntertainmentConfigs = [];
var hueGroups = ledProperties.groups;
for (var groupid in hueGroups) {
if (hueGroups[groupid].type == 'Entertainment') {
hueGroups[groupid].id = groupid;
hueEntertainmentConfigs.push(hueGroups[groupid]);
}
}
}
}
if (Object.keys(hueEntertainmentConfigs).length > 0) {
$('.lidsb').html("");
$('#wh_topcontainer').toggle(false);
$('#hue_grp_ids_t').toggle(true);
var gC = 0;
for (var groupid in hueGroups) {
if (hueGroups[groupid].type == 'Entertainment') {
$('.gidsb').append(createTableRow([groupid + ' (' + hueGroups[groupid].name + ')', '<button class="btn btn-sm btn-primary" onClick=useGroupId(' + groupid + ')>' + $.i18n('wiz_hue_e_use_groupid', groupid) + '</button>']));
gC++;
}
}
if (gC == 0) {
noAPISupport('wiz_hue_e_noegrpids');
}
for (var groupid in hueEntertainmentConfigs) {
$('.gidsb').append(createTableRow([groupid + ' (' + hueEntertainmentConfigs[groupid].name + ')', '<button class="btn btn-sm btn-primary" onClick=useGroupId("' + groupid + '","' + username + '")>' + $.i18n('wiz_hue_e_use_group') + '</button>']));
}
} else {
noAPISupport('wiz_hue_e_noegrpids', username);
}
}
}
function noAPISupport(txt) {
function noAPISupport(txt, username) {
showNotification('danger', $.i18n('wiz_hue_e_title'), $.i18n('wiz_hue_e_noapisupport_hint'));
conf_editor.getEditor("root.specificOptions.useEntertainmentAPI").setValue(false);
$("#root_specificOptions_useEntertainmentAPI").trigger("change");
@ -1269,21 +1516,32 @@ function noAPISupport(txt) {
var txt = (txt) ? $.i18n(txt) : $.i18n('wiz_hue_e_nogrpids');
$('<p style="font-weight:bold;color:red;">' + txt + '<br />' + $.i18n('wiz_hue_e_noapisupport') + '</p>').insertBefore('#wizp2_body #hue_ids_t');
$('#hue_id_headline').html($.i18n('wiz_hue_desc2'));
hueType = 'philipshue';
get_hue_lights();
get_hue_lights(username);
}
function get_hue_lights() {
function get_hue_lights(username) {
var host = hueIPs[hueIPsinc].host;
if (devicesProperties['philipshue'][host]) {
var ledProperties = devicesProperties['philipshue'][host];
if (devicesProperties['philipshue'][host] && devicesProperties['philipshue'][host][username]) {
var ledProperties = devicesProperties['philipshue'][host][username];
if (isAPIv2Ready) {
if (!jQuery.isEmptyObject(ledProperties.data)) {
if (Object.keys(ledProperties.data).length > 0) {
hueLights = ledProperties.data.filter(config => {
return config.type === "light";
});
}
}
} else {
if (!jQuery.isEmptyObject(ledProperties.lights)) {
hueLights = ledProperties.lights;
}
}
if (Object.keys(hueLights).length > 0) {
if (hueType == 'philipshue') {
if (!isEntertainmentReady) {
$('#wh_topcontainer').toggle(false);
}
$('#hue_ids_t, #btn_wiz_save').toggle(true);
@ -1299,21 +1557,41 @@ function get_hue_lights() {
"lightPosBottomLeft112", "lightPosBottomLeftNewMid", "lightPosBottomLeft121"
];
if (hueType == 'philipshue') {
if (isEntertainmentReady) {
lightOptions.unshift("entertainment_center");
lightOptions.unshift("entertainment");
} else {
lightOptions.unshift("disabled");
groupLights = Object.keys(hueLights);
}
$('.lidsb').html("");
var pos = "";
for (var lightid in hueLights) {
if (hueType == 'philipshueentertainment') {
if (groupLights.indexOf(lightid) == -1) continue;
if (groupLightsLocations.hasOwnProperty(lightid)) {
lightLocation = groupLightsLocations[lightid];
var x = lightLocation[0];
var y = lightLocation[1];
var z = lightLocation[2];
var pos = "";
for (var id in groupLights) {
var lightId = groupLights[id];
var lightId_v1 = "/lights/" + lightId;
var lightName;
if (isAPIv2Ready) {
var light = hueLights.find(light => light.id === lightId);
lightName = light.metadata.name;
lightId_v1 = light.id_v1;
} else {
lightName = hueLights[lightId].name;
}
if (isEntertainmentReady) {
var lightLocation = {};
lightLocation = groupLightsLocations[id];
if (lightLocation) {
if (isAPIv2Ready) {
pos = 0;
} else {
var x = lightLocation.position.x;
var y = lightLocation.position.y;
var z = lightLocation.position.z;
var xval = (x < 0) ? "left" : "right";
if (z != 1 && x >= -0.25 && x <= 0.25) xval = "";
switch (z) {
@ -1329,6 +1607,8 @@ function get_hue_lights() {
}
}
}
}
var options = "";
for (var opt in lightOptions) {
var val = lightOptions[opt];
@ -1337,12 +1617,13 @@ function get_hue_lights() {
if (pos == val) options += ' selected="selected"';
options += '>' + $.i18n(txt + val) + '</option>';
}
$('.lidsb').append(createTableRow([lightid + ' (' + hueLights[lightid].name + ')', '<select id="hue_' + lightid + '" class="hue_sel_watch form-control">'
$('.lidsb').append(createTableRow([id + ' (' + lightName + ')', '<select id="hue_' + lightId + '" class="hue_sel_watch form-control">'
+ options
+ '</select>', '<button class="btn btn-sm btn-primary" onClick=identify_hue_device("' + encodeURIComponent($("#host").val()) + '","' + $('#port').val() + '","' + $("#user").val() + '",' + lightid + ')>' + $.i18n('wiz_hue_blinkblue', lightid) + '</button>']));
+ '</select>', '<button class="btn btn-sm btn-primary" onClick=identify_hue_device("' + encodeURIComponent($("#host").val()) + '","' + $('#port').val() + '","' + $("#user").val() + '","' + encodeURIComponent(lightName) + '","' + lightId + '","' + lightId_v1 + '")>' + $.i18n('wiz_hue_blinkblue', id) + '</button>']));
}
if (hueType != 'philipshueentertainment') {
if (!isEntertainmentReady) {
$('.hue_sel_watch').on("change", function () {
var cC = 0;
for (var key in hueLights) {
@ -1362,7 +1643,6 @@ function get_hue_lights() {
}
}
}
}
function abortConnection(UserInterval) {
clearInterval(UserInterval);
@ -1437,7 +1717,7 @@ function beginWizardYeelight() {
finalLights.push(lights[key]);
var idx_content = assignLightPos(key, $('#yee_' + key).val(), name);
var idx_content = assignLightPos($('#yee_' + key).val(), name);
yeelightLedConfig.push(JSON.parse(JSON.stringify(idx_content)));
}
}
@ -1733,7 +2013,7 @@ function beginWizardAtmoOrb() {
if (lights[key].host !== "")
name += ':' + lights[key].host;
var idx_content = assignLightPos(key, $('#orb_' + key).val(), name);
var idx_content = assignLightPos($('#orb_' + key).val(), name);
atmoorbLedConfig.push(JSON.parse(JSON.stringify(idx_content)));
}
}
@ -1907,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

@ -95,7 +95,7 @@ else
fi
# Determine if PR number exists
pulls=$(request_call "$api_url/pulls")
pulls=$(request_call "$api_url/pulls?state=open")
pr_exists=$(echo "$pulls" | tr '\r\n' ' ' | ${pythonCmd} -c """
import json,sys
@ -108,7 +108,7 @@ for i in data:
""" 2>/dev/null)
if [ "$pr_exists" != "exists" ]; then
echo "---> Pull Request $pr_number not found -> abort"
echo "---> Pull Request $pr_number not found as open PR -> abort"
exit 1
fi
@ -124,7 +124,7 @@ for i in data:
""" 2>/dev/null)
if [ -z "$head_sha" ]; then
echo "---> The specified PR #$pr_number has no longer any artifacts."
echo "---> The specified PR #$pr_number has no longer any artifacts or has been closed."
echo "---> It may be older than 14 days. Ask the PR creator to recreate the artifacts at the following URL:"
echo "---> https://github.com/hyperion-project/hyperion.ng/pull/$pr_number"
exit 1
@ -132,13 +132,13 @@ fi
if [ -z "$run_id" ]; then
# Determine run_id from head_sha
runs=$(request_call "$api_url/actions/runs")
runs=$(request_call "$api_url/actions/runs?head_sha=$head_sha")
run_id=$(echo "$runs" | tr '\r\n' ' ' | ${pythonCmd} -c """
import json,sys
data = json.load(sys.stdin)
for i in data['workflow_runs']:
if i['head_sha'] == '"$head_sha"':
if i['name'] == 'Hyperion PR Build':
print(i['id'])
break
""" 2>/dev/null)

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

@ -9,6 +9,15 @@
// Constants
namespace {
const uint16_t RESOLUTION = 255;
//Constants vuMeter
const QJsonArray DEFAULT_HOTCOLOR { 255,0,0 };
const QJsonArray DEFAULT_WARNCOLOR { 255,255,0 };
const QJsonArray DEFAULT_SAFECOLOR { 0,255,0 };
const int DEFAULT_WARNVALUE { 80 };
const int DEFAULT_SAFEVALUE { 45 };
const int DEFAULT_MULTIPLIER { 0 };
const int DEFAULT_TOLERANCE { 20 };
}
#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
@ -28,12 +37,12 @@ AudioGrabber::AudioGrabber()
, _deviceProperties()
, _device("none")
, _hotColor(QColorConstants::Red)
, _warnValue(80)
, _warnValue(DEFAULT_WARNVALUE)
, _warnColor(QColorConstants::Yellow)
, _safeValue(45)
, _safeValue(DEFAULT_SAFEVALUE)
, _safeColor(QColorConstants::Green)
, _multiplier(0)
, _tolerance(20)
, _multiplier(DEFAULT_MULTIPLIER)
, _tolerance(DEFAULT_TOLERANCE)
, _dynamicMultiplier(INT16_MAX)
, _started(false)
{
@ -61,18 +70,27 @@ void AudioGrabber::setDevice(const QString& device)
void AudioGrabber::setConfiguration(const QJsonObject& config)
{
QJsonArray hotColorArray = config["hotColor"].toArray(QJsonArray::fromVariantList(QList<QVariant>({ QVariant(255), QVariant(0), QVariant(0) })));
QJsonArray warnColorArray = config["warnColor"].toArray(QJsonArray::fromVariantList(QList<QVariant>({ QVariant(255), QVariant(255), QVariant(0) })));
QJsonArray safeColorArray = config["safeColor"].toArray(QJsonArray::fromVariantList(QList<QVariant>({ QVariant(0), QVariant(255), QVariant(0) })));
QString audioEffect = config["audioEffect"].toString();
QJsonObject audioEffectConfig = config[audioEffect].toObject();
if (audioEffect == "vuMeter")
{
QJsonArray hotColorArray = audioEffectConfig.value("hotColor").toArray(DEFAULT_HOTCOLOR);
QJsonArray warnColorArray = audioEffectConfig.value("warnColor").toArray(DEFAULT_WARNCOLOR);
QJsonArray safeColorArray = audioEffectConfig.value("safeColor").toArray(DEFAULT_SAFECOLOR);
_hotColor = QColor(hotColorArray.at(0).toInt(), hotColorArray.at(1).toInt(), hotColorArray.at(2).toInt());
_warnColor = QColor(warnColorArray.at(0).toInt(), warnColorArray.at(1).toInt(), warnColorArray.at(2).toInt());
_safeColor = QColor(safeColorArray.at(0).toInt(), safeColorArray.at(1).toInt(), safeColorArray.at(2).toInt());
_warnValue = config["warnValue"].toInt(80);
_safeValue = config["safeValue"].toInt(45);
_multiplier = config["multiplier"].toDouble(0);
_tolerance = config["tolerance"].toInt(20);
_warnValue = audioEffectConfig["warnValue"].toInt(DEFAULT_WARNVALUE);
_safeValue = audioEffectConfig["safeValue"].toInt(DEFAULT_SAFEVALUE);
_multiplier = audioEffectConfig["multiplier"].toDouble(DEFAULT_MULTIPLIER);
_tolerance = audioEffectConfig["tolerance"].toInt(DEFAULT_MULTIPLIER);
}
else
{
Error(_log, "Unknow Audio-Effect: \"%s\" configured", QSTRING_CSTR(audioEffect));
}
}
void AudioGrabber::resetMultiplier()

View File

@ -723,6 +723,7 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config)
}
//Migration steps for versions <= 2.0.13
_previousVersion = targetVersion;
targetVersion.setVersion("2.0.13");
if (_previousVersion <= targetVersion)
{
@ -774,6 +775,126 @@ bool SettingsManager::handleConfigUpgrade(QJsonObject& config)
}
}
}
//Migration steps for versions <= 2.0.16
_previousVersion = targetVersion;
targetVersion.setVersion("2.0.16");
if (_previousVersion <= targetVersion)
{
Info(_log, "Instance [%u]: Migrate from version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str());
// Have Hostname/IP-address separate from port for LED-Devices
if (config.contains("device"))
{
QJsonObject newDeviceConfig = config["device"].toObject();
if (newDeviceConfig.contains("type"))
{
QString type = newDeviceConfig["type"].toString();
if (type == "philipshue")
{
if (newDeviceConfig.contains("groupId"))
{
if (newDeviceConfig["groupId"].isDouble())
{
int groupID = newDeviceConfig["groupId"].toInt();
newDeviceConfig["groupId"] = QString::number(groupID);
migrated = true;
}
}
if (newDeviceConfig.contains("lightIds"))
{
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);
if (value.isDouble())
{
int lightId = value.toInt();
lightIds.replace(i, QString::number(lightId));
migrated = true;
}
}
newDeviceConfig["lightIds"] = lightIds;
}
}
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)
{
config["device"] = newDeviceConfig;
Debug(_log, "LED-Device records migrated");
}
}
}
}
}
return migrated;

View File

@ -4,6 +4,7 @@
//std includes
#include <sstream>
#include <iomanip>
#include <cmath>
// Qt includes
#include <QNetworkReply>
@ -33,13 +34,14 @@ 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 bool DEFAULT_IS_RESTORE_STATE = true;
const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true;
const int BRI_MAX = 100;
// 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";
@ -75,7 +77,8 @@ const char API_EXT_MODE_STRING_V2[] = "{\"write\" : {\"command\" : \"display\",
const char API_STATE[] = "state";
const char API_PANELLAYOUT[] = "panelLayout";
const char API_EFFECT[] = "effects";
const char API_IDENTIFY[] = "identify";
const char API_ADD_USER[] = "new";
const char API_EFFECT_SELECT[] = "select";
//Nanoleaf Control data stream
@ -87,21 +90,10 @@ 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
};
const double ROTATION_STEPS_DEGREE = 15.0;
} //End of constants
// Nanoleaf external control versions
enum EXTCONTROLVERSIONS {
@ -115,8 +107,6 @@ LedDeviceNanoleaf::LedDeviceNanoleaf(const QJsonObject& deviceConfig)
, _apiPort(API_DEFAULT_PORT)
, _topDown(true)
, _leftRight(true)
, _startPos(0)
, _endPos(0)
, _extControlVersion(EXTCTRLVER_V2)
, _panelLedCount(0)
{
@ -170,30 +160,60 @@ bool LedDeviceNanoleaf::init(const QJsonObject& deviceConfig)
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;
@ -225,8 +245,30 @@ 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();
@ -238,24 +280,36 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
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);
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;
@ -436,7 +486,7 @@ QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params)
_hostName = params[CONFIG_HOST].toString("");
_apiPort = API_DEFAULT_PORT;
_authToken = params["token"].toString("");
_authToken = params[CONFIG_AUTH_TOKEN].toString("");
Info(_log, "Get properties for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName));
@ -453,7 +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());
@ -466,8 +523,8 @@ void LedDeviceNanoleaf::identify(const QJsonObject& params)
DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());
_hostName = params[CONFIG_HOST].toString("");
_apiPort = API_DEFAULT_PORT;if (NetUtils::resolveHostToAddress(_log, _hostName, _address))
_authToken = params["token"].toString("");
_apiPort = API_DEFAULT_PORT;
_authToken = params[CONFIG_AUTH_TOKEN].toString("");
Info(_log, "Identify %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName));
@ -475,9 +532,7 @@ void LedDeviceNanoleaf::identify(const QJsonObject& params)
{
if (openRestAPI())
{
_restApi->setPath("identify");
// Perform request
_restApi->setPath(API_IDENTIFY);
httpResponse response = _restApi->put();
if (response.error())
{
@ -487,6 +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;
@ -513,7 +598,8 @@ bool LedDeviceNanoleaf::powerOn()
QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason());
this->setInError(errorReason);
on = false;
} else {
}
else {
on = true;
}
@ -671,7 +757,8 @@ bool LedDeviceNanoleaf::restoreState()
{
Warning(_log, "%s restoring effect failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason()));
}
} else {
}
else {
Warning(_log, "%s restoring effect failed with error: Cannot restore dynamic or solid effect. Device is switched off", QSTRING_CSTR(_activeDeviceType));
_originalIsOn = false;
}
@ -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);

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;

File diff suppressed because it is too large Load Diff

View File

@ -16,31 +16,6 @@
#include "ProviderRestApi.h"
#include "ProviderUdpSSL.h"
//Streaming message header and payload definition
const uint8_t HEADER[] =
{
'H', 'u', 'e', 'S', 't', 'r', 'e', 'a', 'm', //protocol
0x01, 0x00, //version 1.0
0x01, //sequence number 1
0x00, 0x00, //Reserved write 0s
0x01, //xy Brightness
0x00, // Reserved, write 0s
};
const uint8_t PAYLOAD_PER_LIGHT[] =
{
0x01, 0x00, 0x06, //light ID
//color: 16 bpc
0xff, 0xff,
0xff, 0xff,
0xff, 0xff,
/*
(message.R >> 8) & 0xff, message.R & 0xff,
(message.G >> 8) & 0xff, message.G & 0xff,
(message.B >> 8) & 0xff, message.B & 0xff
*/
};
/**
* A XY color point in the color space of the hue system without brightness.
*/
@ -145,13 +120,19 @@ public:
/// Constructs the light.
///
/// @param log the logger
/// @param bridge the bridge
/// @param useApiV2 make use of Hue API version 2
/// @param id the light id
/// @param lightAttributes the light's attributes as provied by the Hue Bridge
/// @param onBlackTimeToPowerOff Timeframe of Black output that triggers powering off the light
/// @param onBlackTimeToPowerOn Timeframe of non Black output that triggers powering on the light
///
PhilipsHueLight(Logger* log, int id, QJsonObject values, int ledidx,
PhilipsHueLight(Logger* log, bool useApiV2, const QString& id, const QJsonObject& lightAttributes,
int onBlackTimeToPowerOff,
int onBlackTimeToPowerOn);
void setDeviceDetails(const QJsonObject& details);
void setEntertainmentSrvDetails(const QJsonObject& details);
///
/// @param on
///
@ -167,7 +148,14 @@ public:
///
void setColor(const CiColor& color);
int getId() const;
QString getId() const;
QString getdeviceId() const;
QString getProduct() const;
QString getModel() const;
QString getName() const;
QString getArcheType() const;
int getMaxSegments() const;
bool getOnOffState() const;
int getTransitionTime() const;
@ -179,7 +167,7 @@ public:
CiColorTriangle getColorSpace() const;
void saveOriginalState(const QJsonObject& values);
QString getOriginalState() const;
QJsonObject getOriginalState() const;
bool isBusy();
bool isBlack(bool isBlack);
@ -189,24 +177,30 @@ public:
private:
Logger* _log;
/// light id
int _id;
int _ledidx;
bool _useApiV2;
QString _id;
QString _deviceId;
QString _product;
QString _model;
QString _name;
QString _archeType;
QString _gamutType;
int _maxSegments;
bool _on;
int _transitionTime;
CiColor _color;
bool _hasColor;
/// darkes blue color in hue lamp GAMUT = black
CiColor _colorBlack;
/// The model id of the hue lamp which is used to determine the color space.
QString _modelId;
QString _lightname;
CiColorTriangle _colorSpace;
/// The json string of the original state.
QJsonObject _originalStateJSON;
QString _originalState;
QJsonObject _originalState;
CiColor _originalColor;
qint64 _lastSendColorTime;
qint64 _lastBlackTime;
@ -242,23 +236,40 @@ public:
QJsonDocument get(const QString& route);
///
/// @brief Perform a REST-API POST
/// @brief Perform a REST-API GET
///
/// @param route the route of the POST request.
/// @param content the content of the POST request.
/// @param routeElements the route's elements of the GET request.
///
QJsonDocument put(const QString& route, const QString& content, bool supressError = false);
/// @return the content of the GET request.
///
QJsonDocument get(const QStringList& routeElements);
QJsonDocument getLightState( int lightId);
void setLightState( int lightId = 0, const QString &state = "");
///
/// @brief Perform a REST-API PUT
///
/// @param routeElements the route's elements of the PUT request.
/// @param content the content of the PUT request.
/// @param supressError Treat an error as a warning
///
/// @return the content of the PUT request.
///
QJsonDocument put(const QStringList& routeElements, const QJsonObject& content, bool supressError = false);
QMap<int,QJsonObject> getLightMap() const;
QJsonDocument retrieveBridgeDetails();
QJsonObject getDeviceDetails(const QString& deviceId);
QJsonObject getEntertainmentSrvDetails(const QString& deviceId);
QMap<int,QJsonObject> getGroupMap() const;
QJsonObject getLightDetails(const QString& lightId);
QJsonDocument setLightState(const QString& lightId, const QJsonObject& state);
QString getGroupName(int groupId = 0) const;
QMap<QString,QJsonObject> getDevicesMap() const;
QMap<QString,QJsonObject> getLightMap() const;
QMap<QString,QJsonObject> getGroupMap() const;
QMap<QString,QJsonObject> getEntertainmentMap() const;
QJsonArray getGroupLights(int groupId = 0) const;
QString getGroupName(const QString& groupId) const;
QStringList getGroupLights(const QString& groupId) const;
int getGroupChannelsCount(const QString& groupId) const;
protected:
@ -338,23 +349,41 @@ protected:
///
QJsonObject addAuthorization(const QJsonObject& params) override;
bool isApiEntertainmentReady(const QString& apiVersion);
bool isAPIv2Ready (int swVersion);
int getFirmwareVerion() { return _deviceFirmwareVersion; }
void setBridgeDetails( const QJsonDocument &doc, bool isLogging = false );
void setBaseApiEnvironment(bool apiV2 = true, const QString& path = "");
QJsonDocument getGroupDetails( const QString& groupId );
QJsonDocument setGroupState( const QString& groupId, bool state);
bool isStreamOwner(const QString &streamOwner) const;
bool initDevicesMap();
bool initLightsMap();
bool initGroupsMap();
bool initEntertainmentSrvsMap();
void log(const char* msg, const char* type, ...) const;
bool configureSsl();
const int * getCiphersuites() const override;
///REST-API wrapper
ProviderRestApi* _restApi;
int _apiPort;
/// User name for the API ("newdeveloper")
QString _authToken;
QString _applicationID;
bool _useHueEntertainmentAPI;
bool _useEntertainmentAPI;
bool _useApiV2;
bool _isAPIv2Ready;
QJsonDocument getGroupState( int groupId );
QJsonDocument setGroupState( int groupId, bool state);
bool isStreamOwner(const QString &streamOwner) const;
bool initMaps();
void log(const char* msg, const char* type, ...) const;
const int * getCiphersuites() const override;
bool _isDiyHue;
private:
@ -364,16 +393,25 @@ private:
///
/// @return A JSON structure holding a list of devices found
///
QJsonArray discover();
QJsonArray discoverSsdp();
QJsonDocument getAllBridgeInfos();
void setBridgeConfig( const QJsonDocument &doc );
QJsonDocument retrieveDeviceDetails(const QString& deviceId = "");
QJsonDocument retrieveLightDetails(const QString& lightId = "");
QJsonDocument retrieveGroupDetails(const QString& groupId = "");
QJsonDocument retrieveEntertainmentSrvDetails(const QString& deviceId = "");
bool retrieveApplicationId();
void setDevicesMap( const QJsonDocument &doc );
void setLightsMap( const QJsonDocument &doc );
void setGroupMap( const QJsonDocument &doc );
void setEntertainmentSrvMap( const QJsonDocument &doc );
//Philips Hue Bridge details
QString _deviceName;
QString _deviceBridgeId;
QString _deviceModel;
QString _deviceFirmwareVersion;
int _deviceFirmwareVersion;
QString _deviceAPIVersion;
uint _api_major;
@ -382,8 +420,12 @@ private:
bool _isHueEntertainmentReady;
QMap<int,QJsonObject> _lightsMap;
QMap<int,QJsonObject> _groupsMap;
QMap<QString,QJsonObject> _devicesMap;
QMap<QString,QJsonObject> _lightsMap;
QMap<QString,QJsonObject> _groupsMap;
QMap<QString,QJsonObject> _entertainmentMap;
int _lightsCount;
};
/**
@ -440,7 +482,7 @@ public:
///
/// @return Number of device's LEDs
///
unsigned int getLightsCount() const { return _lightsCount; }
int getLightsCount() const { return _lightsCount; }
void setOnOffState(PhilipsHueLight& light, bool on, bool force = false);
void setTransitionTime(PhilipsHueLight& light);
@ -547,18 +589,18 @@ private:
bool setLights();
/// creates new PhilipsHueLight(s) based on user lightid with bridge feedback
/// creates new PhilipsHueLight(s) based on user lightId with bridge feedback
///
/// @param map Map of lightid/value pairs of bridge
/// @param map Map of lightId/value pairs of bridge
///
bool updateLights(const QMap<int, QJsonObject> &map);
bool updateLights(const QMap<QString, QJsonObject> &map);
///
/// @brief Set the number of LEDs supported by the device.
///
/// @rparam[in] Number of device's LEDs
//
void setLightsCount( unsigned int lightsCount);
void setLightsCount(int lightsCount);
bool openStream();
bool getStreamGroupState();
@ -566,10 +608,8 @@ private:
bool startStream();
bool stopStream();
void writeStream(bool flush = false);
int writeSingleLights(const std::vector<ColorRgb>& ledValues);
QByteArray prepareStreamData() const;
int writeStreamData(const std::vector<ColorRgb>& ledValues, bool flush = false);
///
bool _switchOffOnBlack;
@ -582,12 +622,15 @@ private:
bool _isInitLeds;
/// Array of the light ids.
std::vector<int> _lightIds;
QStringList _lightIds;
/// Array to save the lamps.
std::vector<PhilipsHueLight> _lights;
int _lightsCount;
int _groupId;
int _channelsCount;
QString _groupId;
QString _groupName;
QString _streamOwner;
int _blackLightsTimeout;
double _blackLevel;
@ -595,15 +638,5 @@ private:
int _onBlackTimeToPowerOn;
bool _candyGamma;
// TODO: Check what is the correct class
uint32_t _handshake_timeout_min;
uint32_t _handshake_timeout_max;
bool _stopConnection;
QString _groupName;
QString _streamOwner;
qint64 _lastConfirm;
int _lastId;
bool _groupStreamState;
};

View File

@ -2,11 +2,18 @@
#include "ProviderRestApi.h"
// Qt includes
#include <QObject>
#include <QEventLoop>
#include <QNetworkReply>
#include <QByteArray>
#include <QJsonObject>
#include <QList>
#include <QHash>
#include <QFile>
#include <QSslSocket>
//std includes
#include <iostream>
#include <chrono>
@ -30,12 +37,12 @@ ProviderRestApi::ProviderRestApi(const QString& scheme, const QString& host, int
: _log(Logger::getInstance("LEDDEVICE"))
, _networkManager(nullptr)
, _requestTimeout(DEFAULT_REST_TIMEOUT)
,_isSeflSignedCertificateAccpeted(false)
{
_networkManager = new QNetworkAccessManager();
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
_networkManager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
#endif
_apiUrl.setScheme(scheme);
_apiUrl.setHost(host);
_apiUrl.setPort(port);
@ -46,7 +53,7 @@ ProviderRestApi::ProviderRestApi(const QString& scheme, const QString& host, int
: ProviderRestApi(scheme, host, port, "") {}
ProviderRestApi::ProviderRestApi(const QString& host, int port, const QString& basePath)
: ProviderRestApi("http", host, port, basePath) {}
: ProviderRestApi((port == 443) ? "https" : "http", host, port, basePath) {}
ProviderRestApi::ProviderRestApi(const QString& host, int port)
: ProviderRestApi(host, port, "") {}
@ -59,18 +66,33 @@ ProviderRestApi::~ProviderRestApi()
delete _networkManager;
}
void ProviderRestApi::setScheme(const QString& scheme)
{
_apiUrl.setScheme(scheme);
}
void ProviderRestApi::setUrl(const QUrl& url)
{
_apiUrl = url;
_basePath = url.path();
}
void ProviderRestApi::setBasePath(const QStringList& pathElements)
{
setBasePath(pathElements.join(ONE_SLASH));
}
void ProviderRestApi::setBasePath(const QString& basePath)
{
_basePath.clear();
appendPath(_basePath, basePath);
}
void ProviderRestApi::clearBasePath()
{
_basePath.clear();
}
void ProviderRestApi::setPath(const QStringList& pathElements)
{
_path.clear();
@ -83,6 +105,11 @@ void ProviderRestApi::setPath(const QString& path)
appendPath(_path, path);
}
void ProviderRestApi::clearPath()
{
_path.clear();
}
void ProviderRestApi::appendPath(const QString& path)
{
appendPath(_path, path);
@ -204,6 +231,7 @@ httpResponse ProviderRestApi::executeOperation(QNetworkAccessManager::Operation
QDateTime start = QDateTime::currentDateTime();
QString opCode;
QNetworkReply* reply;
switch (operation) {
case QNetworkAccessManager::GetOperation:
opCode = "GET";
@ -255,11 +283,11 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply)
HttpStatusCode httpStatusCode = static_cast<HttpStatusCode>(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
response.setHttpStatusCode(httpStatusCode);
response.setNetworkReplyError(reply->error());
response.setHeaders(reply->rawHeaderPairs());
if (reply->error() == QNetworkReply::NoError)
{
QByteArray replyData = reply->readAll();
if (!replyData.isEmpty())
{
QJsonParseError error;
@ -284,6 +312,12 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply)
else
{
QString errorReason;
if (reply->error() == QNetworkReply::OperationCanceledError)
{
errorReason = "Network request timeout error";
}
else
{
if (httpStatusCode > 0) {
QString httpReason = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
QString advise;
@ -292,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";
@ -307,17 +341,11 @@ httpResponse ProviderRestApi::getResponse(QNetworkReply* const& reply)
errorReason = QString ("[%3 %4] - %5").arg(httpStatusCode).arg(httpReason, advise);
}
else
{
if (reply->error() == QNetworkReply::OperationCanceledError)
{
errorReason = "Network request timeout error";
}
else
{
errorReason = reply->errorString();
}
}
response.setError(true);
response.setErrorReason(errorReason);
}
@ -344,3 +372,121 @@ void ProviderRestApi::setHeader(const QByteArray &headerName, const QByteArray &
{
_networkRequestHeaders.setRawHeader(headerName, headerValue);
}
void httpResponse::setHeaders(const QList<QNetworkReply::RawHeaderPair>& pairs)
{
_responseHeaders.clear();
for (const auto &item: pairs)
{
_responseHeaders[item.first] = item.second;
}
}
QByteArray httpResponse::getHeader(const QByteArray header) const
{
return _responseHeaders.value(header);
}
bool ProviderRestApi::setCaCertificate(const QString& caFileName)
{
bool rc {false};
/// Add our own CA to the default SSL configuration
QSslConfiguration configuration = QSslConfiguration::defaultConfiguration();
QFile caFile (caFileName);
if (!caFile.open(QIODevice::ReadOnly))
{
Error(_log,"Unable to open CA-Certificate file: %s", QSTRING_CSTR(caFileName));
return false;
}
QSslCertificate cert (&caFile);
caFile.close();
QList<QSslCertificate> allowedCAs;
allowedCAs << cert;
configuration.setCaCertificates(allowedCAs);
QSslConfiguration::setDefaultConfiguration(configuration);
#ifndef QT_NO_SSL
if (QSslSocket::supportsSsl())
{
QObject::connect( _networkManager, &QNetworkAccessManager::sslErrors, this, &ProviderRestApi::onSslErrors, Qt::UniqueConnection );
_networkManager->connectToHostEncrypted(_apiUrl.host(), _apiUrl.port(), configuration);
rc = true;
}
#endif
return rc;
}
void ProviderRestApi::acceptSelfSignedCertificates(bool isAccepted)
{
_isSeflSignedCertificateAccpeted = isAccepted;
}
void ProviderRestApi::setAlternateServerIdentity(const QString& serverIdentity)
{
_serverIdentity = serverIdentity;
}
QString ProviderRestApi::getAlternateServerIdentity() const
{
return _serverIdentity;
}
bool ProviderRestApi::checkServerIdentity(const QSslConfiguration& sslConfig) const
{
bool isServerIdentified {false};
// Perform common name validation
QSslCertificate serverCertificate = sslConfig.peerCertificate();
QStringList commonName = serverCertificate.subjectInfo(QSslCertificate::CommonName);
if ( commonName.contains(getAlternateServerIdentity(), Qt::CaseInsensitive) )
{
isServerIdentified = true;
}
return isServerIdentified;
}
void ProviderRestApi::onSslErrors(QNetworkReply* reply, const QList<QSslError>& errors)
{
int ignoredErrorCount {0};
for (const QSslError &error : errors)
{
bool ignoreSslError{false};
switch (error.error()) {
case QSslError::HostNameMismatch :
if (checkServerIdentity(reply->sslConfiguration()) )
{
ignoreSslError = true;
}
break;
case QSslError::SelfSignedCertificate :
if (_isSeflSignedCertificateAccpeted)
{
ignoreSslError = true;
}
break;
default:
break;
}
if (ignoreSslError)
{
++ignoredErrorCount;
}
else
{
Debug (_log,"SSL Error occured: [%d] %s ",error.error(), QSTRING_CSTR(error.errorString()));
}
}
if (ignoredErrorCount == errors.size())
{
reply->ignoreSslErrors();
}
}

View File

@ -10,12 +10,13 @@
#include <QUrlQuery>
#include <QJsonDocument>
#include <QFile>
#include <QBasicTimer>
#include <QTimerEvent>
#include <chrono>
constexpr std::chrono::milliseconds DEFAULT_REST_TIMEOUT{ 1000 };
constexpr std::chrono::milliseconds DEFAULT_REST_TIMEOUT{ 2000 };
//Set QNetworkReply timeout without external timer
//https://stackoverflow.com/questions/37444539/how-to-set-qnetworkreply-timeout-without-external-timer
@ -87,6 +88,10 @@ public:
QJsonDocument getBody() const { return _responseBody; }
void setBody(const QJsonDocument& body) { _responseBody = body; }
QByteArray getHeader(const QByteArray header) const;
void setHeaders(const QList<QNetworkReply::RawHeaderPair>& pairs);
QString getErrorReason() const { return _errorReason; }
void setErrorReason(const QString& errorReason) { _errorReason = errorReason; }
@ -99,6 +104,8 @@ public:
private:
QJsonDocument _responseBody {};
QHash<QByteArray, QByteArray> _responseHeaders;
bool _hasError = false;
QString _errorReason;
@ -131,6 +138,7 @@ class ProviderRestApi : public QObject
public:
///
/// @brief Constructor of the REST-API wrapper
///
ProviderRestApi();
@ -176,6 +184,20 @@ public:
///
virtual ~ProviderRestApi() override;
///
/// @brief Set the API's scheme
///
/// @param[in] scheme
///
void setScheme(const QString& scheme);
///
/// @brief Get the API's scheme
///
/// return schme
///
QString getScheme() { return _apiUrl.scheme(); }
///
/// @brief Set an API's host
///
@ -190,6 +212,13 @@ public:
///
void setPort(const int port) { _apiUrl.setPort(port); }
///
/// @brief Get the API's port
///
/// return port
///
int getPort() { return _apiUrl.port(); }
///
/// @brief Set an API's url
///
@ -204,6 +233,13 @@ public:
///
QUrl getUrl() const;
///
/// @brief Set an API's base path (the stable path element before addressing resources)
///
/// @param[in] pathElements to form a path, e.g. (clip,v2,resource) results in "/clip/v2/resource"
///
void setBasePath(const QStringList& pathElements);
///
/// @brief Set an API's base path (the stable path element before addressing resources)
///
@ -211,6 +247,11 @@ public:
///
void setBasePath(const QString& basePath);
///
/// @brief Clear an API's base path (the stable path element before addressing resources)
///
void clearBasePath();
///
/// @brief Set an API's path to address resources
///
@ -218,12 +259,18 @@ public:
///
void setPath(const QString& path);
///
/// @brief Set an API's path to address resources
///
/// @param[in] pathElements to form a path, e.g. (lights,1,state) results in "/lights/1/state/"
///
void setPath(const QStringList& pathElements);
///
/// @brief Clear an API's path
///
void clearPath();
///
/// @brief Append an API's path element to path set before
///
@ -252,6 +299,10 @@ public:
///
void setQuery(const QUrlQuery& query);
QString getBasePath() {return _basePath;}
QString getPath() {return _path;}
///
/// @brief Execute GET request
///
@ -359,6 +410,14 @@ public:
/// @param[in] timeout in milliseconds.
void setTransferTimeout(std::chrono::milliseconds timeout = DEFAULT_REST_TIMEOUT) { _requestTimeout = timeout; }
bool setCaCertificate(const QString& caFileName);
void acceptSelfSignedCertificates(bool accept);
void setAlternateServerIdentity(const QString& serverIdentity);
QString getAlternateServerIdentity() const;
///
/// @brief Set the common logger for LED-devices.
///
@ -366,6 +425,10 @@ public:
///
void setLogger(Logger* log) { _log = log; }
protected slots:
/// Handle the SSLErrors
void onSslErrors(QNetworkReply* reply, const QList<QSslError>& errors);
private:
///
@ -379,9 +442,11 @@ private:
httpResponse executeOperation(QNetworkAccessManager::Operation op, const QUrl& url, const QByteArray& body = {});
bool checkServerIdentity(const QSslConfiguration& sslConfig) const;
Logger* _log;
// QNetworkAccessManager object for sending REST-requests.
/// QNetworkAccessManager object for sending REST-requests.
QNetworkAccessManager* _networkManager;
std::chrono::milliseconds _requestTimeout;
@ -394,6 +459,9 @@ private:
QUrlQuery _query;
QNetworkRequest _networkRequestHeaders;
QString _serverIdentity;
bool _isSeflSignedCertificateAccpeted;
};
#endif // PROVIDERRESTKAPI_H

View File

@ -150,6 +150,11 @@ const int *ProviderUdpSSL::getCiphersuites() const
return mbedtls_ssl_list_ciphersuites();
}
void ProviderUdpSSL::setPSKidentity(const QString& pskIdentity)
{
_psk_identity = pskIdentity;
}
bool ProviderUdpSSL::initNetwork()
{
if ((!_isDeviceReady || _streamPaused) && _streamReady)
@ -334,6 +339,11 @@ void ProviderUdpSSL::freeSSLConnection()
}
}
void ProviderUdpSSL::writeBytes(QByteArray data, bool flush)
{
writeBytes(static_cast<uint>(data.size()), reinterpret_cast<unsigned char*>(data.data()), flush);
}
void ProviderUdpSSL::writeBytes(unsigned int size, const uint8_t* data, bool flush)
{
if (!_streamReady || _streamPaused)

View File

@ -100,6 +100,14 @@ protected:
///
void stopConnection();
///
/// Writes the given bytes/bits to the UDP-device and sleeps the latch time to ensure that the
/// values are latched.
///
/// @param[in] data The data
///
void writeBytes(QByteArray data, bool flush = false);
///
/// Writes the given bytes/bits to the UDP-device and sleeps the latch time to ensure that the
/// values are latched.
@ -116,6 +124,8 @@ protected:
///
virtual const int * getCiphersuites() const;
void setPSKidentity(const QString& pskIdentity);
private:
bool initConnection();

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

View File

@ -35,26 +35,45 @@
},
"propertyOrder": 4
},
"useAPIv2": {
"type": "boolean",
"format": "checkbox",
"title": "edt_dev_spec_useAPIv2_title",
"default": false,
"options": {
"hidden": true
},
"access": "expert",
"propertyOrder": 5
},
"useEntertainmentAPI": {
"type": "boolean",
"format": "checkbox",
"title": "edt_dev_spec_useEntertainmentAPI_title",
"default": true,
"propertyOrder": 5
"options": {
"hidden": true
},
"propertyOrder": 6
},
"switchOffOnBlack": {
"type": "boolean",
"format": "checkbox",
"title": "edt_dev_spec_switchOffOnBlack_title",
"default": false,
"propertyOrder": 6
"options": {
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 7
},
"restoreOriginalState": {
"type": "boolean",
"format": "checkbox",
"title": "edt_dev_spec_restoreOriginalState_title",
"default": false,
"propertyOrder": 7
"propertyOrder": 8
},
"blackLevel": {
"type": "number",
@ -64,7 +83,12 @@
"step": 0.01,
"minimum": 0.001,
"maximum": 1.0,
"propertyOrder": 8
"options": {
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 9
},
"onBlackTimeToPowerOff": {
"type": "integer",
@ -76,7 +100,12 @@
"maximum": 100000,
"default": 600,
"required": true,
"propertyOrder": 9
"options": {
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 10
},
"onBlackTimeToPowerOn": {
"type": "integer",
@ -88,14 +117,24 @@
"maximum": 100000,
"default": 300,
"required": true,
"propertyOrder": 9
"options": {
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 11
},
"candyGamma": {
"type": "boolean",
"format": "checkbox",
"title": "edt_dev_spec_candyGamma_title",
"default": true,
"propertyOrder": 10
"options": {
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 12
},
"lightIds": {
"type": "array",
@ -112,20 +151,23 @@
"useEntertainmentAPI": false
}
},
"propertyOrder": 11
"propertyOrder": 13
},
"groupId": {
"type": "number",
"format": "stepper",
"step": 1,
"type": "string",
"title": "edt_dev_spec_groupId_title",
"default": 0,
"default": "",
"options": {
"dependencies": {
"useEntertainmentAPI": true
}
},
"propertyOrder": 12
"options": {
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 14
},
"brightnessFactor": {
"type": "number",
@ -136,7 +178,12 @@
"minimum": 0.5,
"maximum": 10.0,
"access": "advanced",
"propertyOrder": 13
"options": {
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 15
},
"handshakeTimeoutMin": {
"type": "number",
@ -154,7 +201,7 @@
"useEntertainmentAPI": true
}
},
"propertyOrder": 14
"propertyOrder": 16
},
"handshakeTimeoutMax": {
"type": "number",
@ -172,7 +219,7 @@
"useEntertainmentAPI": true
}
},
"propertyOrder": 15
"propertyOrder": 17
},
"verbose": {
"type": "boolean",
@ -180,7 +227,7 @@
"title": "edt_dev_spec_verbose_title",
"default": false,
"access": "expert",
"propertyOrder": 16
"propertyOrder": 18
},
"transitiontime": {
"type": "number",
@ -195,24 +242,29 @@
"useEntertainmentAPI": false
}
},
"propertyOrder": 17
"propertyOrder": 19
},
"blackLightsTimeout": {
"type": "number",
"title": "edt_dev_spec_blackLightsTimeout_title",
"default": 5000,
"options": {
"hidden": true
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 18
"propertyOrder": 20
},
"brightnessThreshold": {
"type": "number",
"title": "edt_dev_spec_brightnessThreshold_title",
"default": 0.0001,
"options": {
"hidden": true
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 19
"propertyOrder": 21
},
"brightnessMin": {
"type": "number",
@ -223,9 +275,11 @@
"maximum": 1.0,
"access": "advanced",
"options": {
"hidden": true
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 20
"propertyOrder": 22
},
"brightnessMax": {
"type": "number",
@ -236,9 +290,11 @@
"maximum": 1.0,
"access": "advanced",
"options": {
"hidden": true
"dependencies": {
"useAPIv2": false
}
},
"propertyOrder": 21
"propertyOrder": 23
}
},
"additionalProperties": true

View File

@ -182,6 +182,7 @@ bool MdnsBrowser::resolveAddress(Logger* log, const QString& hostname, QHostAddr
}
else
{
QObject::disconnect(&MdnsBrowser::getInstance(), &MdnsBrowser::addressResolved, nullptr, nullptr);
Error(log, "Resolved mDNS hostname [%s] timed out", QSTRING_CSTR(hostname));
}
}

View File

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICMjCCAdigAwIBAgIUO7FSLbaxikuXAljzVaurLXWmFw4wCgYIKoZIzj0EAwIw
OTELMAkGA1UEBhMCTkwxFDASBgNVBAoMC1BoaWxpcHMgSHVlMRQwEgYDVQQDDAty
b290LWJyaWRnZTAiGA8yMDE3MDEwMTAwMDAwMFoYDzIwMzgwMTE5MDMxNDA3WjA5
MQswCQYDVQQGEwJOTDEUMBIGA1UECgwLUGhpbGlwcyBIdWUxFDASBgNVBAMMC3Jv
b3QtYnJpZGdlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjNw2tx2AplOf9x86
aTdvEcL1FU65QDxziKvBpW9XXSIcibAeQiKxegpq8Exbr9v6LBnYbna2VcaK0G22
jOKkTqOBuTCBtjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNV
HQ4EFgQUZ2ONTFrDT6o8ItRnKfqWKnHFGmQwdAYDVR0jBG0wa4AUZ2ONTFrDT6o8
ItRnKfqWKnHFGmShPaQ7MDkxCzAJBgNVBAYTAk5MMRQwEgYDVQQKDAtQaGlsaXBz
IEh1ZTEUMBIGA1UEAwwLcm9vdC1icmlkZ2WCFDuxUi22sYpLlwJY81Wrqy11phcO
MAoGCCqGSM49BAMCA0gAMEUCIEBYYEOsa07TH7E5MJnGw557lVkORgit2Rm1h3B2
sFgDAiEA1Fj/C3AN5psFMjo0//mrQebo0eKd3aWRx+pQY08mk48=
-----END CERTIFICATE-----