From 4f1b95ec83f2465197f9c9fbd0c84d1593ffedc8 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Sun, 25 Aug 2024 17:34:27 +0200 Subject: [PATCH] Add Home Assistant Lights support (#1763) * New HomeAssistant LEDDevice * Fix typos * Ping Qt for Windows to 6.7 until aqtinstaller is fixed * Fix HA default port handling * HA - Update default latchtime and range * Add HA Wizard and light selection * Naming consistency * Fix "Selected Hyperion instance is not running" * CodeQL findings * HA - allow to overwrite brightness by HA yes or no * HA - Support switch off on black * HA - Add transition time --- .github/workflows/qt5_6.yml | 1 - assets/webconfig/i18n/en.json | 13 +- assets/webconfig/js/content_index.js | 2 +- assets/webconfig/js/content_leds.js | 123 ++++- assets/webconfig/js/ui_utils.js | 30 +- assets/webconfig/js/wizard.js | 25 +- .../webconfig/js/wizards/LedDevice_atmoorb.js | 15 +- .../wizards/LedDevice_layoutLedPositions.js | 74 +++ .../js/wizards/LedDevice_philipshue.js | 15 +- .../webconfig/js/wizards/LedDevice_utils.js | 11 + .../js/wizards/LedDevice_yeelight.js | 15 +- include/mdns/MdnsServiceRegister.h | 1 + libsrc/api/JsonAPI.cpp | 2 +- libsrc/leddevice/LedDeviceSchemas.qrc | 1 + .../dev_net/LedDeviceHomeAssistant.cpp | 446 ++++++++++++++++++ .../dev_net/LedDeviceHomeAssistant.h | 181 +++++++ .../leddevice/dev_net/LedDevicePhilipsHue.cpp | 4 +- libsrc/leddevice/dev_net/LedDeviceRazer.cpp | 2 +- .../schemas/schema-homeassistant.json | 135 ++++++ 19 files changed, 1020 insertions(+), 76 deletions(-) create mode 100644 assets/webconfig/js/wizards/LedDevice_layoutLedPositions.js create mode 100644 libsrc/leddevice/dev_net/LedDeviceHomeAssistant.cpp create mode 100644 libsrc/leddevice/dev_net/LedDeviceHomeAssistant.h create mode 100644 libsrc/leddevice/schemas/schema-homeassistant.json diff --git a/.github/workflows/qt5_6.yml b/.github/workflows/qt5_6.yml index 203d05535..eab564b8d 100644 --- a/.github/workflows/qt5_6.yml +++ b/.github/workflows/qt5_6.yml @@ -194,7 +194,6 @@ jobs: version: ${{ inputs.qt_version == '6' && '6.7' || '5.15.*' }} target: 'desktop' modules: ${{ inputs.qt_version == '6' && 'qtserialport' || '' }} - arch: 'win64_msvc2019_64' cache: 'true' cache-key-prefix: 'cache-qt-windows' diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 186262492..3e35b1e4d 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -85,6 +85,7 @@ "conf_leds_layout_cl_bottomleft": "Bottom Left (Corner)", "conf_leds_layout_cl_bottomright": "Bottom Right (Corner)", "conf_leds_layout_cl_cornergap": "Corner Gap", + "conf_leds_layout_cl_disabled": "Deactivated", "conf_leds_layout_cl_edgegap": "Edge Gap", "conf_leds_layout_cl_entertainment": "Entertainment Area", "conf_leds_layout_cl_entertainment_center": "Entertainment Area Center", @@ -103,6 +104,7 @@ "conf_leds_layout_cl_lightPosBottomLeft112": "Bottom: 0 - 50% from Left", "conf_leds_layout_cl_lightPosBottomLeft121": "Bottom: 50 - 100% from Left", "conf_leds_layout_cl_lightPosBottomLeftNewMid": "Bottom: 25 - 75% from Left", + "conf_leds_layout_cl_lightPosEntire": "Whole picture", "conf_leds_layout_cl_lightPosTopLeft112": "Top: 0 - 50% from Left", "conf_leds_layout_cl_lightPosTopLeft121": "Top: 50 - 100% from Left", "conf_leds_layout_cl_lightPosTopLeftNewMid": "Top: 25 - 75% from Left", @@ -661,13 +663,14 @@ "edt_dev_spec_colorComponent_title": "Colour component", "edt_dev_spec_debugLevel_title": "Debug Level", "edt_dev_spec_delayAfterConnect_title": "Delay after connect", - "edt_dev_spec_devices_discovered_none": "No Devices Discovered", - "edt_dev_spec_devices_discovered_title": "Devices Discovered", + "edt_dev_spec_devices_discovered_none": "No Devices discovered", + "edt_dev_spec_devices_discovered_title": "Devices discovered", "edt_dev_spec_devices_discovered_title_info": "Select your LED-Device discovered", "edt_dev_spec_devices_discovered_title_info_custom": "Select your LED-Device discovered or configure a custome one", "edt_dev_spec_devices_discovery_inprogress": "Discovery in progress", "edt_dev_spec_dithering_title": "Dithering", "edt_dev_spec_dmaNumber_title": "DMA channel", + "edt_dev_spec_fullBrightnessAtStart_title": "Full brightness at start", "edt_dev_spec_gamma_title": "Gamma", "edt_dev_spec_globalBrightnessControlMaxLevel_title": "Max Current Level", "edt_dev_spec_globalBrightnessControlThreshold_title": "Adaptive Current Threshold", @@ -685,6 +688,7 @@ "edt_dev_spec_ledType_title": "LED Type", "edt_dev_spec_lightid_itemtitle": "ID", "edt_dev_spec_lightid_title": "Light ID(s)", + "edt_dev_spec_lights_discovered_none": "No Lights discovered", "edt_dev_spec_lights_itemtitle": "Light", "edt_dev_spec_lights_name": "Name", "edt_dev_spec_lights_title": "Light(s)", @@ -1184,9 +1188,10 @@ "wiz_identify_tip": "Identify configured device by lighting it up", "wiz_identify_light": "Identify $1", "wiz_layout": "Generate Layout", + "wiz_layout_led_position_title": "LED position", + "wiz_layout_led_positions_title": "LED position layout wizard", + "wiz_layout_led_positions_expl": "Select the LED position for the $1 controller lights.", "wiz_layout_tip": "Generate a layout for the configured device", - "wiz_ids_disabled": "Deactivated", - "wiz_ids_entire": "Whole picture", "wiz_nanoleaf_failure_auth_token": "Please press the Nanoleaf Power On/Off button within 30 seconds", "wiz_nanoleaf_failure_auth_token_t": "User authorization token generating timeout", "wiz_nanoleaf_press_onoff_button": "Please press the Power On/Off button on your Nanoleaf device for 5-7 seconds", diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 6609b3bd9..70d9ce184 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -197,7 +197,7 @@ $(document).ready(function () { removeStorage("loginToken"); requestRequiresDefaultPasswortChange(); } - else if (event.reason == "Selected Hyperion instance isn't running") { + else if (event.reason == "Selected Hyperion instance is not running") { //Switch to default instance instanceSwitch(0); } else { diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 9ddf7bcfe..fbc1d2895 100755 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -22,7 +22,7 @@ var devSPI = ['apa102', 'apa104', 'ws2801', 'lpd6803', 'lpd8806', 'p9813', 'sk68 var devFTDI = ['apa102_ftdi', 'sk6812_ftdi', 'ws2812_ftdi']; var devRPiPWM = ['ws281x']; var devRPiGPIO = ['piblaster']; -var devNET = ['atmoorb', 'cololight', 'fadecandy', 'philipshue', 'nanoleaf', 'razer', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udpddp', 'udph801', 'udpraw', 'wled', 'yeelight']; +var devNET = ['atmoorb', 'cololight', 'fadecandy', 'homeassistant', 'philipshue', 'nanoleaf', 'razer', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udpddp', 'udph801', 'udpraw', 'wled', 'yeelight']; var devSerial = ['adalight', 'dmx', 'atmo', 'sedu', 'tpm2', 'karate']; var devHID = ['hyperionusbasp', 'lightpack', 'paintpack', 'rawhid']; @@ -1100,6 +1100,7 @@ $(document).ready(function () { switch (ledType) { case "wled": case "cololight": + case "homeassistant": case "nanoleaf": showAllDeviceInputOptions("hostList", false); case "apa102": @@ -1279,7 +1280,21 @@ $(document).ready(function () { if (hostList !== "SELECT") { const host = conf_editor.getEditor("root.specificOptions.host").getValue(); const token = conf_editor.getEditor("root.specificOptions.token").getValue(); - if (host !== "" && token !== "") { + if (host !== "" && token !== "" && entityIds) { + canIdentify = true; + canSave = true; + } + } + } + break; + + case "homeassistant": { + const hostList = conf_editor.getEditor("root.specificOptions.hostList").getValue(); + if (hostList !== "SELECT") { + const host = conf_editor.getEditor("root.specificOptions.host").getValue(); + const token = conf_editor.getEditor("root.specificOptions.token").getValue(); + const entityIds = conf_editor.getEditor("root.specificOptions.entityIds").getValue(); + if (host !== "" && token !== "" && entityIds) { canIdentify = true; canSave = true; } @@ -1387,6 +1402,16 @@ $(document).ready(function () { getProperties_device(ledType, host, params); break; + case "homeassistant": + var token = conf_editor.getEditor("root.specificOptions.token").getValue(); + if (token === "") { + return; + } + + params = { host: host, token: token, filter: "states" }; + getProperties_device(ledType, host, params); + break; + case "nanoleaf": $('#btn_wiz_holder').show(); @@ -1552,6 +1577,14 @@ $(document).ready(function () { var host = ""; switch (ledType) { + case "homeassistant": + host = conf_editor.getEditor("root.specificOptions.host").getValue(); + if (host === "") { + return + } + params = { host: host, token: token, filter: "states" }; + break; + case "nanoleaf": host = conf_editor.getEditor("root.specificOptions.host").getValue(); if (host === "") { @@ -1654,6 +1687,16 @@ $(document).ready(function () { default: } }); + + conf_editor.watch('root.specificOptions.entityIds', () => { + var entityIds = conf_editor.getEditor("root.specificOptions.entityIds").getValue(); + if (entityIds.length > 0) { + $('#btn_test_controller').prop('disabled', false); + } else { + $('#btn_test_controller').prop('disabled', true); + } + }); + }); //philipshueentertainment backward fix @@ -1684,7 +1727,7 @@ $(document).ready(function () { else if ($.inArray(ledDevices[idx], devHID) != -1) optArr[4].push(ledDevices[idx]); else if (ledDevices[idx].endsWith("_ftdi")) { - var title = ledDevices[idx].replace('_ftdi',''); + var title = ledDevices[idx].replace('_ftdi', ''); optArr[5].push(ledDevices[idx] + ":" + title); } else @@ -1744,6 +1787,13 @@ $(document).ready(function () { params = { host: host }; break; + case "homeassistant": + var host = conf_editor.getEditor("root.specificOptions.host").getValue(); + var token = conf_editor.getEditor("root.specificOptions.token").getValue(); + const entityIds = conf_editor.getEditor("root.specificOptions.entityIds").getValue(); + params = { host: host, token: token, entity_id: entityIds }; + break; + case "nanoleaf": var host = conf_editor.getEditor("root.specificOptions.host").getValue(); var token = conf_editor.getEditor("root.specificOptions.token").getValue(); @@ -1878,6 +1928,7 @@ function saveLedConfig(genDefLayout = false) { } break; + case "homeassistant": case "nanoleaf": case "wled": case "yeelight": @@ -2311,6 +2362,12 @@ function updateElements(ledType, key) { } break; + case "homeassistant": + updateElementsHomeAssistant(ledType, key); + hardwareLedCount = 1; + conf_editor.getEditor("root.generalOptions.hardwareLedCount").setValue(hardwareLedCount); + break; + case "atmo": case "karate": var ledProperties = devicesProperties[ledType][key]; @@ -2438,6 +2495,63 @@ function validateWledLedCount(hardwareLedCount) { } } +function updateElementsHomeAssistant(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 light selection list values + var enumVals = []; + var enumTitleVals = []; + var enumDefaultVal = []; + + if (devicesProperties[ledType] && devicesProperties[ledType][key]) { + var ledDeviceProperties = devicesProperties[ledType][key]; + + if (!jQuery.isEmptyObject(ledDeviceProperties)) { + if (ledDeviceProperties && ledDeviceProperties.lightEntities) { + + + for (const light of ledDeviceProperties.lightEntities) { + enumVals.push(light.entity_id); + enumTitleVals.push(light.attributes.friendly_name); + } + + } + } + } + + // Select configured device + if (configuredDeviceType == ledType && configuredHost == host) { + let configuredEntityIds = window.serverConfig.device.entityIds; + for (const light of configuredEntityIds) { + if ($.inArray(enumVals, light) != -1) { + enumVals.push(light); + } + enumDefaultVal.push(light); + } + } + + if (enumVals.length < 1) { + enumVals.push("NONE"); + enumTitleVals.push($.i18n('edt_dev_spec_lights_discovered_none')); + } + else { + $('#btn_wiz_holder').show(); + } + + + let addSchemaElements = { + "uniqueItems": true, + "minItems": 1, + "required": true + }; + + updateJsonEditorMultiSelection(conf_editor, 'root.specificOptions', 'entityIds', addSchemaElements, enumVals, enumTitleVals, enumDefaultVal); +} + function updateElementsWled(ledType, key) { // Get configured device's details @@ -2533,6 +2647,7 @@ function updateElementsWled(ledType, key) { } showInputOptionForItem(conf_editor, "root.specificOptions.segments", "switchOffOtherSegments", showAdditionalOptions); } + function sortByPanelCoordinates(arr, topToBottom, leftToRight) { arr.sort((a, b) => { //Nanoleaf corodinates start at bottom left, therefore reverse topToBottom @@ -2591,7 +2706,7 @@ function nanoleafGeneratelayout(panelLayout, panelOrderTopDown, panelOrderLeftRi 29: { name: "4DLightstrip", led: true, sideLengthX: 50, sideLengthY: 50 }, 30: { name: "Skylight Panel", led: true, sideLengthX: 180, sideLengthY: 180 }, 31: { name: "SkylightControllerPrimary", led: true, sideLengthX: 180, sideLengthY: 180 }, - 32: { name: "SkylightControllerPassive", led: true, sideLengthX: 180, sideLengthY: 180 }, + 32: { name: "SkylightControllerPassive", led: true, sideLengthX: 180, sideLengthY: 180 }, 999: { name: "Unknown", led: true, sideLengthX: 100, sideLengthY: 100 } }; diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 4bfeb85e0..578e31287 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -321,7 +321,7 @@ function showInfoDialog(type, header, message) { $(document).on('click', '[data-dismiss-modal]', function () { var target = $(this).data('dismiss-modal'); $($.find(target)).modal('hide'); -}); + }); } function createHintH(type, text, container) { @@ -478,7 +478,7 @@ function createJsonEditor(container, schema, setconfig, usePanel, arrayre) { return editor; } -function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { +function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVals, newTitleVals, newDefaultVal, addSelect, addCustom, addCustomAsFirst, customText) { var editor = rootEditor.getEditor(path); var orginalProperties = editor.schema.properties[key]; @@ -516,8 +516,8 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa if (addCustom) { - if (newTitelVals.length === 0) { - newTitelVals = [...newEnumVals]; + if (newTitleVals.length === 0) { + newTitleVals = [...newEnumVals]; } if (!!!customText) { @@ -526,10 +526,10 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa if (addCustomAsFirst) { newEnumVals.unshift("CUSTOM"); - newTitelVals.unshift(customText); + newTitleVals.unshift(customText); } else { newEnumVals.push("CUSTOM"); - newTitelVals.push(customText); + newTitleVals.push(customText); } if (newSchema[key].options.infoText) { @@ -540,7 +540,7 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa if (addSelect) { newEnumVals.unshift("SELECT"); - newTitelVals.unshift("edt_conf_enum_please_select"); + newTitleVals.unshift("edt_conf_enum_please_select"); newDefaultVal = "SELECT"; } @@ -548,8 +548,8 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa newSchema[key]["enum"] = newEnumVals; } - if (newTitelVals) { - newSchema[key]["options"]["enum_titles"] = newTitelVals; + if (newTitleVals) { + newSchema[key]["options"]["enum_titles"] = newTitleVals; } if (newDefaultVal) { newSchema[key]["default"] = newDefaultVal; @@ -572,7 +572,7 @@ function updateJsonEditorSelection(rootEditor, path, key, addElements, newEnumVa rootEditor.notifyWatchers(path + "." + key); } -function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitelVals, newDefaultVal) { +function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newEnumVals, newTitleVals, newDefaultVal) { var editor = rootEditor.getEditor(path); var orginalProperties = editor.schema.properties[key]; @@ -617,8 +617,8 @@ function updateJsonEditorMultiSelection(rootEditor, path, key, addElements, newE newSchema[key]["items"]["enum"] = newEnumVals; } - if (newTitelVals) { - newSchema[key]["items"]["options"]["enum_titles"] = newTitelVals; + if (newTitleVals) { + newSchema[key]["items"]["options"]["enum_titles"] = newTitleVals; } if (newDefaultVal) { @@ -923,8 +923,8 @@ function createTableRow(list, head, align) { el.style.verticalAlign = "middle"; var purifyConfig = { - ADD_TAGS: ['button'], - ADD_ATTR: ['onclick'] + ADD_TAGS: ['button'], + ADD_ATTR: ['onclick'] }; el.innerHTML = DOMPurify.sanitize(list[i], purifyConfig); row.appendChild(el); @@ -1403,7 +1403,7 @@ function loadScript(src, callback, ...params) { if (isScriptLoaded(src)) { debugMessage('Script ' + src + ' already loaded'); if (callback && typeof callback === 'function') { - callback( ...params); + callback(...params); } return; } diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index 2524924f9..220888ef3 100755 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -37,27 +37,37 @@ function createLedDeviceWizards(ledType) { $('#btn_led_device_wiz').off(); if (ledType == "philipshue") { $('#btn_wiz_holder').show(); - data = { ledType }; + wizardName = ledType; + data = { wizardName }; title = 'wiz_hue_title'; } else if (ledType == "nanoleaf") { $('#btn_wiz_holder').hide(); - data = { ledType }; + wizardName = ledType; + data = { wizardName }; title = 'wiz_nanoleaf_user_auth_title'; } + else if (ledType == "homeassistant") { + $('#btn_wiz_holder').hide(); + wizardName = "layoutLedPositions"; + data = { wizardName, ledType }; + title = 'wiz_layout_led_positions_title'; + } else if (ledType == "atmoorb") { $('#btn_wiz_holder').show(); - data = { ledType }; + wizardName = ledType; + data = { wizardName }; title = 'wiz_atmoorb_title'; } else if (ledType == "yeelight") { $('#btn_wiz_holder').show(); - data = { ledType }; + wizardName = ledType; + data = { wizardName }; title = 'wiz_yeelight_title'; } if (Object.keys(data).length !== 0) { - startLedDeviceWizard(data, title, ledType + "Wizard"); + startLedDeviceWizard(data, title, wizardName + "Wizard"); } } @@ -66,8 +76,7 @@ function startLedDeviceWizard(data, hint, wizardName) { createHint("wizard", $.i18n(hint), "btn_wiz_holder", "btn_led_device_wiz"); $('#btn_led_device_wiz').off(); $('#btn_led_device_wiz').on('click', async (e) => { - const { [wizardName]: winzardObject } = await import('./wizards/LedDevice_' + data.ledType + '.js'); - winzardObject.start(e); + const { [wizardName]: winzardObject } = await import('./wizards/LedDevice_' + data.wizardName + '.js'); + winzardObject.start(e, data); }); } - diff --git a/assets/webconfig/js/wizards/LedDevice_atmoorb.js b/assets/webconfig/js/wizards/LedDevice_atmoorb.js index 67d9bd5a4..768bdda51 100644 --- a/assets/webconfig/js/wizards/LedDevice_atmoorb.js +++ b/assets/webconfig/js/wizards/LedDevice_atmoorb.js @@ -151,17 +151,7 @@ const atmoorbWizard = (() => { $('#wh_topcontainer').toggle(false); $('#orb_ids_t, #btn_wiz_save').toggle(true); - const lightOptions = [ - "top", "topleft", "topright", - "bottom", "bottomleft", "bottomright", - "left", "lefttop", "leftmiddle", "leftbottom", - "right", "righttop", "rightmiddle", "rightbottom", - "entire", - "lightPosTopLeft112", "lightPosTopLeftNewMid", "lightPosTopLeft121", - "lightPosBottomLeft14", "lightPosBottomLeft12", "lightPosBottomLeft34", "lightPosBottomLeft11", - "lightPosBottomLeft112", "lightPosBottomLeftNewMid", "lightPosBottomLeft121" - ]; - + const lightOptions = utils.getLayoutPositions(); lightOptions.unshift("disabled"); $('.lidsb').html(""); @@ -178,10 +168,9 @@ const atmoorbWizard = (() => { let options = ""; for (const opt in lightOptions) { const val = lightOptions[opt]; - const txt = (val !== 'entire' && val !== 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_'; options += ''; } let enabled = 'enabled'; diff --git a/assets/webconfig/js/wizards/LedDevice_layoutLedPositions.js b/assets/webconfig/js/wizards/LedDevice_layoutLedPositions.js new file mode 100644 index 000000000..d316713f6 --- /dev/null +++ b/assets/webconfig/js/wizards/LedDevice_layoutLedPositions.js @@ -0,0 +1,74 @@ +//**************************** +// Wizard LED Layout +//**************************** + +import { ledDeviceWizardUtils as utils } from './LedDevice_utils.js'; + +const layoutLedPositionsWizard = (() => { + + let wiz_editor; + + function createEditor() { + wiz_editor = createJsonEditor('editor_container_wiz', { + layoutPosition: { + "type": "string", + "title": "wiz_layout_led_position_title", + "enum": utils.getLayoutPositions(), + "options": { + "enum_titles": utils.getLayoutPositionsTitles() + } + } + }, true, true); + } + + function stopWizardLedLayout(reload) { + resetWizard(reload); + } + + function beginWizardLayoutLedPositions() { + createEditor(); + setStorage("wizardactive", true); + + $('#btn_wiz_abort').off().on('click', function () { + stopWizardLedLayout(true); + }); + + $('#btn_wiz_ok').off().on('click', function () { + const layoutPosition = wiz_editor.getEditor("root.layoutPosition").getValue(); + const layoutObject = utils.assignLightPos(layoutPosition); + + var layoutObjects = []; + layoutObjects.push(JSON.parse(JSON.stringify(layoutObject))); + aceEdt.set(layoutObjects); + + stopWizardLedLayout(true); + }); + } + + return { + start: function (e, data) { + $('#wiz_header').html('' + $.i18n('wiz_layout_led_positions_title')); + $('#wizp1_body').html('
' + $.i18n('wiz_layout_led_positions_expl', data.ledType) + '

' + + '
' + ); + $('#wizp1_footer').html('' + ); + + if (getStorage("darkMode") == "on") + $('#wizard_logo').attr("src", 'img/hyperion/logo_negativ.png'); + + //open modal + $("#wizard_modal").modal({ + backdrop: "static", + keyboard: false, + show: true + }); + + beginWizardLayoutLedPositions(); + } + }; +})(); + +export { layoutLedPositionsWizard }; + diff --git a/assets/webconfig/js/wizards/LedDevice_philipshue.js b/assets/webconfig/js/wizards/LedDevice_philipshue.js index bfc33bd8b..8c1d4c14d 100644 --- a/assets/webconfig/js/wizards/LedDevice_philipshue.js +++ b/assets/webconfig/js/wizards/LedDevice_philipshue.js @@ -794,17 +794,7 @@ const philipshueWizard = (() => { } $('#hue_ids_t, #btn_wiz_save').toggle(true); - const lightOptions = [ - "top", "topleft", "topright", - "bottom", "bottomleft", "bottomright", - "left", "lefttop", "leftmiddle", "leftbottom", - "right", "righttop", "rightmiddle", "rightbottom", - "entire", - "lightPosTopLeft112", "lightPosTopLeftNewMid", "lightPosTopLeft121", - "lightPosBottomLeft14", "lightPosBottomLeft12", "lightPosBottomLeft34", "lightPosBottomLeft11", - "lightPosBottomLeft112", "lightPosBottomLeftNewMid", "lightPosBottomLeft121" - ]; - + const lightOptions = utils.getLayoutPositions(); if (isEntertainmentReady && hueEntertainmentConfigs.length > 0) { lightOptions.unshift("entertainment_center"); lightOptions.unshift("entertainment"); @@ -866,10 +856,9 @@ const philipshueWizard = (() => { let options = ""; for (const opt in lightOptions) { const val = lightOptions[opt]; - const txt = (val != 'entire' && val != 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_'; options += ''; } $('.lidsb').append(createTableRow([id + ' (' + lightName + ')', diff --git a/assets/webconfig/js/wizards/LedDevice_utils.js b/assets/webconfig/js/wizards/LedDevice_utils.js index 1f3eab3ee..a1f054717 100644 --- a/assets/webconfig/js/wizards/LedDevice_utils.js +++ b/assets/webconfig/js/wizards/LedDevice_utils.js @@ -52,6 +52,17 @@ const ledDeviceWizardUtils = (() => { const i = positionMap[pos] || positionMap["lightPosEntire"]; i.name = name; return i; + }, + getLayoutPositions: function () { + return Object.keys(positionMap); + }, + getLayoutPositionsTitles: function () { + + let layoutPositionTitles = []; + for (const layoutPosition of Object.keys(positionMap)) { + layoutPositionTitles.push($.i18n('conf_leds_layout_cl_' + layoutPosition)); + } + return layoutPositionTitles; } }; diff --git a/assets/webconfig/js/wizards/LedDevice_yeelight.js b/assets/webconfig/js/wizards/LedDevice_yeelight.js index 4f53eb076..2be0f91c1 100644 --- a/assets/webconfig/js/wizards/LedDevice_yeelight.js +++ b/assets/webconfig/js/wizards/LedDevice_yeelight.js @@ -173,17 +173,7 @@ const yeelightWizard = (() => { $('#wh_topcontainer').toggle(false); $('#yee_ids_t, #btn_wiz_save').toggle(true); - const lightOptions = [ - "top", "topleft", "topright", - "bottom", "bottomleft", "bottomright", - "left", "lefttop", "leftmiddle", "leftbottom", - "right", "righttop", "rightmiddle", "rightbottom", - "entire", - "lightPosTopLeft112", "lightPosTopLeftNewMid", "lightPosTopLeft121", - "lightPosBottomLeft14", "lightPosBottomLeft12", "lightPosBottomLeft34", "lightPosBottomLeft11", - "lightPosBottomLeft112", "lightPosBottomLeftNewMid", "lightPosBottomLeft121" - ]; - + const lightOptions = utils.getLayoutPositions(); lightOptions.unshift("disabled"); $('.lidsb').html(""); @@ -200,10 +190,9 @@ const yeelightWizard = (() => { let options = ""; for (const opt in lightOptions) { const val = lightOptions[opt]; - const txt = (val !== 'entire' && val !== 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_'; options += ''; } let enabled = 'enabled'; diff --git a/include/mdns/MdnsServiceRegister.h b/include/mdns/MdnsServiceRegister.h index 33bf7057b..32980cc94 100644 --- a/include/mdns/MdnsServiceRegister.h +++ b/include/mdns/MdnsServiceRegister.h @@ -22,6 +22,7 @@ const MdnsServiceMap mDnsServiceMap = { //LED Devices {"cololight" , {"_hap._tcp.local.", "ColoLight.*"}}, + {"homeassistant", {"_home-assistant._tcp.local.", ".*"}}, {"nanoleaf" , {"_nanoleafapi._tcp.local.", ".*"}}, {"philipshue" , {"_hue._tcp.local.", ".*"}}, {"wled" , {"_wled._tcp.local.", ".*"}}, diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index ff4c68417..d92eab799 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -735,7 +735,7 @@ void JsonAPI::handleConfigSetCommand(const QJsonObject &message, const JsonApiCo } else { - sendErrorReply("Saving configuration while Hyperion is disabled isn't possible", cmd); + sendErrorReply("It is not possible saving a configuration while Hyperion is disabled", cmd); } } } diff --git a/libsrc/leddevice/LedDeviceSchemas.qrc b/libsrc/leddevice/LedDeviceSchemas.qrc index d2a93fb55..7c1796501 100644 --- a/libsrc/leddevice/LedDeviceSchemas.qrc +++ b/libsrc/leddevice/LedDeviceSchemas.qrc @@ -7,6 +7,7 @@ schemas/schema-dmx.json schemas/schema-fadecandy.json schemas/schema-file.json + schemas/schema-homeassistant.json schemas/schema-hyperionusbasp.json schemas/schema-lightpack.json schemas/schema-lpd6803.json diff --git a/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.cpp b/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.cpp new file mode 100644 index 000000000..df4c1de19 --- /dev/null +++ b/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.cpp @@ -0,0 +1,446 @@ +// Local-Hyperion includes +#include "LedDeviceHomeAssistant.h" + +#include +// mDNS discover +#ifdef ENABLE_MDNS +#include +#include +#endif +#include +#include + +#include + +// Constants +namespace { +const bool verbose = false; + +// Configuration settings +const char CONFIG_HOST[] = "host"; +const char CONFIG_PORT[] = "port"; +const char CONFIG_AUTH_TOKEN[] = "token"; +const char CONFIG_ENITYIDS[] = "entityIds"; +const char CONFIG_BRIGHTNESS[] = "brightness"; +const char CONFIG_BRIGHTNESS_OVERWRITE[] = "overwriteBrightness"; +const char CONFIG_FULL_BRIGHTNESS_AT_START[] = "fullBrightnessAtStart"; +const char CONFIG_ON_OFF_BLACK[] = "switchOffOnBlack"; +const char CONFIG_TRANSITIONTIME[] = "transitionTime"; + +const bool DEFAULT_IS_BRIGHTNESS_OVERWRITE = true; +const bool DEFAULT_IS_FULL_BRIGHTNESS_AT_START = true; +const int BRI_MAX = 255; +const bool DEFAULT_IS_SWITCH_OFF_ON_BLACK = false; + +// Home Assistant API +const int API_DEFAULT_PORT = 8123; +const char API_BASE_PATH[] = "/api/"; +const char API_STATES[] = "states"; +const char API_LIGHT_TURN_ON[] = "services/light/turn_on"; +const char API_LIGHT_TURN_OFF[] = "services/light/turn_off"; + +const char ENTITY_ID[] = "entity_id"; +const char RGB_COLOR[] = "rgb_color"; +const char BRIGHTNESS[] = "brightness"; +const char TRANSITION[] = "transition"; +const char FLASH[] = "flash"; + +// // Home Assistant ssdp services +const char SSDP_ID[] = "ssdp:all"; +const char SSDP_FILTER_HEADER[] = "ST"; +const char SSDP_FILTER[] = "(.*)home-assistant.io(.*)"; + +} //End of constants + +LedDeviceHomeAssistant::LedDeviceHomeAssistant(const QJsonObject& deviceConfig) + : LedDevice(deviceConfig) + , _restApi(nullptr) + , _apiPort(API_DEFAULT_PORT) + , _isBrightnessOverwrite(DEFAULT_IS_BRIGHTNESS_OVERWRITE) + , _isFullBrightnessAtStart(DEFAULT_IS_FULL_BRIGHTNESS_AT_START) + , _brightness (BRI_MAX) +{ +#ifdef ENABLE_MDNS + QMetaObject::invokeMethod(MdnsBrowser::getInstance().data(), "browseForServiceType", + Qt::QueuedConnection, Q_ARG(QByteArray, MdnsServiceRegister::getServiceType(_activeDeviceType))); +#endif +} + +LedDevice* LedDeviceHomeAssistant::construct(const QJsonObject& deviceConfig) +{ + return new LedDeviceHomeAssistant(deviceConfig); +} + +LedDeviceHomeAssistant::~LedDeviceHomeAssistant() +{ + delete _restApi; + _restApi = nullptr; +} + +bool LedDeviceHomeAssistant::init(const QJsonObject& deviceConfig) +{ + bool isInitOK{ false }; + + if ( LedDevice::init(deviceConfig) ) + { + // Overwrite non supported/required features + if (deviceConfig["rewriteTime"].toInt(0) > 0) + { + Info(_log, "Home Assistant lights do not require rewrites. Refresh time is ignored."); + setRewriteTime(0); + } + DebugIf(verbose, _log, "deviceConfig: [%s]", QString(QJsonDocument(_devConfig).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + //Set hostname as per configuration and default port + _hostName = deviceConfig[CONFIG_HOST].toString(); + _apiPort = deviceConfig[CONFIG_PORT].toInt(API_DEFAULT_PORT); + _bearerToken = deviceConfig[CONFIG_AUTH_TOKEN].toString(); + + _isBrightnessOverwrite = _devConfig[CONFIG_BRIGHTNESS_OVERWRITE].toBool(DEFAULT_IS_BRIGHTNESS_OVERWRITE); + _isFullBrightnessAtStart = _devConfig[CONFIG_FULL_BRIGHTNESS_AT_START].toBool(DEFAULT_IS_FULL_BRIGHTNESS_AT_START); + _brightness = _devConfig[CONFIG_BRIGHTNESS].toInt(BRI_MAX); + _switchOffOnBlack = _devConfig[CONFIG_ON_OFF_BLACK].toBool(DEFAULT_IS_SWITCH_OFF_ON_BLACK); + int transitionTimeMs = _devConfig[CONFIG_TRANSITIONTIME].toInt(0); + _transitionTime = transitionTimeMs / 1000.0; + + Debug(_log, "Hostname/IP : %s", QSTRING_CSTR(_hostName)); + Debug(_log, "Port : %d", _apiPort ); + + Debug(_log, "Overwrite Brightn.: %s", _isBrightnessOverwrite ? "Yes" : "No" ); + Debug(_log, "Set Brightness to : %d", _brightness); + Debug(_log, "Full Bri. at start: %s", _isFullBrightnessAtStart ? "Yes" : "No" ); + Debug(_log, "Off on Black : %s", _switchOffOnBlack ? "Yes" : "No" ); + Debug(_log, "Transition Time : %d ms", transitionTimeMs ); + + _lightEntityIds = _devConfig[ CONFIG_ENITYIDS ].toVariant().toStringList(); + int configuredLightsCount = _lightEntityIds.size(); + + if ( configuredLightsCount == 0 ) + { + this->setInError( "No light entity-ids configured" ); + isInitOK = false; + } + else + { + Debug(_log, "Lights configured : %d", configuredLightsCount ); + isInitOK = true; + } + } + + return isInitOK; +} + +bool LedDeviceHomeAssistant::initLedsConfiguration() +{ + bool isInitOK = false; + + //Currently on one light is supported + QString lightEntityId = _lightEntityIds[0]; + + //Get properties for configured light entitiy to check availability + _restApi->setPath({ API_STATES, lightEntityId}); + httpResponse response = _restApi->get(); + if (response.error()) + { + QString errorReason = QString("%1 get properties failed with error: '%2'").arg(_activeDeviceType,response.getErrorReason()); + this->setInError(errorReason); + } + else + { + QJsonObject propertiesDetails = response.getBody().object(); + if (propertiesDetails.isEmpty()) + { + QString errorReason = QString("Light [%1] does not exist").arg(lightEntityId); + this->setInError(errorReason); + } + else + { + if (propertiesDetails.value("state").toString().compare("unavailable") == 0) + { + Warning(_log, "Light [%s] is currently unavailable", QSTRING_CSTR(lightEntityId)); + } + isInitOK = true; + } + } + return isInitOK; +} + +bool LedDeviceHomeAssistant::openRestAPI() +{ + bool isInitOK{ true }; + + if (_restApi == nullptr) + { + if (_apiPort == 0) + { + _apiPort = API_DEFAULT_PORT; + } + + _restApi = new ProviderRestApi(_address.toString(), _apiPort); + _restApi->setLogger(_log); + + _restApi->setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + _restApi->setHeader("Authorization", QByteArrayLiteral("Bearer ") + _bearerToken.toUtf8()); + + //Base-path is api-path + _restApi->setBasePath(API_BASE_PATH); + } + return isInitOK; +} + +int LedDeviceHomeAssistant::open() +{ + int retval = -1; + _isDeviceReady = false; + + if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) + { + if (openRestAPI()) + { + // Read LedDevice configuration and validate against device configuration + if (initLedsConfiguration()) + { + // Everything is OK, device is ready + _isDeviceReady = true; + retval = 0; + } + } + else + { + _restApi->setHost(_address.toString()); + _restApi->setPort(_apiPort); + } + } + return retval; +} + +QJsonArray LedDeviceHomeAssistant::discoverSsdp() const +{ + QJsonArray deviceList; + SSDPDiscover ssdpDiscover; + ssdpDiscover.skipDuplicateKeys(true); + ssdpDiscover.setSearchFilter(SSDP_FILTER, SSDP_FILTER_HEADER); + QString searchTarget = SSDP_ID; + + if (ssdpDiscover.discoverServices(searchTarget) > 0) + { + deviceList = ssdpDiscover.getServicesDiscoveredJson(); + } + return deviceList; +} + +QJsonObject LedDeviceHomeAssistant::discover(const QJsonObject& /*params*/) +{ + QJsonObject devicesDiscovered; + devicesDiscovered.insert("ledDeviceType", _activeDeviceType); + + QJsonArray deviceList; + +#ifdef ENABLE_MDNS + QString discoveryMethod("mDNS"); + deviceList = MdnsBrowser::getInstance().data()->getServicesDiscoveredJson( + MdnsServiceRegister::getServiceType(_activeDeviceType), + MdnsServiceRegister::getServiceNameFilter(_activeDeviceType), + DEFAULT_DISCOVER_TIMEOUT + ); +#else + QString discoveryMethod("ssdp"); + deviceList = discoverSsdp(); +#endif + + devicesDiscovered.insert("discoveryMethod", discoveryMethod); + devicesDiscovered.insert("devices", deviceList); + + DebugIf(verbose, _log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + return devicesDiscovered; +} + +QJsonObject LedDeviceHomeAssistant::getProperties(const QJsonObject& params) +{ + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + QJsonObject properties; + + _hostName = params[CONFIG_HOST].toString(""); + _apiPort = API_DEFAULT_PORT; + _bearerToken = params[CONFIG_AUTH_TOKEN].toString(""); + + Info(_log, "Get properties for %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName)); + + if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) + { + if (openRestAPI()) + { + QString filter = params["filter"].toString(""); + _restApi->setPath(filter); + + // Perform request + httpResponse response = _restApi->get(); + if (response.error()) + { + Warning(_log, "%s get properties failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); + } + + QJsonObject propertiesDetails; + const QJsonDocument jsonDoc = response.getBody(); + if (jsonDoc.isArray()) { + const QJsonArray jsonArray = jsonDoc.array(); + QVector filteredVector; + + // Iterate over the array and filter objects with entity_id starting with "light." + for (const QJsonValue &value : jsonArray) + { + QJsonObject obj = value.toObject(); + QString entityId = obj[ENTITY_ID].toString(); + + if (entityId.startsWith("light.")) + { + filteredVector.append(obj); + } + } + + // Sort the filtered vector by "friendly_name" in ascending order + std::sort(filteredVector.begin(), filteredVector.end(), [](const QJsonValue &a, const QJsonValue &b) { + QString nameA = a.toObject()["attributes"].toObject()["friendly_name"].toString(); + QString nameB = b.toObject()["attributes"].toObject()["friendly_name"].toString(); + return nameA < nameB; // Ascending order + }); + // Convert the sorted vector back to a QJsonArray + QJsonArray sortedArray; + for (const QJsonValue &value : filteredVector) { + sortedArray.append(value); + } + + propertiesDetails.insert("lightEntities", sortedArray); + + } + + if (!propertiesDetails.isEmpty()) + { + propertiesDetails.insert("ledCount", 1); + } + properties.insert("properties", propertiesDetails); + } + + DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); + } + return properties; +} + +void LedDeviceHomeAssistant::identify(const QJsonObject& params) +{ + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + _hostName = params[CONFIG_HOST].toString(""); + _apiPort = API_DEFAULT_PORT; + _bearerToken = params[CONFIG_AUTH_TOKEN].toString(""); + + Info(_log, "Identify %s, hostname (%s)", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(_hostName)); + + if (NetUtils::resolveHostToAddress(_log, _hostName, _address, _apiPort)) + { + if (openRestAPI()) + { + QJsonArray lightEntityIds = params[ ENTITY_ID ].toArray(); + + _restApi->setPath(API_LIGHT_TURN_ON); + QJsonObject serviceAttributes{{ENTITY_ID, lightEntityIds}}; + serviceAttributes.insert(FLASH, "short"); + + httpResponse response = _restApi->post(serviceAttributes); + if (response.error()) + { + Warning(_log, "%s identification failed with error: '%s'", QSTRING_CSTR(_activeDeviceType), QSTRING_CSTR(response.getErrorReason())); + } + } + } +} + +bool LedDeviceHomeAssistant::powerOn() +{ + bool isOn = false; + if (_isDeviceReady) + { + _restApi->setPath(API_LIGHT_TURN_ON); + QJsonObject serviceAttributes {{ENTITY_ID, QJsonArray::fromStringList(_lightEntityIds)}}; + + if (_isFullBrightnessAtStart) + { + serviceAttributes.insert(BRIGHTNESS, BRI_MAX); + } + + httpResponse response = _restApi->post(serviceAttributes); + if (response.error()) + { + QString errorReason = QString("Power-on request failed with error: '%1'").arg(response.getErrorReason()); + this->setInError(errorReason); + isOn = false; + } + else { + isOn = true; + } + } + return isOn; +} + +bool LedDeviceHomeAssistant::powerOff() +{ + bool isOff = true; + if (_isDeviceReady) + { + _restApi->setPath(API_LIGHT_TURN_OFF); + QJsonObject serviceAttributes {{ENTITY_ID, QJsonArray::fromStringList(_lightEntityIds)}}; + httpResponse response = _restApi->post(serviceAttributes); + if (response.error()) + { + QString errorReason = QString("Power-off request failed with error: '%1'").arg(response.getErrorReason()); + this->setInError(errorReason); + isOff = false; + } + } + return isOff; +} + +int LedDeviceHomeAssistant::write(const std::vector& ledValues) +{ + int retVal = 0; + + QJsonObject serviceAttributes {{ENTITY_ID, QJsonArray::fromStringList(_lightEntityIds)}}; + ColorRgb ledValue = ledValues.at(0); + + if (_switchOffOnBlack && ledValue == ColorRgb::BLACK) + { + _restApi->setPath(API_LIGHT_TURN_OFF); + } + else + { + // http://hostname:port/api/services/light/turn_on + // { + // "entity_id": [ entity-IDs ], + // "rgb_color": [R,G,B] + // } + + _restApi->setPath(API_LIGHT_TURN_ON); + QJsonArray rgbColor {ledValue.red, ledValue.green, ledValue.blue}; + serviceAttributes.insert(RGB_COLOR, rgbColor); + + if (_isBrightnessOverwrite) + { + serviceAttributes.insert(BRIGHTNESS, _brightness); + } + if (_transitionTime > 0) + { + // Transition time in seconds + serviceAttributes.insert(TRANSITION, _transitionTime); + } + } + + httpResponse response = _restApi->post(serviceAttributes); + if (response.error()) + { + Warning(_log,"Updating lights failed with error: '%s'", QSTRING_CSTR(response.getErrorReason()) ); + retVal = -1; + } + + return retVal; +} diff --git a/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.h b/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.h new file mode 100644 index 000000000..ef4a841de --- /dev/null +++ b/libsrc/leddevice/dev_net/LedDeviceHomeAssistant.h @@ -0,0 +1,181 @@ +#ifndef LEDEVICEHOMEASSISTANT_H +#define LEDEVICEHOMEASSISTANT_H + +// LedDevice includes +#include +#include "ProviderRestApi.h" + +// Qt includes +#include +#include +#include + +/// +/// Implementation of the LedDevice interface for sending to +/// lights made available via the Home Assistant platform. +/// +class LedDeviceHomeAssistant : LedDevice +{ +public: + /// + /// @brief Constructs LED-device for Home Assistant Lights + /// + /// following code shows all configuration options + /// @code + /// "device" : + /// { + /// "type" : "homeassistant" + /// "host" : "hostname or IP", + /// "port" : port + /// "token": "bearer token", + /// }, + ///@endcode + /// + /// @param deviceConfig Device's configuration as JSON-Object + /// + explicit LedDeviceHomeAssistant(const QJsonObject& deviceConfig); + + /// + /// @brief Destructor of the LED-device + /// + ~LedDeviceHomeAssistant() override; + + /// + /// @brief Constructs the LED-device + /// + /// @param[in] deviceConfig Device's configuration as JSON-Object + /// @return LedDevice constructed + static LedDevice* construct(const QJsonObject& deviceConfig); + + /// + /// @brief Discover Home Assistant lights available (for configuration). + /// + /// @param[in] params Parameters used to overwrite discovery default behaviour + /// + /// @return A JSON structure holding a list of devices found + /// + QJsonObject discover(const QJsonObject& params) override; + + /// + /// @brief Get the Home Assistant light's resource properties + /// + /// Following parameters are required + /// @code + /// { + /// "host" : "hostname or IP", + /// "port" : port + /// "token" : "bearer token", + /// "filter": "resource to query", root "/" is used, if empty + /// } + ///@endcode + /// + /// @param[in] params Parameters to query device + /// @return A JSON structure holding the device's properties + /// + QJsonObject getProperties(const QJsonObject& params) override; + + /// + /// @brief Send an update to the Nanoleaf device to identify it. + /// + /// Following parameters are required + /// @code + /// { + /// "host" : "hostname or IP", + /// "port" : port + /// "token" : "bearer token", + /// "entity_id": array of lightIds + /// } + ///@endcode + /// + /// @param[in] params Parameters to address device + /// + void identify(const QJsonObject& params) override; + +protected: + + /// + /// @brief Initialise the Home Assistant light's configuration and network address details + /// + /// @param[in] deviceConfig the JSON device configuration + /// @return True, if success + /// + bool init(const QJsonObject& deviceConfig) override; + + /// + /// @brief Opens the output device. + /// + /// @return Zero on success (i.e. device is ready), else negative + /// + int open() override; + + /// + /// @brief Writes the RGB-Color values to the Home Assistant light. + /// + /// @param[in] ledValues The RGB-color + /// @return Zero on success, else negative + ////// + int write(const std::vector& ledValues) override; + + /// + /// @brief Power-/turn on the Home Assistant light. + /// + /// @brief Store the device's original state. + /// + bool powerOn() override; + + /// + /// @brief Power-/turn off the Home Assistant light. + /// + /// @return True if success + /// + bool powerOff() override; + +private: + + /// + /// @brief Initialise the access to the REST-API wrapper + /// + /// @return True, if success + /// + bool openRestAPI(); + + /// + /// @brief Get Nanoleaf device details and configuration + /// + /// @return True, if Nanoleaf device capabilities fit configuration + /// + bool initLedsConfiguration(); + + /// + /// @brief Discover Home Assistant lights available (for configuration). + /// + /// @return A JSON structure holding a list of devices found + /// + QJsonArray discoverSsdp() const; + + // /// + // /// @brief Get number of panels that can be used as LEds. + // /// + // /// @return Number of usable LED panels + // /// + // int getHwLedCount(const QJsonObject& jsonLayout) const; + + QString _hostName; + QHostAddress _address; + ProviderRestApi* _restApi; + int _apiPort; + QString _bearerToken; + + /// List of the HA light entity_ids. + QStringList _lightEntityIds; + + bool _isBrightnessOverwrite; + bool _isFullBrightnessAtStart; + int _brightness; + bool _switchOffOnBlack; + /// Transition time in seconds + double _transitionTime; + +}; + +#endif // LEDEVICEHOMEASSISTANT_H diff --git a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp index e3df5c7de..54d7bd612 100644 --- a/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp +++ b/libsrc/leddevice/dev_net/LedDevicePhilipsHue.cpp @@ -31,7 +31,7 @@ const char CONFIG_TRANSITIONTIME[] = "transitiontime"; const char CONFIG_BLACK_LIGHTS_TIMEOUT[] = "blackLightsTimeout"; const char CONFIG_ON_OFF_BLACK[] = "switchOffOnBlack"; const char CONFIG_RESTORE_STATE[] = "restoreOriginalState"; -const char CONFIG_lightIdS[] = "lightIds"; +const char CONFIG_LIGHTIDS[] = "lightIds"; const char CONFIG_USE_HUE_API_V2[] = "useAPIv2"; const char CONFIG_USE_HUE_ENTERTAINMENT_API[] = "useEntertainmentAPI"; const char CONFIG_groupId[] = "groupId"; @@ -1849,7 +1849,7 @@ bool LedDevicePhilipsHue::setLights() _useEntertainmentAPI = false; Error(_log, "Group-ID [%s] is not usable - Entertainment API usage was disabled!", QSTRING_CSTR(_groupId) ); } - lights = _devConfig[ CONFIG_lightIdS ].toVariant().toStringList(); + lights = _devConfig[ CONFIG_LIGHTIDS ].toVariant().toStringList(); } _lightIds = lights; diff --git a/libsrc/leddevice/dev_net/LedDeviceRazer.cpp b/libsrc/leddevice/dev_net/LedDeviceRazer.cpp index 6f01098bc..26a116a82 100644 --- a/libsrc/leddevice/dev_net/LedDeviceRazer.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceRazer.cpp @@ -23,7 +23,7 @@ namespace { const char CONFIG_RAZER_DEVICE_TYPE[] = "subType"; const char CONFIG_SINGLE_COLOR[] = "singleColor"; - // WLED JSON-API elements + // API elements const char API_DEFAULT_HOST[] = "localhost"; const int API_DEFAULT_PORT = 54235; diff --git a/libsrc/leddevice/schemas/schema-homeassistant.json b/libsrc/leddevice/schemas/schema-homeassistant.json new file mode 100644 index 000000000..87ad345af --- /dev/null +++ b/libsrc/leddevice/schemas/schema-homeassistant.json @@ -0,0 +1,135 @@ +{ + "type": "object", + "required": true, + "properties": { + "hostList": { + "type": "string", + "title": "edt_dev_spec_devices_discovered_title", + "enum": [ "NONE" ], + "options": { + "enum_titles": [ "edt_dev_spec_devices_discovery_inprogress" ], + "infoText": "edt_dev_spec_devices_discovered_title_info" + }, + "required": true, + "propertyOrder": 1 + }, + "host": { + "type": "string", + "format": "hostname_or_ip", + "title": "edt_dev_spec_targetIpHost_title", + "options": { + "infoText": "edt_dev_spec_targetIpHost_title_info" + }, + "required": true, + "propertyOrder": 2 + }, + "port": { + "type": "integer", + "title": "edt_dev_spec_port_title", + "default": 8123, + "minimum": 0, + "maximum": 65535, + "access": "expert", + "propertyOrder": 3 + }, + "token": { + "type": "string", + "title": "edt_dev_auth_key_title", + "options": { + "infoText": "edt_dev_auth_key_title_info" + }, + "propertyOrder": 4 + }, + "restoreOriginalState": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_restoreOriginalState_title", + "default": true, + "required": true, + "options": { + "hidden": true, + "infoText": "edt_dev_spec_restoreOriginalState_title_info" + }, + "propertyOrder": 5 + }, + "overwriteBrightness": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_brightnessOverwrite_title", + "default": true, + "required": true, + "access": "advanced", + "propertyOrder": 5 + }, + "brightness": { + "type": "integer", + "title": "edt_dev_spec_brightness_title", + "default": 255, + "minimum": 1, + "maximum": 255, + "options": { + "dependencies": { + "overwriteBrightness": true + } + }, + "access": "advanced", + "propertyOrder": 6 + }, + "fullBrightnessAtStart": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_fullBrightnessAtStart_title", + "default": true, + "required": true, + "access": "advanced", + "propertyOrder": 7 + }, + "switchOffOnBlack": { + "type": "boolean", + "format": "checkbox", + "title": "edt_dev_spec_switchOffOnBlack_title", + "default": false, + "access": "advanced", + "propertyOrder": 8 + }, + "transitionTime": { + "type": "integer", + "title": "edt_dev_spec_transistionTime_title", + "default": 0, + "append": "ms", + "minimum": 0, + "maximum": 2000, + "required": false, + "access": "advanced", + "propertyOrder": 9 + }, + "entityIds": { + "title": "edt_dev_spec_lightid_title", + "type": "array", + "required": true, + "format": "select", + "options": { + "hidden": true + }, + "items": { + "type": "string", + "title": "edt_dev_spec_lights_itemtitle" + }, + "propertyOrder": 10 + }, + "latchTime": { + "type": "integer", + "title": "edt_dev_spec_latchtime_title", + "default": 250, + "append": "edt_append_ms", + "minimum": 100, + "maximum": 2000, + "access": "expert", + "options": { + "infoText": "edt_dev_spec_latchtime_title_info" + }, + "propertyOrder": 11 + } + }, + "additionalProperties": true +}