diff --git a/assets/webconfig/content/conf_leds.html b/assets/webconfig/content/conf_leds.html index e7ff559f..3229c206 100755 --- a/assets/webconfig/content/conf_leds.html +++ b/assets/webconfig/content/conf_leds.html @@ -322,6 +322,17 @@ + + + + + + + + diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index bb3453b8..445eb0fb 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -58,11 +58,13 @@ "conf_leds_contr_label_contrtype": "Controller type:", "conf_leds_device_info_log": "In case your LEDs do not work, check here for errors:", "conf_leds_device_intro": "Hyperion supports a lot of controllers to transmit data to your target device. Select a LED controller out of the sorted list and configure it. We have chosen the best default settings for each device.", - "conf_leds_error_get_properties_text" : "Failed to get the device's properties. Please check the configuration items.", - "conf_leds_error_get_properties_title" : "Device properties", + "conf_leds_error_get_properties_text": "Failed to get the device's properties. Please check the configuration items.", + "conf_leds_error_get_properties_title": "Device properties", "conf_leds_error_hwled_gt_layout": "The hardware LED count ($1) is greater than LEDs configured via layout ($2),
$3 {{plural:$3|LED|LEDs}} will stay black if you continue.", "conf_leds_error_hwled_lt_layout": "The hardware LED count ($1) is less than LEDs configured via layout ($2).
The number of LEDs configured in the layout must not exceed the available LEDs", "conf_leds_error_hwled_gt_maxled": "The hardware LED count ($1) is greater than the maximum number of LEDs supported by the device ($2).
The hardware LED count is set to ($3).", + "conf_leds_error_hwled_gt_maxled_protocol": "The hardware LED count ($1) is greater than the maximum number of LEDs supported by the streaming protocol ($2).
The streaming protocol will be changed to ($3).", + "conf_leds_error_wled_segment_missing": "The currently configured segment ($1) is not configured at your WLED device.
You might need to check the WLED configuration!
The configuration page represents the current WLED setup.", "conf_leds_info_ws281x": "Hyperion must run with 'root' privileges for this controller type!", "conf_leds_layout_advanced": "Advanced Settings", "conf_leds_layout_blacklist_num_title": "Number of LEDs", @@ -558,7 +560,7 @@ "edt_dev_spec_brightnessOverwrite_title": "Overwrite brightness", "edt_dev_spec_brightnessThreshold_title": "Signal detection brightness minimum", "edt_dev_spec_brightness_title": "Brightness", - "edt_dev_spec_candyGamma_title" : "'Candy' mode (double gamma correction)", + "edt_dev_spec_candyGamma_title": "'Candy' mode (double gamma correction)", "edt_dev_spec_chanperfixture_title": "Channels per Fixture", "edt_dev_spec_cid_title": "CID", "edt_dev_spec_clientKey_title": "Clientkey", @@ -615,15 +617,22 @@ "edt_dev_spec_razer_device_title": "Razer Chroma Device", "edt_dev_spec_restoreOriginalState_title": "Restore lights' state", "edt_dev_spec_restoreOriginalState_title_info": "Restore the device's original state when device is disabled", - "edt_dev_spec_rgbw_calibration_enable" : "White channel calibration (RGBW only)", - "edt_dev_spec_rgbw_calibration_limit" : "White channel limit", - "edt_dev_spec_rgbw_calibration_red" : "Red/White channel aspect", - "edt_dev_spec_rgbw_calibration_green" : "Green/White channel aspect", - "edt_dev_spec_rgbw_calibration_blue" : "Blue/White channel aspect", + "edt_dev_spec_rgbw_calibration_enable": "White channel calibration (RGBW only)", + "edt_dev_spec_rgbw_calibration_limit": "White channel limit", + "edt_dev_spec_rgbw_calibration_red": "Red/White channel aspect", + "edt_dev_spec_rgbw_calibration_green": "Green/White channel aspect", + "edt_dev_spec_rgbw_calibration_blue": "Blue/White channel aspect", + "edt_dev_spec_segments_disabled_title": "Segment streaming disabled at WLED.", + "edt_dev_spec_segments_title": "Stream to segment", + "edt_dev_spec_segmentId_title": "Segment-ID", + "edt_dev_spec_segmentsSwitchOffOthers_title": "Switch-off other segments", + "edt_dev_spec_segmentsOverlapValidation_error": "Correct the WLED setup! The segment must not overlap with {{plural:$1| segment|segments}}: \"$2\".", "edt_dev_spec_serial_title": "Serial number", "edt_dev_spec_spipath_title": "SPI Device", "edt_dev_spec_sslHSTimeoutMax_title": "Streamer handshake timeout maximum", "edt_dev_spec_sslHSTimeoutMin_title": "Streamer handshake timeout minimum", + "edt_dev_spec_stayOnAfterStreaming_title": "Stay on after streaming", + "edt_dev_spec_stayOnAfterStreaming_title_info": "The device will stay on after streaming or restoring state.", "edt_dev_spec_switchOffOnBlack_title": "Switch off on black", "edt_dev_spec_switchOffOnbelowMinBrightness_title": "Switch-off, below minimum", "edt_dev_spec_syncOverwrite_title": "Disable synchronisation", @@ -866,11 +875,11 @@ "general_country_us": "United States", "general_disabled": "disabled", "general_enabled": "enabled", - "general_speech_ca": "Catalan", + "general_speech_ca": "Catalan", "general_speech_cs": "Czech", "general_speech_da": "Danish", "general_speech_de": "German", - "general_speech_el": "Greek", + "general_speech_el": "Greek", "general_speech_en": "English", "general_speech_es": "Spanish", "general_speech_fr": "French", diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 26ef9529..6421d6de 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -1002,6 +1002,21 @@ $(document).ready(function () { addJsonEditorHostValidation(); + JSONEditor.defaults.custom_validators.push(function (schema, value, path) { + var errors = []; + + if (path === "root.specificOptions.segments.segmentList") { + var overlapSegNames = validateWledSegmentConfig(value); + if (overlapSegNames.length > 0) { + errors.push({ + path: "root.specificOptions.segments", + message: $.i18n('edt_dev_spec_segmentsOverlapValidation_error', overlapSegNames.length, overlapSegNames.join(', ')) + }); + } + } + return errors; + }); + $("#leddevices").off().on("change", function () { var generalOptions = window.serverSchema.properties.device; @@ -1080,8 +1095,8 @@ $(document).ready(function () { $('#btn_test_controller').hide(); switch (ledType) { - case "cololight": case "wled": + case "cololight": case "nanoleaf": showAllDeviceInputOptions("hostList", false); case "apa102": @@ -1107,7 +1122,22 @@ $(document).ready(function () { if (storedAccess === 'expert') { filter.discoverAll = true; } - discover_device(ledType, filter); + + $('#btn_submit_controller').prop('disabled', true); + + discover_device(ledType, filter) + .then(discoveryResult => { + updateOutputSelectList(ledType, discoveryResult); + }) + .then(discoveryResult => { + if (ledType === "wled") { + updateElementsWled(ledType); + } + }) + .catch(error => { + showNotification('danger', "Device discovery for " + ledType + " failed with error:" + error); + }); + hwLedCountDefault = 1; colorOrderDefault = "rgb"; break; @@ -1211,8 +1241,8 @@ $(document).ready(function () { } break; - case "cololight": case "wled": + case "cololight": var hostList = conf_editor.getEditor("root.specificOptions.hostList").getValue(); if (hostList !== "SELECT") { var host = conf_editor.getEditor("root.specificOptions.host").getValue(); @@ -1339,7 +1369,9 @@ $(document).ready(function () { break; case "wled": - params = { host: host, filter: "info" }; + //Ensure that elements are defaulted for new host + updateElementsWled(ledType, host); + params = { host: host }; getProperties_device(ledType, host, params); break; @@ -1452,6 +1484,10 @@ $(document).ready(function () { } conf_editor.getEditor("root.specificOptions.rateList").setValue(rate); break; + case "wled": + var hardwareLedCount = conf_editor.getEditor("root.generalOptions.hardwareLedCount").getValue(); + validateWledLedCount(hardwareLedCount); + break; default: } }); @@ -1547,12 +1583,54 @@ $(document).ready(function () { } }); + //WLED + conf_editor.watch('root.specificOptions.segments.segmentList', () => { + + //Update hidden streamSegmentId element + var selectedSegment = conf_editor.getEditor("root.specificOptions.segments.segmentList").getValue(); + var streamSegmentId = parseInt(selectedSegment); + conf_editor.getEditor("root.specificOptions.segments.streamSegmentId").setValue(streamSegmentId); + + if (devicesProperties[ledType]) { + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + var ledDeviceProperties = devicesProperties[ledType][host]; + + if (ledDeviceProperties) { + var hardwareLedCount = 1; + if (streamSegmentId > -1) { + // Set hardware LED count to segment length + if (ledDeviceProperties.state) { + var segments = ledDeviceProperties.state.seg; + var segmentConfig = segments.filter(seg => seg.id == streamSegmentId)[0]; + hardwareLedCount = segmentConfig.len; + } + } else { + //"Use main segment only" is disabled, i.e. stream to all LEDs + if (ledDeviceProperties.info) { + hardwareLedCount = ledDeviceProperties.info.leds.count; + } + } + conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + } + } + }); + //Handle Hardware Led Count constraint list conf_editor.watch('root.generalOptions.hardwareLedCountList', () => { var hwLedCountSelected = conf_editor.getEditor("root.generalOptions.hardwareLedCountList").getValue(); conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(Number(hwLedCountSelected)); }); + //Handle Hardware Led update and constraints + conf_editor.watch('root.generalOptions.hardwareLedCount', () => { + var hardwareLedCount = conf_editor.getEditor("root.generalOptions.hardwareLedCount").getValue(); + switch (ledType) { + case "wled": + validateWledLedCount(hardwareLedCount); + break; + default: + } + }); }); //philipshueentertainment backward fix @@ -1798,8 +1876,8 @@ function saveLedConfig(genDefLayout = false) { location.reload(); } -// build dynamic enum -var updateSelectList = function (ledType, discoveryInfo) { +// build dynamic enum for hosts or output paths +var updateOutputSelectList = function (ledType, discoveryInfo) { // Only update, if ledType is equal of selected controller type and discovery info exists if (ledType !== $("#leddevices").val() || !discoveryInfo.devices) { return; @@ -1810,7 +1888,7 @@ var updateSelectList = function (ledType, discoveryInfo) { var key; var enumVals = []; - var enumTitelVals = []; + var enumTitleVals = []; var enumDefaultVal = ""; var addSelect = false; var addCustom = false; @@ -1835,7 +1913,7 @@ var updateSelectList = function (ledType, discoveryInfo) { if (discoveryInfo.devices.length === 0) { enumVals.push("NONE"); - enumTitelVals.push($.i18n('edt_dev_spec_devices_discovered_none')); + enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); } else { var name; @@ -1876,7 +1954,7 @@ var updateSelectList = function (ledType, discoveryInfo) { } enumVals.push(host); - enumTitelVals.push(name); + enumTitleVals.push(name); } //Always allow to add custom configuration @@ -1904,7 +1982,7 @@ var updateSelectList = function (ledType, discoveryInfo) { if (discoveryInfo.devices.length == 0) { enumVals.push("NONE"); - enumTitelVals.push($.i18n('edt_dev_spec_devices_discovered_none')); + enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); $('#btn_submit_controller').prop('disabled', true); showAllDeviceInputOptions(key, false); } @@ -1922,7 +2000,7 @@ var updateSelectList = function (ledType, discoveryInfo) { } else { enumVals.push(device.portName); } - enumTitelVals.push(device.portName + " (" + device.vendorIdentifier + "|" + device.productIdentifier + ") - " + device.manufacturer); + enumTitleVals.push(device.portName + " (" + device.vendorIdentifier + "|" + device.productIdentifier + ") - " + device.manufacturer); } // Select configured device @@ -1951,7 +2029,7 @@ var updateSelectList = function (ledType, discoveryInfo) { if (discoveryInfo.devices.length == 0) { enumVals.push("NONE"); - enumTitelVals.push($.i18n('edt_dev_spec_devices_discovered_none')); + enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); $('#btn_submit_controller').prop('disabled', true); showAllDeviceInputOptions(key, false); } @@ -1970,7 +2048,7 @@ var updateSelectList = function (ledType, discoveryInfo) { case "piblaster": for (const device of discoveryInfo.devices) { enumVals.push(device.systemLocation); - enumTitelVals.push(device.deviceName + " (" + device.systemLocation + ")"); + enumTitleVals.push(device.deviceName + " (" + device.systemLocation + ")"); } // Select configured device @@ -1993,7 +2071,7 @@ var updateSelectList = function (ledType, discoveryInfo) { if (discoveryInfo.devices.length == 0) { enumVals.push("NONE"); - enumTitelVals.push($.i18n('edt_dev_spec_devices_discovered_none')); + enumTitleVals.push($.i18n('edt_dev_spec_devices_discovered_none')); $('#btn_submit_controller').prop('disabled', true); showAllDeviceInputOptions(key, false); @@ -2004,18 +2082,19 @@ var updateSelectList = function (ledType, discoveryInfo) { } if (enumVals.length > 0) { - updateJsonEditorSelection(conf_editor, 'root.specificOptions', key, addSchemaElements, enumVals, enumTitelVals, enumDefaultVal, addSelect, addCustom); + updateJsonEditorSelection(conf_editor, 'root.specificOptions', key, addSchemaElements, enumVals, enumTitleVals, enumDefaultVal, addSelect, addCustom); } }; async function discover_device(ledType, params) { - $('#btn_submit_controller').prop('disabled', true); - const result = await requestLedDeviceDiscovery(ledType, params); - var discoveryResult; - if (result && !result.error) { + var discoveryResult = {}; + if (result) { + if (result.error) { + throw (result.error); + } discoveryResult = result.info; } else { @@ -2024,8 +2103,7 @@ async function discover_device(ledType, params) { ledDevicetype: ledType } } - - updateSelectList(ledType, discoveryResult); + return discoveryResult; } async function getProperties_device(ledType, key, params) { @@ -2089,23 +2167,7 @@ function updateElements(ledType, key) { conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); break; case "wled": - var ledProperties = devicesProperties[ledType][key]; - - if (ledProperties && ledProperties.leds) { - hardwareLedCount = ledProperties.leds.count; - if (ledProperties.maxLedCount) { - var maxLedCount = ledProperties.maxLedCount; - if (hardwareLedCount > maxLedCount) { - showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_hwled_gt_maxled', hardwareLedCount, maxLedCount, maxLedCount)); - hardwareLedCount = maxLedCount; - conf_editor.getEditor("root.specificOptions.streamProtocol").setValue("RAW"); - //Workaround, as value seems to getting updated property when a 'getEditor("root.specificOptions").getValue()' is done during save - var editor = conf_editor.getEditor("root.specificOptions"); - editor.value["streamProtocol"] = "RAW"; - } - } - } - conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + updateElementsWled(ledType, key); break; case "nanoleaf": @@ -2190,3 +2252,162 @@ function disableAutoResolvedGeneralOptions() { conf_editor.getEditor("root.generalOptions.colorOrder").disable(); } +function validateWledSegmentConfig(streamSegmentId) { + var overlapSegNames = []; + if (streamSegmentId > -1) { + if (!jQuery.isEmptyObject(devicesProperties)) { + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + var ledProperties = devicesProperties['wled'][host]; + if (ledProperties && ledProperties.state) { + var segments = ledProperties.state.seg; + var segmentConfig = segments.filter(seg => seg.id == streamSegmentId)[0]; + + var overlappingSegments = segments.filter((seg) => { + if (seg.id != streamSegmentId) { + if ((segmentConfig.start >= seg.stop) || (segmentConfig.start < seg.start && segmentConfig.stop <= seg.start)) { + return false; + } + return true; + } + }); + + if (overlappingSegments.length > 0) { + var overlapSegNames = []; + for (const segment of overlappingSegments) { + if (segment.n) { + overlapSegNames.push(segment.n); + } else { + overlapSegNames.push("Segment " + segment.id); + } + } + } + } + } + } + return overlapSegNames; +} + +function validateWledLedCount(hardwareLedCount) { + + if (!jQuery.isEmptyObject(devicesProperties)) { + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + var ledDeviceProperties = devicesProperties["wled"][host]; + + if (ledDeviceProperties) { + + var streamProtocol = conf_editor.getEditor("root.specificOptions.streamProtocol").getValue(); + if (streamProtocol === "RAW") { + var maxLedCount = 490; + if (ledDeviceProperties.maxLedCount) { + //WLED not DDP ready + maxLedCount = ledDeviceProperties.maxLedCount; + if (hardwareLedCount > maxLedCount) { + showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_hwled_gt_maxled', hardwareLedCount, maxLedCount, maxLedCount)); + hardwareLedCount = maxLedCount; + conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + conf_editor.getEditor("root.specificOptions.streamProtocol").setValue("RAW"); + } + } else { + //WLED is DDP ready + if (hardwareLedCount > maxLedCount) { + var newStreamingProtocol = "DDP"; + showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_hwled_gt_maxled_protocol', hardwareLedCount, maxLedCount, newStreamingProtocol)); + conf_editor.getEditor("root.specificOptions.streamProtocol").setValue(newStreamingProtocol); + } + } + } + } + } +} + +function updateElementsWled(ledType, key) { + + // Get configured device's details + var configuredDeviceType = window.serverConfig.device.type; + var configuredHost = window.serverConfig.device.host; + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + + //New segment selection list values + var enumSegSelectVals = []; + var enumSegSelectTitleVals = []; + var enumSegSelectDefaultVal = ""; + + if (devicesProperties[ledType] && devicesProperties[ledType][key]) { + var ledDeviceProperties = devicesProperties[ledType][key]; + + if (!jQuery.isEmptyObject(ledDeviceProperties)) { + + if (ledDeviceProperties.info) { + if (ledDeviceProperties.info.liveseg && ledDeviceProperties.info.liveseg < 0) { + // "Use main segment only" is disabled + var defaultSegmentId = "-1"; + enumSegSelectVals.push(defaultSegmentId); + enumSegSelectTitleVals.push($.i18n('edt_dev_spec_segments_disabled_title')); + enumSegSelectDefaultVal = defaultSegmentId; + + } else { + if (ledDeviceProperties.state) { + //Prepare new segment selection list + var segments = ledDeviceProperties.state.seg; + for (const segment of segments) { + enumSegSelectVals.push(segment.id.toString()); + if (segment.n) { + enumSegSelectTitleVals.push(segment.n); + } else { + enumSegSelectTitleVals.push("Segment " + segment.id); + } + } + var currentSegmentId = conf_editor.getEditor("root.specificOptions.segments.streamSegmentId").getValue().toString(); + enumSegSelectDefaultVal = currentSegmentId; + } + } + + // Check if currently configured segment is available at WLED + var configuredDeviceType = window.serverConfig.device.type; + var configuredHost = window.serverConfig.device.host; + + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + if (configuredDeviceType == ledType && configuredHost == host) { + var configuredStreamSegmentId = window.serverConfig.device.segments.streamSegmentId.toString(); + var segmentIdFound = enumSegSelectVals.filter(segId => segId == configuredStreamSegmentId).length; + if (!segmentIdFound) { + showInfoDialog('warning', $.i18n("conf_leds_config_warning"), $.i18n('conf_leds_error_wled_segment_missing', configuredStreamSegmentId)); + } + } + } + } + } else { + //If failed to get properties + var hardwareLedCount; + + if (configuredDeviceType == ledType && configuredHost == host) { + // Populate elements from existing configuration + var configuredstreamSegmentId = window.serverConfig.device.segments.streamSegmentId.toString(); + enumSegSelectVals = [configuredstreamSegmentId]; + enumSegSelectTitleVals = ["Segment " + configuredstreamSegmentId]; + enumSegSelectDefaultVal = configuredstreamSegmentId; + + hardwareLedCount = window.serverConfig.device.hardwareLedCount; + } else { + // Populate elements with default values + defaultSegmentId = "-1"; + enumSegSelectVals.push(defaultSegmentId); + enumSegSelectTitleVals.push($.i18n('edt_dev_spec_segments_disabled_title')); + enumSegSelectDefaultVal = defaultSegmentId; + + hardwareLedCount = 1; + } + conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + } + + updateJsonEditorSelection(conf_editor, 'root.specificOptions.segments', + 'segmentList', {}, enumSegSelectVals, enumSegSelectTitleVals, enumSegSelectDefaultVal, false, false); + + //Show additional configuration options, if more than one segment is available + var showAdditionalOptions = false; + if (enumSegSelectVals.length > 1) { + showAdditionalOptions = true; + } + showInputOptionForItem(conf_editor, "root.specificOptions.segments", "switchOffOtherSegments", showAdditionalOptions); +} + diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 84dff5c5..e1a2ae5b 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -1,1344 +1,1393 @@ -var prevTag; - -function removeOverlay() { - $("#loading_overlay").removeClass("overlay"); -} - -function reload() { - location.reload(); -} - -function storageComp() { - if (typeof (Storage) !== "undefined") - return true; - return false; -} - -function getStorage(item) { - if (storageComp()) { - return localStorage.getItem(item); - } - return null; -} - -function setStorage(item, value) { - if (storageComp()) { - localStorage.setItem(item, value); - } -} - -function removeStorage(item) { - if (storageComp()) { - localStorage.removeItem(item); - } -} - -function debugMessage(msg) { - if (window.debugMessagesActive) { - console.log(msg); - } -} - -function validateDuration(d) { - if (typeof d === "undefined" || d < 0) - return ENDLESS; - else - return d *= 1000; -} - -function getHashtag() { - if (getStorage('lasthashtag') != null) - return getStorage('lasthashtag'); - else { - var tag = document.URL; - tag = tag.substr(tag.indexOf("#") + 1); - if (tag == "" || typeof tag === "undefined" || tag.startsWith("http")) - tag = "dashboard" - return tag; - } -} - -function loadContent(event, forceRefresh) { - var tag; - - var lastSelectedInstance = getStorage('lastSelectedInstance'); - - if (lastSelectedInstance && (lastSelectedInstance != window.currentHyperionInstance)) { - if (window.serverInfo.instance[lastSelectedInstance] && window.serverInfo.instance[lastSelectedInstance].running) { - instanceSwitch(lastSelectedInstance); - } else { - removeStorage('lastSelectedInstance'); - } - } - - if (typeof event != "undefined") { - tag = event.currentTarget.hash; - tag = tag.substr(tag.indexOf("#") + 1); - setStorage('lasthashtag', tag); - } - else - tag = getHashtag(); - - if (forceRefresh || prevTag != tag) { - prevTag = tag; - $("#page-content").off(); - $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { - if (status == "error") { - tag = 'dashboard'; - console.log("Could not find page:", prevTag, ", Redirecting to:", tag); - setStorage('lasthashtag', tag); - - $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { - if (status == "error") { - $("#page-content").html('

' + encode_utf8(tag) + '
' + $.i18n('info_404') + '

'); - removeOverlay(); - } - }); - } - updateUiOnInstance(window.currentHyperionInstance); - }); - } -} - -function getInstanceNameByIndex(index) { - var instData = window.serverInfo.instance - for (var key in instData) { - if (instData[key].instance == index) - return instData[key].friendly_name; - } - return "unknown" -} - -function updateHyperionInstanceListing() { - if (window.serverInfo.instance) { - var data = window.serverInfo.instance.filter(entry => entry.running); - $('#hyp_inst_listing').html(""); - for (var key in data) { - var currInstMarker = (data[key].instance == window.currentHyperionInstance) ? "component-on" : ""; - - var html = '
  • \ - \ -
    \ - \ - '+ data[key].friendly_name + ' \ -
    \ -
    \ -
  • ' - - if (data.length - 1 > key) - html += '
  • ' - - $('#hyp_inst_listing').append(html); - - $('#hyperioninstance_' + data[key].instance).off().on("click", function (e) { - var inst = e.currentTarget.id.split("_")[1] - instanceSwitch(inst) - }); - } - } -} - -function initLanguageSelection() { - // Initialise language selection list with languages supported - for (var i = 0; i < availLang.length; i++) { - $("#language-select").append(''); - } - - var langLocale = storedLang; - - //Test, if language is supported by hyperion - var langIdx = availLang.indexOf(langLocale); - if (langIdx > -1) { - langText = availLangText[langIdx]; - } else { - // If language is not supported by hyperion, try fallback language - langLocale = $.i18n().options.fallbackLocale.substring(0, 2); - langIdx = availLang.indexOf(langLocale); - if (langIdx > -1) { - langText = availLangText[langIdx]; - } else { - langLocale = 'en'; - langIdx = availLang.indexOf(langLocale); - if (langIdx > -1) { - langText = availLangText[langIdx]; - } - } - } - - $('#language-select').prop('title', langText); - $("#language-select").val(langIdx); - $("#language-select").selectpicker("refresh"); -} - -function updateUiOnInstance(inst) { - - window.currentHyperionInstance = inst; - window.currentHyperionInstanceName = getInstanceNameByIndex(inst); - - $("#active_instance_friendly_name").text(getInstanceNameByIndex(inst)); - if (window.serverInfo.instance.filter(entry => entry.running).length > 1) { - $('#btn_hypinstanceswitch').toggle(true); - $('#active_instance_dropdown').prop('disabled', false); - $('#active_instance_dropdown').css('cursor', 'pointer'); - $("#active_instance_dropdown").css("pointer-events", "auto"); - } else { - $('#btn_hypinstanceswitch').toggle(false); - $('#active_instance_dropdown').prop('disabled', true); - $("#active_instance_dropdown").css('cursor', 'default'); - $("#active_instance_dropdown").css("pointer-events", "none"); - } -} - -function instanceSwitch(inst) { - requestInstanceSwitch(inst) - window.currentHyperionInstance = inst; - window.currentHyperionInstanceName = getInstanceNameByIndex(inst); - setStorage('lastSelectedInstance', inst) -} - -function loadContentTo(containerId, fileName) { - $(containerId).load("/content/" + fileName + ".html"); -} - -function toggleClass(obj, class1, class2) { - if ($(obj).hasClass(class1)) { - $(obj).removeClass(class1); - $(obj).addClass(class2); - } - else { - $(obj).removeClass(class2); - $(obj).addClass(class1); - } -} - -function setClassByBool(obj, enable, class1, class2) { - if (enable) { - $(obj).removeClass(class1); - $(obj).addClass(class2); - } - else { - $(obj).removeClass(class2); - $(obj).addClass(class1); - } -} - -function showInfoDialog(type, header, message) { - if (type == "success") { - $('#id_body').html(''); - if (header == "") - $('#id_body').append('

    ' + $.i18n('infoDialog_general_success_title') + '

    '); - $('#id_footer').html(''); - } - else if (type == "warning") { - $('#id_body').html(''); - if (header == "") - $('#id_body').append('

    ' + $.i18n('infoDialog_general_warning_title') + '

    '); - $('#id_footer').html(''); - } - else if (type == "error") { - $('#id_body').html(''); - if (header == "") - $('#id_body').append('

    ' + $.i18n('infoDialog_general_error_title') + '

    '); - $('#id_footer').html(''); - } - else if (type == "select") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "iswitch") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "uilock") { - $('#id_body').html(''); - $('#id_footer').html('' + $.i18n('InfoDialog_nowrite_foottext') + ''); - } - else if (type == "import") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "delInst") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - else if (type == "renInst") { - $('#id_body_rename').html('
    '); - $('#id_body_rename').append('

    ' + header + '

    '); - $('#id_body_rename').append(''); - $('#id_footer_rename').html(''); - $('#id_footer_rename').append(''); - } - else if (type == "changePassword") { - $('#id_body_rename').html('
    '); - $('#id_body_rename').append('

    ' + header + '


    '); - $('#id_body_rename').append('

    ' + $.i18n('infoDialog_username_text') + - '


    '); - $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_current_text') + - '


    '); - $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_new_text') + - '

    '); - $('#id_body_rename').append('
    ' + $.i18n('infoDialog_password_minimum_length') + '
    '); - $('#id_footer_rename').html(''); - $('#id_footer_rename').append(''); - } - else if (type == "checklist") { - $('#id_body').html(''); - $('#id_body').append('

    ' + $.i18n('infoDialog_checklist_title') + '

    '); - $('#id_body').append(header); - $('#id_footer').html(''); - } - else if (type == "newToken") { - $('#id_body').html(''); - $('#id_footer').html(''); - } - else if (type == "grantToken") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); - } - - if (type != "renInst") { - $('#id_body').append('

    ' + header + '

    '); - $('#id_body').append(message); - } - - if (type == "select" || type == "iswitch") - $('#id_body').append(''); - - if (getStorage("darkMode") == "on") - $('#id_logo').attr("src", 'img/hyperion/logo_negativ.png'); - - $(type == "renInst" || type == "changePassword" ? "#modal_dialog_rename" : "#modal_dialog").modal({ - backdrop: "static", - keyboard: false, - show: true - }); - - $(document).on('click', '[data-dismiss-modal]', function () { - var target = $(this).attr('data-dismiss-modal'); - $.find(target).modal('hide'); - }); -} - -function createHintH(type, text, container) { - type = String(type); - if (type == "intro") - tclass = "introd"; - - $('#' + container).prepend('

    ' + text + '


    '); -} - -function createHint(type, text, container, buttonid, buttontxt) { - var fe, tclass; - - if (type == "intro") { - fe = ''; - tclass = "intro-hint"; - } - else if (type == "info") { - fe = '
    Information
    '; - tclass = "info-hint"; - } - else if (type == "wizard") { - fe = '
    Information
    '; - tclass = "wizard-hint"; - } - else if (type == "warning") { - fe = '
    Information
    '; - tclass = "warning-hint"; - } - - if (buttonid) - buttonid = '

    '; - else - buttonid = ""; - - if (type == "intro") - $('#' + container).prepend('

    ' + $.i18n("conf_helptable_expl") + '

    ' + text + '
    '); - else if (type == "wizard") - $('#' + container).prepend('

    ' + $.i18n("wiz_wizavail") + '

    ' + $.i18n('wiz_guideyou', text) + buttonid + '
    '); - else { - createTable('', 'htb', container, true, tclass); - $('#' + container + ' .htb').append(createTableRow([fe, text], false, true)); - } -} - -function createEffHint(title, text) { - return '

    ' + title + '

    ' + text + '
    '; -} - -function valValue(id, value, min, max) { - if (typeof max === 'undefined' || max == "") - max = 999999; - - if (Number(value) > Number(max)) { - $('#' + id).val(max); - showInfoDialog("warning", "", $.i18n('edt_msg_error_maximum_incl', max)); - return max; - } - else if (Number(value) < Number(min)) { - $('#' + id).val(min); - showInfoDialog("warning", "", $.i18n('edt_msg_error_minimum_incl', min)); - return min; - } - return value; -} - -function readImg(input, cb) { - if (input.files && input.files[0]) { - var reader = new FileReader(); - // inject fileName property - reader.fileName = input.files[0].name - - reader.onload = function (e) { - cb(e.target.result, e.target.fileName); - } - reader.readAsDataURL(input.files[0]); - } -} - -function isJsonString(str) { - try { - JSON.parse(str); - } - catch (e) { - return e; - } - return ""; -} - -function createJsonEditor(container, schema, setconfig, usePanel, arrayre) { - $('#' + container).off(); - $('#' + container).html(""); - - if (typeof arrayre === 'undefined') - arrayre = true; - - var editor = new JSONEditor(document.getElementById(container), - { - theme: 'bootstrap3', - iconlib: "fontawesome4", - disable_collapse: 'true', - form_name_root: 'sa', - disable_edit_json: true, - disable_properties: true, - disable_array_reorder: arrayre, - no_additional_properties: true, - disable_array_delete_all_rows: true, - disable_array_delete_last_row: true, - access: storedAccess, - schema: { - title: '', - properties: schema - } - }); - - if (usePanel) { - $('#' + container + ' .well').first().removeClass('well well-sm'); - $('#' + container + ' h4').first().remove(); - $('#' + container + ' .well').first().removeClass('well well-sm'); - } - - if (setconfig) { - for (var key in editor.root.editors) { - editor.getEditor("root." + key).setValue(Object.assign({}, editor.getEditor("root." + key).value, window.serverConfig[key])); - } - } - - return editor; -} - -function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { - var editor = rootEditor.getEditor(path); - var orginalProperties = editor.schema.properties[key]; - - var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; - rootEditor.unwatch(path + "." + key); - - var newSchema = []; - newSchema[key] = - { - "type": "string", - "enum": [], - "required": true, - "options": { "enum_titles": [], "infoText": "" }, - "propertyOrder": 1 - }; - - //Add additional elements to overwrite defaults - for (var item in addElements) { - newSchema[key][item] = addElements[item]; - } - - if (orginalProperties) { - if (orginalProperties["title"]) { - newSchema[key]["title"] = orginalProperties["title"]; - } - - if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { - newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; - } - - if (orginalProperties["propertyOrder"]) { - newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; - } - } - - if (addCustom) { - - if (newTitelVals.length === 0) { - newTitelVals = [...newEnumVals]; - } - - if (!!!customText) { - customText = "edt_conf_enum_custom"; - } - - if (addCustomAsFirst) { - newEnumVals.unshift("CUSTOM"); - newTitelVals.unshift(customText); - } else { - newEnumVals.push("CUSTOM"); - newTitelVals.push(customText); - } - - if (newSchema[key].options.infoText) { - var customInfoText = newSchema[key].options.infoText + "_custom"; - newSchema[key].options.infoText = customInfoText; - } - } - - if (addSelect) { - newEnumVals.unshift("SELECT"); - newTitelVals.unshift("edt_conf_enum_please_select"); - newDefaultVal = "SELECT"; - } - - if (newEnumVals) { - newSchema[key]["enum"] = newEnumVals; - } - - if (newTitelVals) { - newSchema[key]["options"]["enum_titles"] = newTitelVals; - } - if (newDefaultVal) { - newSchema[key]["default"] = newDefaultVal; - } - - editor.original_schema.properties[key] = orginalProperties; - editor.schema.properties[key] = newSchema[key]; - rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; - - editor.removeObjectProperty(key); - delete editor.cached_editors[key]; - editor.addObjectProperty(key); - - if (orginalWatchFunctions) { - for (var i = 0; i < orginalWatchFunctions.length; i++) { - rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); - } - } - rootEditor.notifyWatchers(path + "." + key); -} - -function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal) { - var editor = rootEditor.getEditor(path); - var orginalProperties = editor.schema.properties[key]; - - var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; - rootEditor.unwatch(path + "." + key); - - var newSchema = []; - newSchema[key] = - { - "type": "array", - "format": "select", - "items": { - "type": "string", - "enum": [], - "options": { "enum_titles": [] }, - }, - "options": { "infoText": "" }, - "default": [], - "propertyOrder": 1 - }; - - //Add additional elements to overwrite defaults - for (var item in addElements) { - newSchema[key][item] = addElements[item]; - } - - if (orginalProperties) { - if (orginalProperties["title"]) { - newSchema[key]["title"] = orginalProperties["title"]; - } - - if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { - newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; - } - - if (orginalProperties["propertyOrder"]) { - newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; - } - } - - if (newEnumVals) { - newSchema[key]["items"]["enum"] = newEnumVals; - } - - if (newTitelVals) { - newSchema[key]["items"]["options"]["enum_titles"] = newTitelVals; - } - - if (newDefaultVal) { - newSchema[key]["default"] = newDefaultVal; - } - - editor.original_schema.properties[key] = orginalProperties; - editor.schema.properties[key] = newSchema[key]; - rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; - - editor.removeObjectProperty(key); - delete editor.cached_editors[key]; - editor.addObjectProperty(key); - - if (orginalWatchFunctions) { - for (var i = 0; i < orginalWatchFunctions.length; i++) { - rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); - } - } - rootEditor.notifyWatchers(path + "." + key); -} - -function updateJsonEditorRange(rootEditor, path, key, minimum, maximum, defaultValue, step, clear) { - var editor = rootEditor.getEditor(path); - - //Preserve current value when updating range - var currentValue = rootEditor.getEditor(path + "." + key).getValue(); - - var orginalProperties = editor.schema.properties[key]; - var newSchema = []; - newSchema[key] = orginalProperties; - - if (clear) { - delete newSchema[key]["minimum"]; - delete newSchema[key]["maximum"]; - delete newSchema[key]["default"]; - delete newSchema[key]["step"]; - } - - if (typeof minimum !== "undefined") { - newSchema[key]["minimum"] = minimum; - } - if (typeof maximum !== "undefined") { - newSchema[key]["maximum"] = maximum; - } - if (typeof defaultValue !== "undefined") { - newSchema[key]["default"] = defaultValue; - currentValue = defaultValue; - } - - if (typeof step !== "undefined") { - newSchema[key]["step"] = step; - } - - editor.original_schema.properties[key] = orginalProperties; - editor.schema.properties[key] = newSchema[key]; - rootEditor.validator.schema.properties[editor.key].properties[key] = newSchema[key]; - - editor.removeObjectProperty(key); - delete editor.cached_editors[key]; - editor.addObjectProperty(key); - - // Restore current (new default) value for new range - rootEditor.getEditor(path + "." + key).setValue(currentValue); -} - -function addJsonEditorHostValidation() { - - JSONEditor.defaults.custom_validators.push(function (schema, value, path) { - var errors = []; - - if (!jQuery.isEmptyObject(value)) { - switch (schema.format) { - case "hostname_or_ip": - if (!isValidHostnameOrIP(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msgcust_error_hostname_ip') - }); - } - break; - case "hostname_or_ip4": - if (!isValidHostnameOrIP4(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msgcust_error_hostname_ip4') - }); - } - break; - - //Remove, when new json-editor 2.x is used - case "ipv4": - if (!isValidIPv4(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msg_error_ipv4') - }); - } - break; - case "ipv6": - if (!isValidIPv6(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msg_error_ipv6') - }); - } - break; - case "hostname": - if (!isValidHostname(value)) { - errors.push({ - path: path, - property: 'format', - message: $.i18n('edt_msg_error_hostname') - }); - } - break; - - default: - } - } - return errors; - }); -} - -function buildWL(link, linkt, cl) { - var baseLink = "https://docs.hyperion-project.org/"; - var lang; - - if (typeof linkt == "undefined") - linkt = "Placeholder"; - - if (storedLang == "de" || navigator.locale == "de") - lang = "de"; - else - lang = "en"; - - if (cl === true) { - linkt = $.i18n(linkt); - return '

    ' + linkt + '

    ' + $.i18n('general_wiki_moreto', linkt) + ': ' + linkt + '
    ' - } - else - return ': ' + linkt + ''; -} - -function rgbToHex(rgb) { - if (rgb.length == 3) { - return "#" + - ("0" + parseInt(rgb[0], 10).toString(16)).slice(-2) + - ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + - ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2); - } - else - debugMessage('rgbToHex: Given rgb is no array or has wrong length'); -} - -function hexToRgb(hex) { - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : { - r: 0, - g: 0, - b: 0 - }; -} - -/* - Show a notification - @param type Valid types are "info","success","warning","danger" - @param message The message to show - @param title A title (optional) - @param addhtml Add custom html to the notification end - */ -function showNotification(type, message, title = "", addhtml = "") { - if (title == "") { - switch (type) { - case "info": - title = $.i18n('infoDialog_general_info_title'); - break; - case "success": - title = $.i18n('infoDialog_general_success_title'); - break; - case "warning": - title = $.i18n('infoDialog_general_warning_title'); - break; - case "danger": - title = $.i18n('infoDialog_general_error_title'); - break; - } - } - - $.notify({ - // options - title: title, - message: message - }, { - // settings - type: type, - animate: { - enter: 'animate__animated animate__fadeInDown', - exit: 'animate__animated animate__fadeOutUp' - }, - placement: { - align: 'center' - }, - mouse_over: 'pause', - template: '' - }); -} - -function createCP(id, color, cb) { - if (Array.isArray(color)) - color = rgbToHex(color); - else if (color == "undefined") - color = "#AA3399"; - - if (color.startsWith("#")) { - $('#' + id).colorpicker({ - format: 'rgb', - customClass: 'colorpicker-2x', - color: color, - sliders: { - saturation: { - maxLeft: 200, - maxTop: 200 - }, - hue: { - maxTop: 200 - }, - } - }); - $('#' + id).colorpicker().on('changeColor', function (e) { - var rgb = e.color.toRGB(); - var hex = e.color.toHex(); - cb(rgb, hex, e); - }); - } - else - debugMessage('createCP: Given color is not legit'); -} - -// Creates a table with thead and tbody ids -// @param string hid : a class for thead -// @param string bid : a class for tbody -// @param string cont : a container id to html() the table -// @param string bless: if true the table is borderless -function createTable(hid, bid, cont, bless, tclass) { - var table = document.createElement('table'); - var thead = document.createElement('thead'); - var tbody = document.createElement('tbody'); - - table.className = "table"; - if (bless === true) - table.className += " borderless"; - if (typeof tclass !== "undefined") - table.className += " " + tclass; - table.style.marginBottom = "0px"; - if (hid != "") - thead.className = hid; - tbody.className = bid; - if (hid != "") - table.appendChild(thead); - table.appendChild(tbody); - - $('#' + cont).append(table); -} - -// Creates a table row -// @param array list :innerHTML content for / -// @param bool head :if null or false it's body -// @param bool align :if null or false no alignment -// -// @return : with or as child(s) -function createTableRow(list, head, align) { - var row = document.createElement('tr'); - - for (var i = 0; i < list.length; i++) { - if (head === true) - var el = document.createElement('th'); - else - var el = document.createElement('td'); - - if (align) - el.style.verticalAlign = "middle"; - - var purifyConfig = { - ADD_TAGS: ['button'], - ADD_ATTR: ['onclick'] - }; - el.innerHTML = DOMPurify.sanitize(list[i], purifyConfig); - row.appendChild(el); - } - return row; -} - -function createRow(id) { - var el = document.createElement('div'); - el.className = "row"; - el.setAttribute('id', id); - return el; -} - -function createOptPanel(phicon, phead, bodyid, footerid, css, panelId) { - phead = '' + phead; - - var pfooter = document.createElement('button'); - pfooter.className = "btn btn-primary"; - pfooter.setAttribute("id", footerid); - pfooter.innerHTML = '' + $.i18n('general_button_savesettings'); - - return createPanel(phead, "", pfooter, "panel-default", bodyid, css, panelId); -} - -function compareTwoValues(key1, key2, order = 'asc') { - return function innerSort(a, b) { - if (!a.hasOwnProperty(key1) || !b.hasOwnProperty(key1)) { - // property key1 doesn't exist on either object - return 0; - } - - const varA1 = (typeof a[key1] === 'string') - ? a[key1].toUpperCase() : a[key1]; - const varB1 = (typeof b[key1] === 'string') - ? b[key1].toUpperCase() : b[key1]; - - let comparison = 0; - if (varA1 > varB1) { - comparison = 1; - } else { - if (varA1 < varB1) { - comparison = -1; - } else { - if (!a.hasOwnProperty(key2) || !b.hasOwnProperty(key2)) { - // property key2 doesn't exist on either object - return 0; - } - - const varA2 = (typeof a[key2] === 'string') - ? a[key2].toUpperCase() : a[key2]; - const varB2 = (typeof b[key1] === 'string') - ? b[key2].toUpperCase() : b[key2]; - - if (varA2 > varB2) { - comparison = 1; - } else { - comparison = -1; - } - } - } - return ( - (order === 'desc') ? (comparison * -1) : comparison - ); - }; -} - -function sortProperties(list) { - for (var key in list) { - list[key].key = key; - } - list = $.map(list, function (value, index) { - return [value]; - }); - return list.sort(function (a, b) { - return a.propertyOrder - b.propertyOrder; - }); -} - -function createHelpTable(list, phead, panelId) { - var table = document.createElement('table'); - var thead = document.createElement('thead'); - var tbody = document.createElement('tbody'); - list = sortProperties(list); - - phead = '' + phead + ' ' + $.i18n("conf_helptable_expl"); - - table.className = 'table table-hover borderless'; - - thead.appendChild(createTableRow([$.i18n('conf_helptable_option'), $.i18n('conf_helptable_expl')], true, false)); - - for (var key in list) { - if (list[key].access != 'system') { - // break one iteration (in the loop), if the schema has the entry hidden=true - if ("options" in list[key] && "hidden" in list[key].options && (list[key].options.hidden)) - continue; - if ("access" in list[key] && ((list[key].access == "advanced" && storedAccess == "default") || (list[key].access == "expert" && storedAccess != "expert"))) - continue; - var text = list[key].title.replace('title', 'expl'); - tbody.appendChild(createTableRow([$.i18n(list[key].title), $.i18n(text)], false, false)); - - if (list[key].items && list[key].items.properties) { - var ilist = sortProperties(list[key].items.properties); - for (var ikey in ilist) { - // break one iteration (in the loop), if the schema has the entry hidden=true - if ("options" in ilist[ikey] && "hidden" in ilist[ikey].options && (ilist[ikey].options.hidden)) - continue; - if ("access" in ilist[ikey] && ((ilist[ikey].access == "advanced" && storedAccess == "default") || (ilist[ikey].access == "expert" && storedAccess != "expert"))) - continue; - var itext = ilist[ikey].title.replace('title', 'expl'); - tbody.appendChild(createTableRow([$.i18n(ilist[ikey].title), $.i18n(itext)], false, false)); - } - } - } - } - table.appendChild(thead); - table.appendChild(tbody); - - return createPanel(phead, table, undefined, undefined, undefined, undefined, panelId); -} - -function createPanel(head, body, footer, type, bodyid, css, panelId) { - var cont = document.createElement('div'); - var p = document.createElement('div'); - var phead = document.createElement('div'); - var pbody = document.createElement('div'); - var pfooter = document.createElement('div'); - - cont.className = "col-lg-6"; - - if (typeof type == 'undefined') - type = 'panel-default'; - - p.className = 'panel ' + type; - if (typeof panelId != 'undefined') { - p.setAttribute("id", panelId); - } - - phead.className = 'panel-heading ' + css; - pbody.className = 'panel-body'; - pfooter.className = 'panel-footer'; - - phead.innerHTML = head; - - if (typeof bodyid != 'undefined') { - pfooter.style.textAlign = 'right'; - pbody.setAttribute("id", bodyid); - } - - if (typeof body != 'undefined' && body != "") - pbody.appendChild(body); - - if (typeof footer != 'undefined') - pfooter.appendChild(footer); - - p.appendChild(phead); - p.appendChild(pbody); - - if (typeof footer != 'undefined') { - pfooter.style.textAlign = "right"; - p.appendChild(pfooter); - } - - cont.appendChild(p); - - return cont; -} - -function createSelGroup(group) { - var el = document.createElement('optgroup'); - el.setAttribute('label', group); - return el; -} - -function createSelOpt(opt, title) { - var el = document.createElement('option'); - el.setAttribute('value', opt); - if (typeof title == 'undefined') - el.innerHTML = opt; - else - el.innerHTML = title; - return el; -} - -function createSel(array, group, split) { - if (array.length != 0) { - var el = createSelGroup(group); - for (var i = 0; i < array.length; i++) { - var opt; - if (split) { - opt = array[i].split(":") - opt = createSelOpt(opt[0], opt[1]) - } - else - opt = createSelOpt(array[i]) - el.appendChild(opt); - } - return el; - } -} - -function performTranslation() { - $('[data-i18n]').i18n(); -} - -function encode_utf8(s) { - return unescape(encodeURIComponent(s)); -} - -function getReleases(callback) { - $.ajax({ - url: window.gitHubReleaseApiUrl, - method: 'get', - error: function (XMLHttpRequest, textStatus, errorThrown) { - callback(false); - }, - success: function (releases) { - window.gitHubVersionList = releases; - var highestRelease = { - tag_name: '0.0.0' - }; - var highestAlphaRelease = { - tag_name: '0.0.0' - }; - var highestBetaRelease = { - tag_name: '0.0.0' - }; - var highestRcRelease = { - tag_name: '0.0.0' - }; - - for (var i in releases) { - //drafts will be ignored - if (releases[i].draft) - continue; - - if (releases[i].tag_name.includes('alpha')) { - if (sem = semverLite.gt(releases[i].tag_name, highestAlphaRelease.tag_name)) - highestAlphaRelease = releases[i]; - } - else if (releases[i].tag_name.includes('beta')) { - if (sem = semverLite.gt(releases[i].tag_name, highestBetaRelease.tag_name)) - highestBetaRelease = releases[i]; - } - else if (releases[i].tag_name.includes('rc')) { - if (semverLite.gt(releases[i].tag_name, highestRcRelease.tag_name)) - highestRcRelease = releases[i]; - } - else { - if (semverLite.gt(releases[i].tag_name, highestRelease.tag_name)) - highestRelease = releases[i]; - } - } - window.latestStableVersion = highestRelease; - window.latestBetaVersion = highestBetaRelease; - window.latestAlphaVersion = highestAlphaRelease; - window.latestRcVersion = highestRcRelease; - - if (window.serverConfig.general.watchedVersionBranch == "Beta" && semverLite.gt(highestBetaRelease.tag_name, highestRelease.tag_name)) - window.latestVersion = highestBetaRelease; - else - window.latestVersion = highestRelease; - - if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.gt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) - window.latestVersion = highestAlphaRelease; - - if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.lt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) - window.latestVersion = highestBetaRelease; - - //next two if statements are only necessary if we don't have a beta or stable release. We need one alpha release at least - if (window.latestVersion.tag_name == '0.0.0' && highestBetaRelease.tag_name != '0.0.0') - window.latestVersion = highestBetaRelease; - - if (window.latestVersion.tag_name == '0.0.0' && highestAlphaRelease.tag_name != '0.0.0') - window.latestVersion = highestAlphaRelease; - - callback(true); - } - }); -} - -function getSystemInfo() { - var sys = window.sysInfo.system; - var shy = window.sysInfo.hyperion; - - var info = "Hyperion Server:\n"; - info += '- Build: ' + shy.build + '\n'; - info += '- Build time: ' + shy.time + '\n'; - info += '- Git Remote: ' + shy.gitremote + '\n'; - info += '- Version: ' + shy.version + '\n'; - info += '- UI Lang: ' + storedLang + ' (BrowserLang: ' + navigator.language + ')\n'; - info += '- UI Access: ' + storedAccess + '\n'; - //info += '- Log lvl: ' + window.serverConfig.logger.level + '\n'; - info += '- Avail Screen Cap.: ' + window.serverInfo.grabbers.screen.available + '\n'; - info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; - info += '- Avail Services: ' + window.serverInfo.services + '\n'; - info += '- Config path: ' + shy.rootPath + '\n'; - info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; - - info += '\n'; - - info += 'Hyperion Server OS:\n'; - info += '- Distribution: ' + sys.prettyName + '\n'; - info += '- Architecture: ' + sys.architecture + '\n'; - - if (sys.cpuModelName) - info += '- CPU Model: ' + sys.cpuModelName + '\n'; - if (sys.cpuModelType) - info += '- CPU Type: ' + sys.cpuModelType + '\n'; - if (sys.cpuRevision) - info += '- CPU Revision: ' + sys.cpuRevision + '\n'; - if (sys.cpuHardware) - info += '- CPU Hardware: ' + sys.cpuHardware + '\n'; - - info += '- Kernel: ' + sys.kernelType + ' (' + sys.kernelVersion + ' (WS: ' + sys.wordSize + '))\n'; - info += '- Root/Admin: ' + sys.isUserAdmin + '\n'; - info += '- Qt Version: ' + sys.qtVersion + '\n'; - if (jQuery.inArray("effectengine", window.serverInfo.services) !== -1) { - info += '- Python Version: ' + sys.pyVersion + '\n'; - } - info += '- Browser: ' + navigator.userAgent; - return info; -} - -function handleDarkMode() { - $("", { - rel: "stylesheet", - type: "text/css", - href: "../css/darkMode.css" - }).appendTo("head"); - - setStorage("darkMode", "on"); - $('#btn_darkmode_icon').removeClass('fa fa-moon-o'); - $('#btn_darkmode_icon').addClass('mdi mdi-white-balance-sunny'); - $('#navbar_brand_logo').attr("src", 'img/hyperion/logo_negativ.png'); -} - -function isAccessLevelCompliant(accessLevel) { - var isOK = true; - if (accessLevel) { - if (accessLevel === 'system') { - isOK = false; - } - else if (accessLevel === 'advanced' && storedAccess === 'default') { - isOK = false; - } - else if (accessLevel === 'expert' && storedAccess !== 'expert') { - isOK = false; - } - } - return isOK -} - -function showInputOptions(path, elements, state) { - for (var i = 0; i < elements.length; i++) { - $('[data-schemapath="root.' + path + '.' + elements[i] + '"]').toggle(state); - } -} - -function showInputOptionForItem(editor, path, item, state) { - var accessLevel = editor.schema.properties[path].properties[item].access; - // Enable element only, if access level compliant - if (!state || isAccessLevelCompliant(accessLevel)) { - showInputOptions(path, [item], state); - } -} - -function showInputOptionsForKey(editor, item, showForKeys, state) { - var elements = []; - var keysToshow = []; - - if (Array.isArray(showForKeys)) { - keysToshow = showForKeys; - } else { - if (typeof showForKeys === 'string') { - keysToshow.push(showForKeys); - } else { - return - } - } - - for (var key in editor.schema.properties[item].properties) { - if ($.inArray(key, keysToshow) === -1) { - var accessLevel = editor.schema.properties[item].properties[key].access; - - //Always disable all elements, but only enable elements, if access level compliant - if (!state || isAccessLevelCompliant(accessLevel)) { - elements.push(key); - } - } - } - showInputOptions(item, elements, state); -} - -function encodeHTML(s) { - return s.replace(/&/g, '&').replace(/ 255) { - return false; - } - } - return true; -} - -function isValidIPv6(value) { - if (value.match( - '^(?:(?:(?:[a-fA-F0-9]{1,4}:){6}|(?=(?:[a-fA-F0-9]{0,4}:){2,6}(?:[0-9]{1,3}.){3}[0-9]{1,3}$)(([0-9a-fA-F]{1,4}:){1,5}|:)((:[0-9a-fA-F]{1,4}){1,5}:|:)|::(?:[a-fA-F0-9]{1,4}:){5})(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]).){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?=(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}$)(([0-9a-fA-F]{1,4}:){1,7}|:)((:[0-9a-fA-F]{1,4}){1,7}|:)|(?:[a-fA-F0-9]{1,4}:){7}:|:(:[a-fA-F0-9]{1,4}){7})$' - )) - return true; - else - return false; -} - -function isValidHostname(value) { - if (value.match( - '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' - )) - return true; - else - return false; -} - -function isValidServicename(value) { - if (value.match( - '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9 -]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' - )) - return true; - else - return false; -} - -function isValidHostnameOrIP4(value) { - return (isValidHostname(value) || isValidIPv4(value)); -} - -function isValidHostnameOrIP(value) { - return (isValidHostnameOrIP4(value) || isValidIPv6(value) || isValidServicename(value)); -} - +var prevTag; + +function removeOverlay() { + $("#loading_overlay").removeClass("overlay"); +} + +function reload() { + location.reload(); +} + +function storageComp() { + if (typeof (Storage) !== "undefined") + return true; + return false; +} + +function getStorage(item) { + if (storageComp()) { + return localStorage.getItem(item); + } + return null; +} + +function setStorage(item, value) { + if (storageComp()) { + localStorage.setItem(item, value); + } +} + +function removeStorage(item) { + if (storageComp()) { + localStorage.removeItem(item); + } +} + +function debugMessage(msg) { + if (window.debugMessagesActive) { + console.log(msg); + } +} + +function validateDuration(d) { + if (typeof d === "undefined" || d < 0) + return ENDLESS; + else + return d *= 1000; +} + +function getHashtag() { + if (getStorage('lasthashtag') != null) + return getStorage('lasthashtag'); + else { + var tag = document.URL; + tag = tag.substr(tag.indexOf("#") + 1); + if (tag == "" || typeof tag === "undefined" || tag.startsWith("http")) + tag = "dashboard" + return tag; + } +} + +function loadContent(event, forceRefresh) { + var tag; + + var lastSelectedInstance = getStorage('lastSelectedInstance'); + + if (lastSelectedInstance && (lastSelectedInstance != window.currentHyperionInstance)) { + if (window.serverInfo.instance[lastSelectedInstance] && window.serverInfo.instance[lastSelectedInstance].running) { + instanceSwitch(lastSelectedInstance); + } else { + removeStorage('lastSelectedInstance'); + } + } + + if (typeof event != "undefined") { + tag = event.currentTarget.hash; + tag = tag.substr(tag.indexOf("#") + 1); + setStorage('lasthashtag', tag); + } + else + tag = getHashtag(); + + if (forceRefresh || prevTag != tag) { + prevTag = tag; + $("#page-content").off(); + $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { + if (status == "error") { + tag = 'dashboard'; + console.log("Could not find page:", prevTag, ", Redirecting to:", tag); + setStorage('lasthashtag', tag); + + $("#page-content").load("/content/" + tag + ".html", function (response, status, xhr) { + if (status == "error") { + $("#page-content").html('

    ' + encode_utf8(tag) + '
    ' + $.i18n('info_404') + '

    '); + removeOverlay(); + } + }); + } + updateUiOnInstance(window.currentHyperionInstance); + }); + } +} + +function getInstanceNameByIndex(index) { + var instData = window.serverInfo.instance + for (var key in instData) { + if (instData[key].instance == index) + return instData[key].friendly_name; + } + return "unknown" +} + +function updateHyperionInstanceListing() { + if (window.serverInfo.instance) { + var data = window.serverInfo.instance.filter(entry => entry.running); + $('#hyp_inst_listing').html(""); + for (var key in data) { + var currInstMarker = (data[key].instance == window.currentHyperionInstance) ? "component-on" : ""; + + var html = '
  • \ + \ +
    \ + \ + '+ data[key].friendly_name + ' \ +
    \ +
    \ +
  • ' + + if (data.length - 1 > key) + html += '
  • ' + + $('#hyp_inst_listing').append(html); + + $('#hyperioninstance_' + data[key].instance).off().on("click", function (e) { + var inst = e.currentTarget.id.split("_")[1] + instanceSwitch(inst) + }); + } + } +} + +function initLanguageSelection() { + // Initialise language selection list with languages supported + for (var i = 0; i < availLang.length; i++) { + $("#language-select").append(''); + } + + var langLocale = storedLang; + + //Test, if language is supported by hyperion + var langIdx = availLang.indexOf(langLocale); + if (langIdx > -1) { + langText = availLangText[langIdx]; + } else { + // If language is not supported by hyperion, try fallback language + langLocale = $.i18n().options.fallbackLocale.substring(0, 2); + langIdx = availLang.indexOf(langLocale); + if (langIdx > -1) { + langText = availLangText[langIdx]; + } else { + langLocale = 'en'; + langIdx = availLang.indexOf(langLocale); + if (langIdx > -1) { + langText = availLangText[langIdx]; + } + } + } + + $('#language-select').prop('title', langText); + $("#language-select").val(langIdx); + $("#language-select").selectpicker("refresh"); +} + +function updateUiOnInstance(inst) { + + window.currentHyperionInstance = inst; + window.currentHyperionInstanceName = getInstanceNameByIndex(inst); + + $("#active_instance_friendly_name").text(getInstanceNameByIndex(inst)); + if (window.serverInfo.instance.filter(entry => entry.running).length > 1) { + $('#btn_hypinstanceswitch').toggle(true); + $('#active_instance_dropdown').prop('disabled', false); + $('#active_instance_dropdown').css('cursor', 'pointer'); + $("#active_instance_dropdown").css("pointer-events", "auto"); + } else { + $('#btn_hypinstanceswitch').toggle(false); + $('#active_instance_dropdown').prop('disabled', true); + $("#active_instance_dropdown").css('cursor', 'default'); + $("#active_instance_dropdown").css("pointer-events", "none"); + } +} + +function instanceSwitch(inst) { + requestInstanceSwitch(inst) + window.currentHyperionInstance = inst; + window.currentHyperionInstanceName = getInstanceNameByIndex(inst); + setStorage('lastSelectedInstance', inst) +} + +function loadContentTo(containerId, fileName) { + $(containerId).load("/content/" + fileName + ".html"); +} + +function toggleClass(obj, class1, class2) { + if ($(obj).hasClass(class1)) { + $(obj).removeClass(class1); + $(obj).addClass(class2); + } + else { + $(obj).removeClass(class2); + $(obj).addClass(class1); + } +} + +function setClassByBool(obj, enable, class1, class2) { + if (enable) { + $(obj).removeClass(class1); + $(obj).addClass(class2); + } + else { + $(obj).removeClass(class2); + $(obj).addClass(class1); + } +} + +function showInfoDialog(type, header, message) { + if (type == "success") { + $('#id_body').html(''); + if (header == "") + $('#id_body').append('

    ' + $.i18n('infoDialog_general_success_title') + '

    '); + $('#id_footer').html(''); + } + else if (type == "warning") { + $('#id_body').html(''); + if (header == "") + $('#id_body').append('

    ' + $.i18n('infoDialog_general_warning_title') + '

    '); + $('#id_footer').html(''); + } + else if (type == "error") { + $('#id_body').html(''); + if (header == "") + $('#id_body').append('

    ' + $.i18n('infoDialog_general_error_title') + '

    '); + $('#id_footer').html(''); + } + else if (type == "select") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "iswitch") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "uilock") { + $('#id_body').html(''); + $('#id_footer').html('' + $.i18n('InfoDialog_nowrite_foottext') + ''); + } + else if (type == "import") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "delInst") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + else if (type == "renInst") { + $('#id_body_rename').html('
    '); + $('#id_body_rename').append('

    ' + header + '

    '); + $('#id_body_rename').append(''); + $('#id_footer_rename').html(''); + $('#id_footer_rename').append(''); + } + else if (type == "changePassword") { + $('#id_body_rename').html('
    '); + $('#id_body_rename').append('

    ' + header + '


    '); + $('#id_body_rename').append('

    ' + $.i18n('infoDialog_username_text') + + '


    '); + $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_current_text') + + '


    '); + $('#id_body_rename').append('

    ' + $.i18n('infoDialog_password_new_text') + + '

    '); + $('#id_body_rename').append('
    ' + $.i18n('infoDialog_password_minimum_length') + '
    '); + $('#id_footer_rename').html(''); + $('#id_footer_rename').append(''); + } + else if (type == "checklist") { + $('#id_body').html(''); + $('#id_body').append('

    ' + $.i18n('infoDialog_checklist_title') + '

    '); + $('#id_body').append(header); + $('#id_footer').html(''); + } + else if (type == "newToken") { + $('#id_body').html(''); + $('#id_footer').html(''); + } + else if (type == "grantToken") { + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); + } + + if (type != "renInst") { + $('#id_body').append('

    ' + header + '

    '); + $('#id_body').append(message); + } + + if (type == "select" || type == "iswitch") + $('#id_body').append(''); + + if (getStorage("darkMode") == "on") + $('#id_logo').attr("src", 'img/hyperion/logo_negativ.png'); + + $(type == "renInst" || type == "changePassword" ? "#modal_dialog_rename" : "#modal_dialog").modal({ + backdrop: "static", + keyboard: false, + show: true + }); + + $(document).on('click', '[data-dismiss-modal]', function () { + var target = $(this).attr('data-dismiss-modal'); + $.find(target).modal('hide'); + }); +} + +function createHintH(type, text, container) { + type = String(type); + if (type == "intro") + tclass = "introd"; + + $('#' + container).prepend('

    ' + text + '


    '); +} + +function createHint(type, text, container, buttonid, buttontxt) { + var fe, tclass; + + if (type == "intro") { + fe = ''; + tclass = "intro-hint"; + } + else if (type == "info") { + fe = '
    Information
    '; + tclass = "info-hint"; + } + else if (type == "wizard") { + fe = '
    Information
    '; + tclass = "wizard-hint"; + } + else if (type == "warning") { + fe = '
    Information
    '; + tclass = "warning-hint"; + } + + if (buttonid) + buttonid = '

    '; + else + buttonid = ""; + + if (type == "intro") + $('#' + container).prepend('

    ' + $.i18n("conf_helptable_expl") + '

    ' + text + '
    '); + else if (type == "wizard") + $('#' + container).prepend('

    ' + $.i18n("wiz_wizavail") + '

    ' + $.i18n('wiz_guideyou', text) + buttonid + '
    '); + else { + createTable('', 'htb', container, true, tclass); + $('#' + container + ' .htb').append(createTableRow([fe, text], false, true)); + } +} + +function createEffHint(title, text) { + return '

    ' + title + '

    ' + text + '
    '; +} + +function valValue(id, value, min, max) { + if (typeof max === 'undefined' || max == "") + max = 999999; + + if (Number(value) > Number(max)) { + $('#' + id).val(max); + showInfoDialog("warning", "", $.i18n('edt_msg_error_maximum_incl', max)); + return max; + } + else if (Number(value) < Number(min)) { + $('#' + id).val(min); + showInfoDialog("warning", "", $.i18n('edt_msg_error_minimum_incl', min)); + return min; + } + return value; +} + +function readImg(input, cb) { + if (input.files && input.files[0]) { + var reader = new FileReader(); + // inject fileName property + reader.fileName = input.files[0].name + + reader.onload = function (e) { + cb(e.target.result, e.target.fileName); + } + reader.readAsDataURL(input.files[0]); + } +} + +function isJsonString(str) { + try { + JSON.parse(str); + } + catch (e) { + return e; + } + return ""; +} + +const getObjectProperty = (obj, path) => path.split(".").reduce((o, key) => o && typeof o[key] !== 'undefined' ? o[key] : undefined, obj); + +const setObjectProperty = (object, path, value) => { + const parts = path.split('.'); + const limit = parts.length - 1; + for (let i = 0; i < limit; ++i) { + const key = parts[i]; + if (key === "__proto__" || key === "constructor") continue; + object = object[key] ?? (object[key] = {}); + } + const key = parts[limit]; + object[key] = value; +}; + +function getLongPropertiesPath(path) { + if (path) { + var path = path.replace('root.', ''); + const parts = path.split('.'); + parts.forEach(function (part, index) { + this[index] += ".properties"; + }, parts); + path = parts.join('.') + '.'; + } + return path; +} + +function createJsonEditor(container, schema, setconfig, usePanel, arrayre) { + $('#' + container).off(); + $('#' + container).html(""); + + if (typeof arrayre === 'undefined') + arrayre = true; + + var editor = new JSONEditor(document.getElementById(container), + { + theme: 'bootstrap3', + iconlib: "fontawesome4", + disable_collapse: 'true', + form_name_root: 'sa', + disable_edit_json: true, + disable_properties: true, + disable_array_reorder: arrayre, + no_additional_properties: true, + disable_array_delete_all_rows: true, + disable_array_delete_last_row: true, + access: storedAccess, + schema: { + title: '', + properties: schema + } + }); + + if (usePanel) { + $('#' + container + ' .well').first().removeClass('well well-sm'); + $('#' + container + ' h4').first().remove(); + $('#' + container + ' .well').first().removeClass('well well-sm'); + } + + if (setconfig) { + for (var key in editor.root.editors) { + editor.getEditor("root." + key).setValue(Object.assign({}, editor.getEditor("root." + key).value, window.serverConfig[key])); + } + } + + return editor; +} + +function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { + var editor = rootEditor.getEditor(path); + var orginalProperties = editor.schema.properties[key]; + + var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; + rootEditor.unwatch(path + "." + key); + + var newSchema = []; + newSchema[key] = + { + "type": "string", + "enum": [], + "required": true, + "options": { "enum_titles": [], "infoText": "" }, + "propertyOrder": 1 + }; + + //Add additional elements to overwrite defaults + for (var item in addElements) { + newSchema[key][item] = addElements[item]; + } + + if (orginalProperties) { + if (orginalProperties["title"]) { + newSchema[key]["title"] = orginalProperties["title"]; + } + + if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { + newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; + } + + if (orginalProperties["propertyOrder"]) { + newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; + } + } + + if (addCustom) { + + if (newTitelVals.length === 0) { + newTitelVals = [...newEnumVals]; + } + + if (!!!customText) { + customText = "edt_conf_enum_custom"; + } + + if (addCustomAsFirst) { + newEnumVals.unshift("CUSTOM"); + newTitelVals.unshift(customText); + } else { + newEnumVals.push("CUSTOM"); + newTitelVals.push(customText); + } + + if (newSchema[key].options.infoText) { + var customInfoText = newSchema[key].options.infoText + "_custom"; + newSchema[key].options.infoText = customInfoText; + } + } + + if (addSelect) { + newEnumVals.unshift("SELECT"); + newTitelVals.unshift("edt_conf_enum_please_select"); + newDefaultVal = "SELECT"; + } + + if (newEnumVals) { + newSchema[key]["enum"] = newEnumVals; + } + + if (newTitelVals) { + newSchema[key]["options"]["enum_titles"] = newTitelVals; + } + if (newDefaultVal) { + newSchema[key]["default"] = newDefaultVal; + } + + editor.original_schema.properties[key] = orginalProperties; + editor.schema.properties[key] = newSchema[key]; + //Update schema properties for validator + setObjectProperty(rootEditor.validator.schema.properties, getLongPropertiesPath(path) + key, newSchema[key]); + + editor.removeObjectProperty(key); + delete editor.cached_editors[key]; + editor.addObjectProperty(key); + + if (orginalWatchFunctions) { + for (var i = 0; i < orginalWatchFunctions.length; i++) { + rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); + } + } + rootEditor.notifyWatchers(path + "." + key); +} + +function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal) { + var editor = rootEditor.getEditor(path); + var orginalProperties = editor.schema.properties[key]; + + var orginalWatchFunctions = rootEditor.watchlist[path + "." + key]; + rootEditor.unwatch(path + "." + key); + + var newSchema = []; + newSchema[key] = + { + "type": "array", + "format": "select", + "items": { + "type": "string", + "enum": [], + "options": { "enum_titles": [] }, + }, + "options": { "infoText": "" }, + "default": [], + "propertyOrder": 1 + }; + + //Add additional elements to overwrite defaults + for (var item in addElements) { + newSchema[key][item] = addElements[item]; + } + + if (orginalProperties) { + if (orginalProperties["title"]) { + newSchema[key]["title"] = orginalProperties["title"]; + } + + if (orginalProperties["options"] && orginalProperties["options"]["infoText"]) { + newSchema[key]["options"]["infoText"] = orginalProperties["options"]["infoText"]; + } + + if (orginalProperties["propertyOrder"]) { + newSchema[key]["propertyOrder"] = orginalProperties["propertyOrder"]; + } + } + + if (newEnumVals) { + newSchema[key]["items"]["enum"] = newEnumVals; + } + + if (newTitelVals) { + newSchema[key]["items"]["options"]["enum_titles"] = newTitelVals; + } + + if (newDefaultVal) { + newSchema[key]["default"] = newDefaultVal; + } + + editor.original_schema.properties[key] = orginalProperties; + editor.schema.properties[key] = newSchema[key]; + //Update schema properties for validator + setObjectProperty(rootEditor.validator.schema.properties, getLongPropertiesPath(path) + key, newSchema[key]); + + editor.removeObjectProperty(key); + delete editor.cached_editors[key]; + editor.addObjectProperty(key); + + if (orginalWatchFunctions) { + for (var i = 0; i < orginalWatchFunctions.length; i++) { + rootEditor.watch(path + "." + key, orginalWatchFunctions[i]); + } + } + rootEditor.notifyWatchers(path + "." + key); +} + +function updateJsonEditorRange(rootEditor, path, key, minimum, maximum, defaultValue, step, clear) { + var editor = rootEditor.getEditor(path); + + //Preserve current value when updating range + var currentValue = rootEditor.getEditor(path + "." + key).getValue(); + + var orginalProperties = editor.schema.properties[key]; + var newSchema = []; + newSchema[key] = orginalProperties; + + if (clear) { + delete newSchema[key]["minimum"]; + delete newSchema[key]["maximum"]; + delete newSchema[key]["default"]; + delete newSchema[key]["step"]; + } + + if (typeof minimum !== "undefined") { + newSchema[key]["minimum"] = minimum; + } + if (typeof maximum !== "undefined") { + newSchema[key]["maximum"] = maximum; + } + if (typeof defaultValue !== "undefined") { + newSchema[key]["default"] = defaultValue; + currentValue = defaultValue; + } + + if (typeof step !== "undefined") { + newSchema[key]["step"] = step; + } + + editor.original_schema.properties[key] = orginalProperties; + editor.schema.properties[key] = newSchema[key]; + //Update schema properties for validator + setObjectProperty(rootEditor.validator.schema.properties, getLongPropertiesPath(path) + key, newSchema[key]); + + editor.removeObjectProperty(key); + delete editor.cached_editors[key]; + editor.addObjectProperty(key); + + // Restore current (new default) value for new range + rootEditor.getEditor(path + "." + key).setValue(currentValue); +} + +function addJsonEditorHostValidation() { + + JSONEditor.defaults.custom_validators.push(function (schema, value, path) { + var errors = []; + + if (!jQuery.isEmptyObject(value)) { + switch (schema.format) { + case "hostname_or_ip": + if (!isValidHostnameOrIP(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msgcust_error_hostname_ip') + }); + } + break; + case "hostname_or_ip4": + if (!isValidHostnameOrIP4(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msgcust_error_hostname_ip4') + }); + } + break; + + //Remove, when new json-editor 2.x is used + case "ipv4": + if (!isValidIPv4(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msg_error_ipv4') + }); + } + break; + case "ipv6": + if (!isValidIPv6(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msg_error_ipv6') + }); + } + break; + case "hostname": + if (!isValidHostname(value)) { + errors.push({ + path: path, + property: 'format', + message: $.i18n('edt_msg_error_hostname') + }); + } + break; + + default: + } + } + return errors; + }); +} + +function buildWL(link, linkt, cl) { + var baseLink = "https://docs.hyperion-project.org/"; + var lang; + + if (typeof linkt == "undefined") + linkt = "Placeholder"; + + if (storedLang == "de" || navigator.locale == "de") + lang = "de"; + else + lang = "en"; + + if (cl === true) { + linkt = $.i18n(linkt); + return '

    ' + linkt + '

    ' + $.i18n('general_wiki_moreto', linkt) + ': ' + linkt + '
    ' + } + else + return ': ' + linkt + ''; +} + +function rgbToHex(rgb) { + if (rgb.length == 3) { + return "#" + + ("0" + parseInt(rgb[0], 10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + + ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2); + } + else + debugMessage('rgbToHex: Given rgb is no array or has wrong length'); +} + +function hexToRgb(hex) { + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : { + r: 0, + g: 0, + b: 0 + }; +} + +/* + Show a notification + @param type Valid types are "info","success","warning","danger" + @param message The message to show + @param title A title (optional) + @param addhtml Add custom html to the notification end + */ +function showNotification(type, message, title = "", addhtml = "") { + if (title == "") { + switch (type) { + case "info": + title = $.i18n('infoDialog_general_info_title'); + break; + case "success": + title = $.i18n('infoDialog_general_success_title'); + break; + case "warning": + title = $.i18n('infoDialog_general_warning_title'); + break; + case "danger": + title = $.i18n('infoDialog_general_error_title'); + break; + } + } + + $.notify({ + // options + title: title, + message: message + }, { + // settings + type: type, + animate: { + enter: 'animate__animated animate__fadeInDown', + exit: 'animate__animated animate__fadeOutUp' + }, + placement: { + align: 'center' + }, + mouse_over: 'pause', + template: '' + }); +} + +function createCP(id, color, cb) { + if (Array.isArray(color)) + color = rgbToHex(color); + else if (color == "undefined") + color = "#AA3399"; + + if (color.startsWith("#")) { + $('#' + id).colorpicker({ + format: 'rgb', + customClass: 'colorpicker-2x', + color: color, + sliders: { + saturation: { + maxLeft: 200, + maxTop: 200 + }, + hue: { + maxTop: 200 + }, + } + }); + $('#' + id).colorpicker().on('changeColor', function (e) { + var rgb = e.color.toRGB(); + var hex = e.color.toHex(); + cb(rgb, hex, e); + }); + } + else + debugMessage('createCP: Given color is not legit'); +} + +// Creates a table with thead and tbody ids +// @param string hid : a class for thead +// @param string bid : a class for tbody +// @param string cont : a container id to html() the table +// @param string bless: if true the table is borderless +function createTable(hid, bid, cont, bless, tclass) { + var table = document.createElement('table'); + var thead = document.createElement('thead'); + var tbody = document.createElement('tbody'); + + table.className = "table"; + if (bless === true) + table.className += " borderless"; + if (typeof tclass !== "undefined") + table.className += " " + tclass; + table.style.marginBottom = "0px"; + if (hid != "") + thead.className = hid; + tbody.className = bid; + if (hid != "") + table.appendChild(thead); + table.appendChild(tbody); + + $('#' + cont).append(table); +} + +// Creates a table row +// @param array list :innerHTML content for / +// @param bool head :if null or false it's body +// @param bool align :if null or false no alignment +// +// @return : with or as child(s) +function createTableRow(list, head, align) { + var row = document.createElement('tr'); + + for (var i = 0; i < list.length; i++) { + if (head === true) + var el = document.createElement('th'); + else + var el = document.createElement('td'); + + if (align) + el.style.verticalAlign = "middle"; + + var purifyConfig = { + ADD_TAGS: ['button'], + ADD_ATTR: ['onclick'] + }; + el.innerHTML = DOMPurify.sanitize(list[i], purifyConfig); + row.appendChild(el); + } + return row; +} + +function createRow(id) { + var el = document.createElement('div'); + el.className = "row"; + el.setAttribute('id', id); + return el; +} + +function createOptPanel(phicon, phead, bodyid, footerid, css, panelId) { + phead = '' + phead; + + var pfooter = document.createElement('button'); + pfooter.className = "btn btn-primary"; + pfooter.setAttribute("id", footerid); + pfooter.innerHTML = '' + $.i18n('general_button_savesettings'); + + return createPanel(phead, "", pfooter, "panel-default", bodyid, css, panelId); +} + +function compareTwoValues(key1, key2, order = 'asc') { + return function innerSort(a, b) { + if (!a.hasOwnProperty(key1) || !b.hasOwnProperty(key1)) { + // property key1 doesn't exist on either object + return 0; + } + + const varA1 = (typeof a[key1] === 'string') + ? a[key1].toUpperCase() : a[key1]; + const varB1 = (typeof b[key1] === 'string') + ? b[key1].toUpperCase() : b[key1]; + + let comparison = 0; + if (varA1 > varB1) { + comparison = 1; + } else { + if (varA1 < varB1) { + comparison = -1; + } else { + if (!a.hasOwnProperty(key2) || !b.hasOwnProperty(key2)) { + // property key2 doesn't exist on either object + return 0; + } + + const varA2 = (typeof a[key2] === 'string') + ? a[key2].toUpperCase() : a[key2]; + const varB2 = (typeof b[key1] === 'string') + ? b[key2].toUpperCase() : b[key2]; + + if (varA2 > varB2) { + comparison = 1; + } else { + comparison = -1; + } + } + } + return ( + (order === 'desc') ? (comparison * -1) : comparison + ); + }; +} + +function sortProperties(list) { + for (var key in list) { + list[key].key = key; + } + list = $.map(list, function (value, index) { + return [value]; + }); + return list.sort(function (a, b) { + return a.propertyOrder - b.propertyOrder; + }); +} + +function createHelpTable(list, phead, panelId) { + var table = document.createElement('table'); + var thead = document.createElement('thead'); + var tbody = document.createElement('tbody'); + list = sortProperties(list); + + phead = '' + phead + ' ' + $.i18n("conf_helptable_expl"); + + table.className = 'table table-hover borderless'; + + thead.appendChild(createTableRow([$.i18n('conf_helptable_option'), $.i18n('conf_helptable_expl')], true, false)); + + for (var key in list) { + if (list[key].access != 'system') { + // break one iteration (in the loop), if the schema has the entry hidden=true + if ("options" in list[key] && "hidden" in list[key].options && (list[key].options.hidden)) + continue; + if ("access" in list[key] && ((list[key].access == "advanced" && storedAccess == "default") || (list[key].access == "expert" && storedAccess != "expert"))) + continue; + var text = list[key].title.replace('title', 'expl'); + tbody.appendChild(createTableRow([$.i18n(list[key].title), $.i18n(text)], false, false)); + + if (list[key].items && list[key].items.properties) { + var ilist = sortProperties(list[key].items.properties); + for (var ikey in ilist) { + // break one iteration (in the loop), if the schema has the entry hidden=true + if ("options" in ilist[ikey] && "hidden" in ilist[ikey].options && (ilist[ikey].options.hidden)) + continue; + if ("access" in ilist[ikey] && ((ilist[ikey].access == "advanced" && storedAccess == "default") || (ilist[ikey].access == "expert" && storedAccess != "expert"))) + continue; + var itext = ilist[ikey].title.replace('title', 'expl'); + tbody.appendChild(createTableRow([$.i18n(ilist[ikey].title), $.i18n(itext)], false, false)); + } + } + } + } + table.appendChild(thead); + table.appendChild(tbody); + + return createPanel(phead, table, undefined, undefined, undefined, undefined, panelId); +} + +function createPanel(head, body, footer, type, bodyid, css, panelId) { + var cont = document.createElement('div'); + var p = document.createElement('div'); + var phead = document.createElement('div'); + var pbody = document.createElement('div'); + var pfooter = document.createElement('div'); + + cont.className = "col-lg-6"; + + if (typeof type == 'undefined') + type = 'panel-default'; + + p.className = 'panel ' + type; + if (typeof panelId != 'undefined') { + p.setAttribute("id", panelId); + } + + phead.className = 'panel-heading ' + css; + pbody.className = 'panel-body'; + pfooter.className = 'panel-footer'; + + phead.innerHTML = head; + + if (typeof bodyid != 'undefined') { + pfooter.style.textAlign = 'right'; + pbody.setAttribute("id", bodyid); + } + + if (typeof body != 'undefined' && body != "") + pbody.appendChild(body); + + if (typeof footer != 'undefined') + pfooter.appendChild(footer); + + p.appendChild(phead); + p.appendChild(pbody); + + if (typeof footer != 'undefined') { + pfooter.style.textAlign = "right"; + p.appendChild(pfooter); + } + + cont.appendChild(p); + + return cont; +} + +function createSelGroup(group) { + var el = document.createElement('optgroup'); + el.setAttribute('label', group); + return el; +} + +function createSelOpt(opt, title) { + var el = document.createElement('option'); + el.setAttribute('value', opt); + if (typeof title == 'undefined') + el.innerHTML = opt; + else + el.innerHTML = title; + return el; +} + +function createSel(array, group, split) { + if (array.length != 0) { + var el = createSelGroup(group); + for (var i = 0; i < array.length; i++) { + var opt; + if (split) { + opt = array[i].split(":") + opt = createSelOpt(opt[0], opt[1]) + } + else + opt = createSelOpt(array[i]) + el.appendChild(opt); + } + return el; + } +} + +function performTranslation() { + $('[data-i18n]').i18n(); +} + +function encode_utf8(s) { + return unescape(encodeURIComponent(s)); +} + +function getReleases(callback) { + $.ajax({ + url: window.gitHubReleaseApiUrl, + method: 'get', + error: function (XMLHttpRequest, textStatus, errorThrown) { + callback(false); + }, + success: function (releases) { + window.gitHubVersionList = releases; + var highestRelease = { + tag_name: '0.0.0' + }; + var highestAlphaRelease = { + tag_name: '0.0.0' + }; + var highestBetaRelease = { + tag_name: '0.0.0' + }; + var highestRcRelease = { + tag_name: '0.0.0' + }; + + for (var i in releases) { + //drafts will be ignored + if (releases[i].draft) + continue; + + if (releases[i].tag_name.includes('alpha')) { + if (sem = semverLite.gt(releases[i].tag_name, highestAlphaRelease.tag_name)) + highestAlphaRelease = releases[i]; + } + else if (releases[i].tag_name.includes('beta')) { + if (sem = semverLite.gt(releases[i].tag_name, highestBetaRelease.tag_name)) + highestBetaRelease = releases[i]; + } + else if (releases[i].tag_name.includes('rc')) { + if (semverLite.gt(releases[i].tag_name, highestRcRelease.tag_name)) + highestRcRelease = releases[i]; + } + else { + if (semverLite.gt(releases[i].tag_name, highestRelease.tag_name)) + highestRelease = releases[i]; + } + } + window.latestStableVersion = highestRelease; + window.latestBetaVersion = highestBetaRelease; + window.latestAlphaVersion = highestAlphaRelease; + window.latestRcVersion = highestRcRelease; + + if (window.serverConfig.general.watchedVersionBranch == "Beta" && semverLite.gt(highestBetaRelease.tag_name, highestRelease.tag_name)) + window.latestVersion = highestBetaRelease; + else + window.latestVersion = highestRelease; + + if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.gt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) + window.latestVersion = highestAlphaRelease; + + if (window.serverConfig.general.watchedVersionBranch == "Alpha" && semverLite.lt(highestAlphaRelease.tag_name, highestBetaRelease.tag_name)) + window.latestVersion = highestBetaRelease; + + //next two if statements are only necessary if we don't have a beta or stable release. We need one alpha release at least + if (window.latestVersion.tag_name == '0.0.0' && highestBetaRelease.tag_name != '0.0.0') + window.latestVersion = highestBetaRelease; + + if (window.latestVersion.tag_name == '0.0.0' && highestAlphaRelease.tag_name != '0.0.0') + window.latestVersion = highestAlphaRelease; + + callback(true); + } + }); +} + +function getSystemInfo() { + var sys = window.sysInfo.system; + var shy = window.sysInfo.hyperion; + + var info = "Hyperion Server:\n"; + info += '- Build: ' + shy.build + '\n'; + info += '- Build time: ' + shy.time + '\n'; + info += '- Git Remote: ' + shy.gitremote + '\n'; + info += '- Version: ' + shy.version + '\n'; + info += '- UI Lang: ' + storedLang + ' (BrowserLang: ' + navigator.language + ')\n'; + info += '- UI Access: ' + storedAccess + '\n'; + //info += '- Log lvl: ' + window.serverConfig.logger.level + '\n'; + info += '- Avail Screen Cap.: ' + window.serverInfo.grabbers.screen.available + '\n'; + info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; + info += '- Avail Services: ' + window.serverInfo.services + '\n'; + info += '- Config path: ' + shy.rootPath + '\n'; + info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; + + info += '\n'; + + info += 'Hyperion Server OS:\n'; + info += '- Distribution: ' + sys.prettyName + '\n'; + info += '- Architecture: ' + sys.architecture + '\n'; + + if (sys.cpuModelName) + info += '- CPU Model: ' + sys.cpuModelName + '\n'; + if (sys.cpuModelType) + info += '- CPU Type: ' + sys.cpuModelType + '\n'; + if (sys.cpuRevision) + info += '- CPU Revision: ' + sys.cpuRevision + '\n'; + if (sys.cpuHardware) + info += '- CPU Hardware: ' + sys.cpuHardware + '\n'; + + info += '- Kernel: ' + sys.kernelType + ' (' + sys.kernelVersion + ' (WS: ' + sys.wordSize + '))\n'; + info += '- Root/Admin: ' + sys.isUserAdmin + '\n'; + info += '- Qt Version: ' + sys.qtVersion + '\n'; + if (jQuery.inArray("effectengine", window.serverInfo.services) !== -1) { + info += '- Python Version: ' + sys.pyVersion + '\n'; + } + info += '- Browser: ' + navigator.userAgent; + return info; +} + +function handleDarkMode() { + $("", { + rel: "stylesheet", + type: "text/css", + href: "../css/darkMode.css" + }).appendTo("head"); + + setStorage("darkMode", "on"); + $('#btn_darkmode_icon').removeClass('fa fa-moon-o'); + $('#btn_darkmode_icon').addClass('mdi mdi-white-balance-sunny'); + $('#navbar_brand_logo').attr("src", 'img/hyperion/logo_negativ.png'); +} + +function isAccessLevelCompliant(accessLevel) { + var isOK = true; + if (accessLevel) { + if (accessLevel === 'system') { + isOK = false; + } + else if (accessLevel === 'advanced' && storedAccess === 'default') { + isOK = false; + } + else if (accessLevel === 'expert' && storedAccess !== 'expert') { + isOK = false; + } + } + return isOK +} + +function showInputOptions(path, elements, state) { + + if (!path.startsWith("root.")) { + path = ["root", path].join('.'); + } + + for (var i = 0; i < elements.length; i++) { + $('[data-schemapath="' + path + '.' + elements[i] + '"]').toggle(state); + } +} + +function showInputOptionForItem(editor, path, item, state) { + //Get access level for full path and item + var accessLevel = getObjectProperty(editor.schema.properties, getLongPropertiesPath(path) + item + ".access"); + // Enable element only, if access level compliant + if (!state || isAccessLevelCompliant(accessLevel)) { + + if (!path) { + debugger; + path = editor.path; + } + showInputOptions(path, [item], state); + } +} + +function showInputOptionsForKey(editor, item, showForKeys, state) { + var elements = []; + var keysToshow = []; + + if (Array.isArray(showForKeys)) { + keysToshow = showForKeys; + } else { + if (typeof showForKeys === 'string') { + keysToshow.push(showForKeys); + } else { + return; + } + } + + for (var key in editor.schema.properties[item].properties) { + if ($.inArray(key, keysToshow) === -1) { + var accessLevel = editor.schema.properties[item].properties[key].access; + + var hidden = false; + if (editor.schema.properties[item].properties[key].options) { + hidden = editor.schema.properties[item].properties[key].options.hidden; + if (typeof hidden === 'undefined') { + hidden = false; + } + } + //Always disable all elements, but only enable elements, if access level compliant + if (!state || isAccessLevelCompliant(accessLevel)) { + if (!hidden) { + elements.push(key); + } + } + } + } + showInputOptions(item, elements, state); +} + +function encodeHTML(s) { + return s.replace(/&/g, '&').replace(/ 255) { + return false; + } + } + return true; +} + +function isValidIPv6(value) { + if (value.match( + '^(?:(?:(?:[a-fA-F0-9]{1,4}:){6}|(?=(?:[a-fA-F0-9]{0,4}:){2,6}(?:[0-9]{1,3}.){3}[0-9]{1,3}$)(([0-9a-fA-F]{1,4}:){1,5}|:)((:[0-9a-fA-F]{1,4}){1,5}:|:)|::(?:[a-fA-F0-9]{1,4}:){5})(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]).){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?=(?:[a-fA-F0-9]{0,4}:){0,7}[a-fA-F0-9]{0,4}$)(([0-9a-fA-F]{1,4}:){1,7}|:)((:[0-9a-fA-F]{1,4}){1,7}|:)|(?:[a-fA-F0-9]{1,4}:){7}:|:(:[a-fA-F0-9]{1,4}){7})$' + )) + return true; + else + return false; +} + +function isValidHostname(value) { + if (value.match( + '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' + )) + return true; + else + return false; +} + +function isValidServicename(value) { + if (value.match( + '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9 -]{0,61}[a-zA-Z0-9])(.([a-zA-Z0-9]|[_a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$' + )) + return true; + else + return false; +} + +function isValidHostnameOrIP4(value) { + return (isValidHostname(value) || isValidIPv4(value)); +} + +function isValidHostnameOrIP(value) { + return (isValidHostnameOrIP4(value) || isValidIPv6(value) || isValidServicename(value)); +} + diff --git a/include/leddevice/LedDevice.h b/include/leddevice/LedDevice.h index f43ee2d9..b17a0fc9 100644 --- a/include/leddevice/LedDevice.h +++ b/include/leddevice/LedDevice.h @@ -438,7 +438,10 @@ protected: uint _ledRGBWCount; /// Does the device allow restoring the original state? - bool _isRestoreOrigState; + bool _isRestoreOrigState; + + /// Does the device should be kept on after streaming + bool _isStayOnAfterStreaming; /// Device, lights state before streaming via hyperion QJsonObject _orignalStateValues; @@ -460,6 +463,9 @@ protected: /// Is the device in error state and stopped? bool _isDeviceInError; + /// Is the device in error state, but is retries might resolve the situation? + bool _isDeviceRecoverable; + /// Timestamp of last write QDateTime _lastWriteTime; @@ -476,8 +482,9 @@ protected slots: /// @brief Set device in error state /// /// @param[in] errorMsg The error message to be logged + /// @param[in] isRecoverable If False, no further retries will be done /// - virtual void setInError( const QString& errorMsg); + virtual void setInError( const QString& errorMsg, bool isRecoverable=true); private: diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index b30dd4fd..f49878c4 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -49,11 +49,13 @@ LedDevice::LedDevice(const QJsonObject& deviceConfig, QObject* parent) , _latchTime_ms(0) , _ledCount(0) , _isRestoreOrigState(false) + , _isStayOnAfterStreaming(false) , _isEnabled(false) , _isDeviceInitialised(false) , _isDeviceReady(false) , _isOn(false) , _isDeviceInError(false) + , _isDeviceRecoverable(false) , _lastWriteTime(QDateTime::currentDateTime()) , _enableAttemptsTimer(nullptr) , _enableAttemptTimerInterval(DEFAULT_ENABLE_ATTEMPTS_INTERVAL) @@ -117,7 +119,7 @@ int LedDevice::close() return retval; } -void LedDevice::setInError(const QString& errorMsg) +void LedDevice::setInError(const QString& errorMsg, bool isRecoverable) { _isOn = false; _isDeviceInError = true; @@ -125,6 +127,10 @@ void LedDevice::setInError(const QString& errorMsg) _isEnabled = false; this->stopRefreshTimer(); + if (isRecoverable) + { + _isDeviceRecoverable = isRecoverable; + } Error(_log, "Device disabled, device '%s' signals error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(errorMsg)); emit enableStateChanged(_isEnabled); } @@ -170,7 +176,7 @@ void LedDevice::enable() { emit enableStateChanged(false); - if (_maxEnableAttempts > 0) + if (_maxEnableAttempts > 0 && _isDeviceRecoverable) { Debug(_log, "Device's enablement failed - Start retry timer. Retried already done [%d], isEnabled: [%d]", _enableAttempts, _isEnabled); startEnableAttemptsTimer(); @@ -257,27 +263,30 @@ void LedDevice::startEnableAttemptsTimer() { ++_enableAttempts; - if (_enableAttempts <= _maxEnableAttempts) + if (_isDeviceRecoverable) { - if (_enableAttemptTimerInterval.count() > 0) + if (_enableAttempts <= _maxEnableAttempts) { - // setup enable retry timer - if (_enableAttemptsTimer == nullptr) + if (_enableAttemptTimerInterval.count() > 0) { - _enableAttemptsTimer = new QTimer(this); - _enableAttemptsTimer->setTimerType(Qt::PreciseTimer); - connect(_enableAttemptsTimer, &QTimer::timeout, this, &LedDevice::enable); - } - _enableAttemptsTimer->setInterval(static_cast(_enableAttemptTimerInterval.count() * 1000)); //NOLINT + // setup enable retry timer + if (_enableAttemptsTimer == nullptr) + { + _enableAttemptsTimer = new QTimer(this); + _enableAttemptsTimer->setTimerType(Qt::PreciseTimer); + connect(_enableAttemptsTimer, &QTimer::timeout, this, &LedDevice::enable); + } + _enableAttemptsTimer->setInterval(static_cast(_enableAttemptTimerInterval.count() * 1000)); //NOLINT - Info(_log, "Start %d. attempt of %d to enable the device in %d seconds", _enableAttempts, _maxEnableAttempts, _enableAttemptTimerInterval.count()); - _enableAttemptsTimer->start(); + Info(_log, "Start %d. attempt of %d to enable the device in %d seconds", _enableAttempts, _maxEnableAttempts, _enableAttemptTimerInterval.count()); + _enableAttemptsTimer->start(); + } + } + else + { + Error(_log, "Device disabled. Maximum number of %d attempts enabling the device reached. Tried for %d seconds.", _maxEnableAttempts, _enableAttempts * _enableAttemptTimerInterval.count()); + _enableAttempts = 0; } - } - else - { - Error(_log, "Device disabled. Maximum number of %d attempts enabling the device reached. Tried for %d seconds.", _maxEnableAttempts, _enableAttempts * _enableAttemptTimerInterval.count()); - _enableAttempts = 0; } } @@ -452,14 +461,16 @@ bool LedDevice::switchOff() bool LedDevice::powerOff() { - bool rc{ false }; + bool rc{ true }; - Debug(_log, "Power Off: %s", QSTRING_CSTR(_activeDeviceType)); - - // Simulate power-off by writing a final "Black" to have a defined outcome - if (writeBlack() >= 0) + if (!_isStayOnAfterStreaming) { - rc = true; + Debug(_log, "Power Off: %s", QSTRING_CSTR(_activeDeviceType)); + // Simulate power-off by writing a final "Black" to have a defined outcome + if (writeBlack() < 0) + { + rc = false; + } } return rc; } diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.cpp b/libsrc/leddevice/dev_net/LedDeviceWled.cpp index 9483c3d4..7b4fed2a 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceWled.cpp @@ -23,35 +23,61 @@ const bool verbose = false; const char CONFIG_HOST[] = "host"; const char CONFIG_STREAM_PROTOCOL[] = "streamProtocol"; const char CONFIG_RESTORE_STATE[] = "restoreOriginalState"; +const char CONFIG_STAY_ON_AFTER_STREAMING[] = "stayOnAfterStreaming"; + const char CONFIG_BRIGHTNESS[] = "brightness"; const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness"; const char CONFIG_SYNC_OVERWRITE[] = "overwriteSync"; +const char CONFIG_STREAM_SEGMENTS[] = "segments"; +const char CONFIG_STREAM_SEGMENT_ID[] = "streamSegmentId"; +const char CONFIG_SWITCH_OFF_OTHER_SEGMENTS[] = "switchOffOtherSegments"; + const char DEFAULT_STREAM_PROTOCOL[] = "DDP"; // UDP-RAW const int UDP_STREAM_DEFAULT_PORT = 19446; const int UDP_MAX_LED_NUM = 490; -// DDP +// Version constraints const char WLED_VERSION_DDP[] = "0.11.0"; +const char WLED_VERSION_SEGMENT_STREAMING[] = "0.13.3"; // WLED JSON-API elements const int API_DEFAULT_PORT = -1; //Use default port per communication scheme const char API_BASE_PATH[] = "/json/"; const char API_PATH_STATE[] = "state"; +const char API_PATH_INFO[] = "info"; -// List of State Information +// List of State keys const char STATE_ON[] = "on"; -const char STATE_VALUE_TRUE[] = "true"; -const char STATE_VALUE_FALSE[] = "false"; +const char STATE_BRI[] = "bri"; const char STATE_LIVE[] = "live"; +const char STATE_LOR[] = "lor"; +const char STATE_SEG[] = "seg"; +const char STATE_SEG_ID[] = "id"; +const char STATE_SEG_LEN[] = "len"; +const char STATE_SEG_FX[] = "fx"; +const char STATE_SEG_SX[] = "sx"; +const char STATE_MAINSEG[] = "mainseg"; +const char STATE_UDPN[] = "udpn"; +const char STATE_UDPN_SEND[] = "send"; +const char STATE_UDPN_RECV[] = "recv"; +const char STATE_TRANSITIONTIME_CURRENTCALL[] = "tt"; +// List of Info keys +const char INFO_VER[] = "ver"; +const char INFO_LIVESEG[] = "liveseg"; + +//Default state values const bool DEFAULT_IS_RESTORE_STATE = false; +const bool DEFAULT_IS_STAY_ON_AFTER_STREAMING = false; const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true; const int BRI_MAX = 255; const bool DEFAULT_IS_SYNC_OVERWRITE = true; +const int DEFAULT_SEGMENT_ID = -1; +const bool DEFAULT_IS_SWITCH_OFF_OTHER_SEGMENTS = true; constexpr std::chrono::milliseconds DEFAULT_IDENTIFY_TIME{ 2000 }; @@ -61,12 +87,16 @@ LedDeviceWled::LedDeviceWled(const QJsonObject &deviceConfig) : ProviderUdp(deviceConfig), LedDeviceUdpDdp(deviceConfig), LedDeviceUdpRaw(deviceConfig) ,_restApi(nullptr) ,_apiPort(API_DEFAULT_PORT) + ,_currentVersion("") ,_isBrightnessOverwrite(DEFAULT_IS_BRIGHTNESS_OVERWRITE) ,_brightness (BRI_MAX) ,_isSyncOverwrite(DEFAULT_IS_SYNC_OVERWRITE) ,_originalStateUdpnSend(false) ,_originalStateUdpnRecv(true) ,_isStreamDDP(true) + ,_streamSegmentId(DEFAULT_SEGMENT_ID) + ,_isSwitchOffOtherSegments(DEFAULT_IS_SWITCH_OFF_OTHER_SEGMENTS) + ,_isStreamToSegment(false) { #ifdef ENABLE_MDNS QMetaObject::invokeMethod(&MdnsBrowser::getInstance(), "browseForServiceType", @@ -112,6 +142,7 @@ bool LedDeviceWled::init(const QJsonObject &deviceConfig) { _apiPort = API_DEFAULT_PORT; _isRestoreOrigState = _devConfig[CONFIG_RESTORE_STATE].toBool(DEFAULT_IS_RESTORE_STATE); + _isStayOnAfterStreaming = _devConfig[CONFIG_STAY_ON_AFTER_STREAMING].toBool(DEFAULT_IS_STAY_ON_AFTER_STREAMING); _isSyncOverwrite = _devConfig[CONFIG_SYNC_OVERWRITE].toBool(DEFAULT_IS_SYNC_OVERWRITE); _isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE); _brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX); @@ -121,6 +152,23 @@ bool LedDeviceWled::init(const QJsonObject &deviceConfig) Debug(_log, "Overwrite Brightn.: %d", _isBrightnessOverwrite); Debug(_log, "Set Brightness to : %d", _brightness); + + QJsonObject segments = _devConfig[CONFIG_STREAM_SEGMENTS].toObject(); + _streamSegmentId = segments[CONFIG_STREAM_SEGMENT_ID].toInt(DEFAULT_SEGMENT_ID); + + if (_streamSegmentId > DEFAULT_SEGMENT_ID) + { + _isStreamToSegment = true; + } + _isSwitchOffOtherSegments = segments[CONFIG_SWITCH_OFF_OTHER_SEGMENTS].toBool(DEFAULT_IS_SWITCH_OFF_OTHER_SEGMENTS); + + Debug(_log, "Stream to one segment: %s", _isStreamToSegment ? "Yes" : "No"); + if (_isStreamToSegment ) + { + Debug(_log, "Stream to segment [%d]", _streamSegmentId); + Debug(_log, "Switch-off other segments: %s", _isSwitchOffOtherSegments ? "Yes" : "No"); + } + isInitOK = true; } @@ -193,79 +241,187 @@ int LedDeviceWled::close() return retval; } -QString LedDeviceWled::getOnOffRequest(bool isOn) const +QJsonObject LedDeviceWled::getUdpnObject(bool isSendOn, bool isRecvOn) const { - QString state = isOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE; - return QString( "\"%1\":%2,\"%3\":%4" ).arg( STATE_ON, state).arg( STATE_LIVE, state); + QJsonObject udpnObj + { + {STATE_UDPN_SEND, isSendOn}, + {STATE_UDPN_RECV, isRecvOn} + }; + return udpnObj; } -QString LedDeviceWled::getBrightnessRequest(int bri) const +QJsonObject LedDeviceWled::getSegmentObject(int segmentId, bool isOn, int brightness) const { - return QString( "\"bri\":%1" ).arg(bri); + QJsonObject segmentObj + { + {STATE_SEG_ID, segmentId}, + {STATE_ON, isOn} + }; + + if ( brightness > -1) + { + segmentObj.insert(STATE_BRI, brightness); + } + return segmentObj; } -QString LedDeviceWled::getEffectRequest(int effect, int speed) const -{ - return QString( "\"seg\":{\"fx\":%1,\"sx\":%2}" ).arg(effect).arg(speed); -} - -QString LedDeviceWled::getLorRequest(int lor) const -{ - return QString( "\"lor\":%1" ).arg(lor); -} - -QString LedDeviceWled::getUdpnRequest(bool isSendOn, bool isRecvOn) const -{ - QString send = isSendOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE; - QString recv = isRecvOn ? STATE_VALUE_TRUE : STATE_VALUE_FALSE; - return QString( "\"udpn\":{\"send\":%1,\"recv\":%2}" ).arg(send, recv); -} - -bool LedDeviceWled::sendStateUpdateRequest(const QString &request) +bool LedDeviceWled::sendStateUpdateRequest(const QJsonObject &request, const QString requestType) { bool rc = true; _restApi->setPath(API_PATH_STATE); - httpResponse response1 = _restApi->put(QString("{%1}").arg(request)); - if ( response1.error() ) + httpResponse response = _restApi->put(request); + if ( response.error() ) { + QString errorReason = QString("%1 request failed with error: '%2'").arg(requestType, response.getErrorReason()); + this->setInError ( errorReason ); rc = false; } return rc; } + +bool LedDeviceWled::isReadyForSegmentStreaming(semver::version& version) const +{ + bool isReady{false}; + + if (version.isValid()) + { + semver::version segmentStreamingVersion{WLED_VERSION_SEGMENT_STREAMING}; + if (version < segmentStreamingVersion) + { + Warning(_log, "Segment streaming not supported by your WLED device version [%s], minimum version expected [%s].", _currentVersion.getVersion().c_str(), segmentStreamingVersion.getVersion().c_str()); + } + else + { + Debug(_log, "Segment streaming is supported by your WLED device version [%s].", _currentVersion.getVersion().c_str()); + isReady = true; + } + } + else + { + Error(_log, "Version provided to test for streaming readiness is not valid "); + } + return isReady; +} + +bool LedDeviceWled::isReadyForDDPStreaming(semver::version& version) const +{ + bool isReady{false}; + + if (version.isValid()) + { + semver::version ddpVersion{WLED_VERSION_DDP}; + if (version < ddpVersion) + { + Warning(_log, "DDP streaming not supported by your WLED device version [%s], minimum version expected [%s]. Fall back to UDP-Streaming (%d LEDs max)", _currentVersion.getVersion().c_str(), ddpVersion.getVersion().c_str(), UDP_MAX_LED_NUM); + } + else + { + Debug(_log, "DDP streaming is supported by your WLED device version [%s]. No limitation in number of LEDs.", _currentVersion.getVersion().c_str()); + isReady = true; + } + } + else + { + Error(_log, "Version provided to test for streaming readiness is not valid "); + } + return isReady; +} + bool LedDeviceWled::powerOn() { bool on = false; if ( _isDeviceReady) { //Power-on WLED device - _restApi->setPath(API_PATH_STATE); - - QString cmd = getOnOffRequest(true); - - if ( _isBrightnessOverwrite) + QJsonObject cmd; + if (_isStreamToSegment) { - cmd += "," + getBrightnessRequest(_brightness); + if (!isReadyForSegmentStreaming(_currentVersion)) + { + return false; + } + + if (_wledInfo[INFO_LIVESEG].toInt() == -1) + { + stopEnableAttemptsTimer(); + this->setInError( "Segment streaming configured, but \"Use main segment only\" in WLED Sync Interface configuration is not enabled!", false); + return false; + } + else + { + QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); + + bool isStreamSegmentIdFound { false }; + + QJsonArray segments; + for (const auto& segmentItem : qAsConst(propertiesSegments)) + { + QJsonObject segmentObj = segmentItem.toObject(); + + int segmentID = segmentObj.value(STATE_SEG_ID).toInt(); + if (segmentID == _streamSegmentId) + { + isStreamSegmentIdFound = true; + int len = segmentObj.value(STATE_SEG_LEN).toInt(); + if (getLedCount() > len) + { + QString errorReason = QString("Too many LEDs [%1] configured for segment [%2], which supports maximum [%3] LEDs. Check your WLED setup!").arg(getLedCount()).arg(_streamSegmentId).arg(len); + this->setInError(errorReason, false); + return false; + } + else + { + int brightness{ -1 }; + if (_isBrightnessOverwrite) + { + brightness = _brightness; + } + segments.append(getSegmentObject(segmentID, true, brightness)); + } + } + else + { + if (_isSwitchOffOtherSegments) + { + segments.append(getSegmentObject(segmentID, false)); + } + } + } + + if (!isStreamSegmentIdFound) + { + QString errorReason = QString("Segment streaming to segment [%1] configured, but segment does not exist on WLED. Check your WLED setup!").arg(_streamSegmentId); + this->setInError(errorReason, false); + return false; + } + + cmd.insert(STATE_SEG, segments); + + //Set segment to be streamed to + cmd.insert(STATE_MAINSEG, _streamSegmentId); + } } + else + { + if (_isBrightnessOverwrite) + { + cmd.insert(STATE_BRI, _brightness); + } + } + + cmd.insert(STATE_LIVE, true); + cmd.insert(STATE_ON, true); if (_isSyncOverwrite) { Debug( _log, "Disable synchronisation with other WLED devices"); - cmd += "," + getUdpnRequest(false, false); + cmd.insert(STATE_UDPN, getUdpnObject(false, false)); } - httpResponse response = _restApi->put(QString("{%1}").arg(cmd)); - if ( response.error() ) - { - QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason()); - this->setInError ( errorReason ); - on = false; - } - else - { - on = true; - } + on = sendStateUpdateRequest(cmd,"Power-on"); } return on; } @@ -279,23 +435,25 @@ bool LedDeviceWled::powerOff() writeBlack(); //Power-off the WLED device physically - _restApi->setPath(API_PATH_STATE); + QJsonObject cmd; + if (_isStreamToSegment) + { + QJsonArray segments; + segments.append(getSegmentObject(_streamSegmentId, _isStayOnAfterStreaming)); + cmd.insert(STATE_SEG, segments); + } - QString cmd = getOnOffRequest(false); + cmd.insert(STATE_LIVE, false); + cmd.insert(STATE_TRANSITIONTIME_CURRENTCALL, 0); + cmd.insert(STATE_ON, _isStayOnAfterStreaming); if (_isSyncOverwrite) { Debug( _log, "Restore synchronisation with other WLED devices"); - cmd += "," + getUdpnRequest(_originalStateUdpnSend, _originalStateUdpnRecv); + cmd.insert(STATE_UDPN, getUdpnObject(_originalStateUdpnSend, _originalStateUdpnRecv)); } - httpResponse response = _restApi->put(QString("{%1}").arg(cmd)); - if ( response.error() ) - { - QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason()); - this->setInError ( errorReason ); - off = false; - } + off = sendStateUpdateRequest(cmd,"Power-off"); } return off; } @@ -304,28 +462,33 @@ bool LedDeviceWled::storeState() { bool rc = true; - if ( _isRestoreOrigState || _isSyncOverwrite ) + if ( _isRestoreOrigState || _isSyncOverwrite || _isStreamToSegment) { - _restApi->setPath(API_PATH_STATE); + _restApi->setPath(""); httpResponse response = _restApi->get(); if ( response.error() ) { - QString errorReason = QString("Storing device state failed with error: '%1'").arg(response.getErrorReason()); + QString errorReason = QString("Retrieving device properties failed with error: '%1'").arg(response.getErrorReason()); setInError(errorReason); rc = false; } else { - _originalStateProperties = response.getBody().object(); + _originalStateProperties = response.getBody().object().value(API_PATH_STATE).toObject(); DebugIf(verbose, _log, "state: [%s]", QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); - QJsonObject udpn = _originalStateProperties.value("udpn").toObject(); + QJsonObject udpn = _originalStateProperties.value(STATE_UDPN).toObject(); if (!udpn.isEmpty()) { - _originalStateUdpnSend = udpn["send"].toBool(false); - _originalStateUdpnRecv = udpn["recv"].toBool(true); + _originalStateUdpnSend = udpn[STATE_UDPN_SEND].toBool(false); + _originalStateUdpnRecv = udpn[STATE_UDPN_RECV].toBool(true); } + + _wledInfo = response.getBody().object().value(API_PATH_INFO).toObject(); + DebugIf(verbose, _log, "info: [%s]", QString(QJsonDocument(_wledInfo).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + + _currentVersion.setVersion(_wledInfo.value(INFO_VER).toString().toStdString()); } } @@ -340,10 +503,29 @@ bool LedDeviceWled::restoreState() { _restApi->setPath(API_PATH_STATE); + if (_isStreamToSegment) + { + QJsonArray propertiesSegments = _originalStateProperties[STATE_SEG].toArray(); + QJsonArray segments; + for (const auto& segmentItem : qAsConst(propertiesSegments)) + { + QJsonObject segmentObj = segmentItem.toObject(); + + int segmentID = segmentObj.value(STATE_SEG_ID).toInt(); + if (segmentID == _streamSegmentId) + { + segmentObj[STATE_ON] = _isStayOnAfterStreaming; + } + segments.append(segmentObj); + } + _originalStateProperties[STATE_SEG] = segments; + } + _originalStateProperties[STATE_LIVE] = false; + _originalStateProperties[STATE_TRANSITIONTIME_CURRENTCALL] = 0; + _originalStateProperties[STATE_ON] = _isStayOnAfterStreaming; - httpResponse response = _restApi->put(QString(QJsonDocument(_originalStateProperties).toJson(QJsonDocument::Compact)).toUtf8().constData()); - + httpResponse response = _restApi->put(_originalStateProperties); if ( response.error() ) { Warning (_log, "%s restoring state failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); @@ -363,10 +545,10 @@ QJsonObject LedDeviceWled::discover(const QJsonObject& /*params*/) #ifdef ENABLE_MDNS QString discoveryMethod("mDNS"); deviceList = MdnsBrowser::getInstance().getServicesDiscoveredJson( - MdnsServiceRegister::getServiceType(_activeDeviceType), - MdnsServiceRegister::getServiceNameFilter(_activeDeviceType), - DEFAULT_DISCOVER_TIMEOUT - ); + MdnsServiceRegister::getServiceType(_activeDeviceType), + MdnsServiceRegister::getServiceNameFilter(_activeDeviceType), + DEFAULT_DISCOVER_TIMEOUT + ); devicesDiscovered.insert("discoveryMethod", discoveryMethod); #endif devicesDiscovered.insert("devices", deviceList); @@ -397,27 +579,21 @@ QJsonObject LedDeviceWled::getProperties(const QJsonObject& params) { Warning (_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); } - - QJsonObject propertiesDetails = response.getBody().object(); - - semver::version currentVersion {""}; - if (currentVersion.setVersion(propertiesDetails.value("ver").toString().toStdString())) + else { - semver::version ddpVersion{WLED_VERSION_DDP}; - if (currentVersion < ddpVersion) + QJsonObject propertiesDetails = response.getBody().object(); + + _wledInfo = propertiesDetails.value(API_PATH_INFO).toObject(); + _currentVersion.setVersion(_wledInfo.value(INFO_VER).toString().toStdString()); + if (!isReadyForDDPStreaming(_currentVersion)) { - Warning(_log, "DDP streaming not supported by your WLED device version [%s], minimum version expected [%s]. Fall back to UDP-Streaming (%d LEDs max)", currentVersion.getVersion().c_str(), ddpVersion.getVersion().c_str(), UDP_MAX_LED_NUM); if (!propertiesDetails.isEmpty()) { propertiesDetails.insert("maxLedCount", UDP_MAX_LED_NUM); } } - else - { - Info(_log, "DDP streaming is supported by your WLED device version [%s]. No limitation in number of LEDs.", currentVersion.getVersion().c_str()); - } + properties.insert("properties", propertiesDetails); } - properties.insert("properties", propertiesDetails); } DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData() ); @@ -441,8 +617,23 @@ void LedDeviceWled::identify(const QJsonObject& params) _isRestoreOrigState = true; storeState(); - QString request = getOnOffRequest(true) + "," + getLorRequest(1) + "," + getEffectRequest(25); - sendStateUpdateRequest(request); + QJsonObject cmd; + + cmd.insert(STATE_ON, true); + cmd.insert(STATE_LOR, 1); + + _streamSegmentId = params[CONFIG_STREAM_SEGMENT_ID].toInt(0); + + QJsonObject segment; + segment = getSegmentObject(_streamSegmentId, true, BRI_MAX); + segment.insert(STATE_SEG_FX, 25); + segment.insert(STATE_SEG_SX, 128); + + QJsonArray segments; + segments.append(segment); + cmd.insert(STATE_SEG, segments); + + sendStateUpdateRequest(cmd,"Identify"); wait(DEFAULT_IDENTIFY_TIME); diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.h b/libsrc/leddevice/dev_net/LedDeviceWled.h index 5b5f9940..7f6b25ed 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.h +++ b/libsrc/leddevice/dev_net/LedDeviceWled.h @@ -7,6 +7,7 @@ #include "LedDeviceUdpDdp.h" #include "LedDeviceUdpRaw.h" +#include /// /// Implementation of a WLED-device /// @@ -146,20 +147,13 @@ private: /// bool openRestAPI(); - /// - /// @brief Get command to power WLED-device on or off - /// - /// @param isOn True, if to switch on device - /// @return Command to switch device on/off - /// - QString getOnOffRequest (bool isOn ) const; + QJsonObject getUdpnObject(bool send, bool recv) const; + QJsonObject getSegmentObject(int segmentId, bool isOn, int brightness=-1) const; - QString getBrightnessRequest (int bri ) const; - QString getEffectRequest(int effect, int speed=128) const; - QString getLorRequest(int lor) const; - QString getUdpnRequest(bool send, bool recv) const; + bool sendStateUpdateRequest(const QJsonObject &request, const QString requestType = ""); - bool sendStateUpdateRequest(const QString &request); + bool isReadyForSegmentStreaming(semver::version& version) const; + bool isReadyForDDPStreaming(semver::version& version) const; QString resolveAddress (const QString& hostName); @@ -169,8 +163,11 @@ private: QString _hostAddress; int _apiPort; + QJsonObject _wledInfo; QJsonObject _originalStateProperties; + semver::version _currentVersion; + bool _isBrightnessOverwrite; int _brightness; @@ -179,6 +176,10 @@ private: bool _originalStateUdpnRecv; bool _isStreamDDP; + + int _streamSegmentId; + bool _isSwitchOffOtherSegments; + bool _isStreamToSegment; }; #endif // LEDDEVICEWLED_H diff --git a/libsrc/leddevice/dev_serial/ProviderRs232.cpp b/libsrc/leddevice/dev_serial/ProviderRs232.cpp index 9b2b1197..ea4f99b9 100644 --- a/libsrc/leddevice/dev_serial/ProviderRs232.cpp +++ b/libsrc/leddevice/dev_serial/ProviderRs232.cpp @@ -207,12 +207,12 @@ bool ProviderRs232::tryOpen(int delayAfterConnect_ms) return _rs232Port.isOpen(); } -void ProviderRs232::setInError(const QString& errorMsg) +void ProviderRs232::setInError(const QString& errorMsg, bool isRecoverable) { _rs232Port.clearError(); this->close(); - LedDevice::setInError( errorMsg ); + LedDevice::setInError( errorMsg, isRecoverable ); } int ProviderRs232::writeBytes(const qint64 size, const uint8_t *data) diff --git a/libsrc/leddevice/dev_serial/ProviderRs232.h b/libsrc/leddevice/dev_serial/ProviderRs232.h index 36348b55..2b02a815 100644 --- a/libsrc/leddevice/dev_serial/ProviderRs232.h +++ b/libsrc/leddevice/dev_serial/ProviderRs232.h @@ -119,9 +119,10 @@ protected slots: /// /// @brief Set device in error state /// - /// @param errorMsg The error message to be logged + /// @param[in] errorMsg The error message to be logged + /// @param[in] isRecoverable If False, no further retries will be done /// - void setInError( const QString& errorMsg) override; + void setInError( const QString& errorMsg, bool isRecoverable=true) override; /// /// @brief Handle any feedback provided by the device diff --git a/libsrc/leddevice/schemas/schema-wled.json b/libsrc/leddevice/schemas/schema-wled.json index 125b7d99..b8ea52fe 100644 --- a/libsrc/leddevice/schemas/schema-wled.json +++ b/libsrc/leddevice/schemas/schema-wled.json @@ -35,6 +35,46 @@ "access": "expert", "propertyOrder": 3 }, + "segments": { + "type": "object", + "title": "Segment streaming", + "required": false, + "properties": { + "segmentList": { + "type": "string", + "title": "edt_dev_spec_segments_title", + "enum": [ "-1" ], + "default": "-1", + "options": { + "enum_titles": [ "edt_dev_spec_segments_disabled_title" ] + }, + "propertyOrder": 1 + }, + "streamSegmentId": { + "type": "integer", + "title": "edt_dev_spec_segmentId_title", + "default": -1, + "minimum": -1, + "maximum": 16, + "options": { + "hidden": true + }, + "access": "expert", + "propertyOrder": 2 + }, + "switchOffOtherSegments": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_segmentsSwitchOffOthers_title", + "default": true, + "required": true, + "access": "advanced", + "propertyOrder": 3 + } + }, + "propertyOrder": 4, + "additionalProperties": false + }, "restoreOriginalState": { "type": "boolean", "format": "checkbox", @@ -44,7 +84,18 @@ "options": { "infoText": "edt_dev_spec_restoreOriginalState_title_info" }, - "propertyOrder": 4 + "propertyOrder": 7 + }, + "stayOnAfterStreaming": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_stayOnAfterStreaming_title", + "default": false, + "required": true, + "options": { + "infoText": "edt_dev_spec_stayOnAfterStreaming_title_info" + }, + "propertyOrder": 8 }, "overwriteSync": { "type": "boolean", @@ -53,7 +104,7 @@ "default": true, "required": true, "access": "advanced", - "propertyOrder": 5 + "propertyOrder": 9 }, "overwriteBrightness": { "type": "boolean", @@ -62,7 +113,7 @@ "default": true, "required": true, "access": "advanced", - "propertyOrder": 6 + "propertyOrder": 10 }, "brightness": { "type": "integer", @@ -76,7 +127,7 @@ } }, "access": "advanced", - "propertyOrder": 7 + "propertyOrder": 11 }, "latchTime": { "type": "integer", @@ -89,8 +140,8 @@ "options": { "infoText": "edt_dev_spec_latchtime_title_info" }, - "propertyOrder": 8 + "propertyOrder": 12 } }, - "additionalProperties": true -} + "additionalProperties": true + }