diff --git a/.gitignore b/.gitignore index 029eb5ba9..1a5396933 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,15 @@ NULL # Docker deploy folder deploy/* + +# ccache/buildcache +.*cache/ + +# release-deps/debug-deps +*-deps/ + +# User defined CMake preset file. +CMakeUserPresets.json + +#Configurations created under config for testing +/configs diff --git a/.version b/.version index 402591665..0f238926d 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.0.17-beta.1 +2.0.17-beta.2 diff --git a/.vs/launch.vs.json b/.vs/launch.vs.json index 37627ab1e..d2573ae3d 100644 --- a/.vs/launch.vs.json +++ b/.vs/launch.vs.json @@ -6,12 +6,42 @@ "type": "default", "project": "CMakeLists.txt", "projectTarget": "hyperiond.exe (bin\\hyperiond.exe)", - "name": "Run hyperion with debug option and external console", + "name": "Run Hyperion" + }, + { + "type": "default", + "project": "CMakeLists.txt", + "projectTarget": "hyperiond.exe (bin\\hyperiond.exe)", + "name": "Run hyperion with debug logging and external console", "args": [ "-d", "-c" ], "externalConsole": true + }, + { + "type": "default", + "project": "CMakeLists.txt", + "projectTarget": "hyperiond.exe (bin\\hyperiond.exe)", + "name": "Run hyperion with verbose logging with external console", + "args": [ + "-v", + "-c" + ], + "externalConsole": true + }, + { + "type": "default", + "project": "CMakeLists.txt", + "projectTarget": "hyperiond.exe (bin\\hyperiond.exe)", + "name": "Run hyperion with debug logging and a test configuration DB", + "args": [ + "-d", + "-c", + "-u", + "${workspaceRoot}\\configs\\testConfig" + ], + "externalConsole": true } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index c892f259c..903371037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **JSON-API** - Align JSON subscription update elements. `ledcolors-imagestream-update, ledcolors-ledstream-update, logmsg-update` now return data via `data` and not `result +- Global global configuration elements are now separated form instance specific ones ### Added @@ -18,6 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support gaps on Matrix Layout (#1696) - Windows: Added a new grabber that uses the DXGI DDA (Desktop Duplication API). This has much better performance than the DX grabber as it does more of its work on the GPU. +- Support to import, export and backup Hyperion's full configuration via the UI, JSON-API and commandline (`--importConfig, --exportConfig`) (#804) +- Allow to force starting Hyperion in read-only mode (`--readonlyMode`) +- JSON-API: Support to query for a dedicated set of configuration items for a set of instances +- JSON-API: Support to save a dedicated set of configuration items for a set of instances + **JSON-API** - New subscription support for event updates, i.e. `Suspend, Resume, Idle, idleResume, Restart, Quit`. - Support direct or multiple instance addressing via single requests (#809) @@ -33,12 +39,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed: Kodi Color Calibration, Refactor Wizards (#1674) - Fixed: Token Dialog not closing - Fixed: Philip Hue APIv2 support without Entertainment group defined (#1742) +- Refactored: Database access layer +- Refactored: Hyperion's configuration database is validated before start-up (and migrated, if required) **JSON-API** - Refactored JSON-API to ensure consistent authorization behaviour across sessions and single requests with token authorization. - Provide additional error details with API responses, esp. on JSON parsing, validation or token errors. - Generate random TANs for every API request from the Hyperion UI - Fixed: Handling of IP4 addresses wrapped in IPv6 for external network connections- +- Fixed: Local Admin API Authentication rejects valid tokens (#1251) ### Removed diff --git a/CMakeLists.txt b/CMakeLists.txt index 86d3f786d..f2ca768e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,11 +20,10 @@ PROJECT(hyperion) include (${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.cmake) file (STRINGS ".version" HYPERION_VERSION) SetVersionNumber(HYPERION ${HYPERION_VERSION}) -set(DEFAULT_JSON_CONFIG_FILE ${CMAKE_CURRENT_SOURCE_DIR}/config/hyperion.config.json.default) +set(DEFAULT_JSON_CONFIG_FILE ${CMAKE_CURRENT_SOURCE_DIR}/settings/hyperion.settings.json.default) file(READ ${DEFAULT_JSON_CONFIG_FILE} DEFAULT_JSON_CONFIG_VAR) string(REPLACE "configVersionValue" ${HYPERION_VERSION} DEFAULT_JSON_CONFIG_VAR "${DEFAULT_JSON_CONFIG_VAR}") -string(REPLACE "previousVersionValue" ${HYPERION_VERSION} DEFAULT_JSON_CONFIG_VAR "${DEFAULT_JSON_CONFIG_VAR}") -file(WRITE ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default "${DEFAULT_JSON_CONFIG_VAR}") +file(WRITE ${CMAKE_BINARY_DIR}/settings/hyperion.settings.json.default "${DEFAULT_JSON_CONFIG_VAR}") # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) @@ -227,7 +226,7 @@ message(STATUS "HYPERION_LIGHT = ${HYPERION_LIGHT}") if(HYPERION_LIGHT) message(STATUS "HYPERION_LIGHT: Hyperion is build with a reduced set of functionality.") - # Disable Grabbers + # Disable Screen/Video Grabbers SET ( DEFAULT_AMLOGIC OFF ) SET ( DEFAULT_DISPMANX OFF ) SET ( DEFAULT_DX OFF ) @@ -240,22 +239,37 @@ if(HYPERION_LIGHT) SET ( DEFAULT_X11 OFF ) SET ( DEFAULT_XCB OFF ) + # Disable Audio Grabbers SET ( DEFAULT_AUDIO OFF ) + # LED-Devices + #SET ( DEFAULT_DEV_NETWORK OFF ) + #SET ( DEFAULT_DEV_FTDI OFF ) + #SET ( DEFAULT_DEV_SERIAL OFF ) + #SET ( DEFAULT_DEV_SPI OFF ) + #SET ( DEFAULT_DEV_TINKERFORGE OFF ) + #SET ( DEFAULT_DEV_USB_HID OFF ) + #SET ( DEFAULT_DEV_WS281XPWM OFF ) + # Disable Input Servers - set(DEFAULT_BOBLIGHT_SERVER OFF) - set(DEFAULT_CEC OFF) - set(DEFAULT_FLATBUF_SERVER OFF) - set(DEFAULT_PROTOBUF_SERVER OFF) + SET ( DEFAULT_BOBLIGHT_SERVER OFF ) + SET ( DEFAULT_CEC OFF ) + SET ( DEFAULT_FLATBUF_SERVER OFF ) + SET ( DEFAULT_PROTOBUF_SERVER OFF ) # Disable Output Connectors - set(DEFAULT_FORWARDER OFF) - set(DEFAULT_FLATBUF_CONNECT OFF) + SET ( DEFAULT_FORWARDER OFF ) + SET ( DEFAULT_FLATBUF_CONNECT OFF ) # Disable Services - set(DEFAULT_EFFECTENGINE OFF) + SET ( DEFAULT_EFFECTENGINE OFF ) + #SET ( DEFAULT_EXPERIMENTAL OFF ) + #SET ( DEFAULT_MDNS OFF ) + #SET ( DEFAULT_REMOTE_CTL OFF ) + + #SET ( ENABLE_JSONCHECKS OFF ) + #SET ( ENABLE_DEPLOY_DEPENDENCIES OFF ) endif() - message(STATUS "Grabber options:") addIndent(" - ") @@ -430,7 +444,7 @@ endif() if(ENABLE_JSONCHECKS) # check all json files file (GLOB_RECURSE HYPERION_SCHEMAS RELATIVE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/libsrc/*schema*.json) - set(JSON_FILES ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default ${HYPERION_SCHEMAS}) + set(JSON_FILES ${CMAKE_BINARY_DIR}/settings/hyperion.settings.json.default ${HYPERION_SCHEMAS}) execute_process ( COMMAND ${PYTHON_EXECUTABLE} test/jsonchecks/checkjson.py ${JSON_FILES} @@ -453,7 +467,7 @@ if(ENABLE_JSONCHECKS) endif() execute_process ( - COMMAND ${PYTHON_EXECUTABLE} test/jsonchecks/checkschema.py ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default libsrc/hyperion/hyperion.schema.json + COMMAND ${PYTHON_EXECUTABLE} test/jsonchecks/checkschema.py ${CMAKE_BINARY_DIR}/settings/hyperion.settings.json.default libsrc/hyperion/schema/schema-settings-default.json WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE CHECK_CONFIG_FAILED ) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..ae1743c4b --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,273 @@ +{ + "version": 8, + "configurePresets": [ + { + "name": "base", + "description": "Base settings that apply to all configurations", + "hidden": true, + "binaryDir": "${sourceDir}/build" + }, + { + "name": "ccache", + "description": "Set ccache variables", + "hidden": true, + "cacheVariables": { + "USE_COMPILER_CACHE": "ON" + }, + "environment": { + "CCACHE_NODEBUG": "true", + "CCACHE_DIRECT": "true", + "CCACHE_MAXSIZE": "500M", + "CCACHE_COMPRESS": "true", + "CCACHE_COMPRESSLEVEL": "1", + "CCACHE_NOSTATS": "true", + "CCACHE_DIR": "${sourceDir}/.ccache" + } + }, + { + "name": "buildcache", + "description": "Set buildcache variables", + "hidden": true, + "cacheVariables": { + "USE_COMPILER_CACHE": "ON" + }, + "environment": { + "BUILDCACHE_DEBUG": "-1", + "BUILDCACHE_DIRECT_MODE": "true", + "BUILDCACHE_MAX_CACHE_SIZE": "524288000", + "BUILDCACHE_COMPRESS": "true", + "BUILDCACHE_COMPRESS_FORMAT": "LZ4", + "BUILDCACHE_DIR": "${sourceDir}/.buildcache" + } + }, + { + "name": "debug", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "clang", + "hidden": true, + "generator": "Ninja", + "cacheVariables": { + "CMAKE_C_COMPILER": "/usr/bin/clang", + "CMAKE_CXX_COMPILER": "/usr/bin/clang++" + } + }, + { + "name": "msvc", + "hidden": true, + "generator": "Visual Studio 17 2022", + "architecture": "x64", + "toolset": "host=x64" + }, + { + "name": "gcc", + "hidden": true, + "generator": "Ninja", + "cacheVariables": { + "CMAKE_C_COMPILER": "/usr/bin/gcc", + "CMAKE_CXX_COMPILER": "/usr/bin/g++" + } + }, + { + "name": "macos-release", + "displayName": "MacOS (release) (clang)", + "description": "Build with Clang as Release without Debug Symbols", + "inherits": [ "base", "release", "clang" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos-release-deps", + "displayName": "MacOS Dependencies (release) (clang)", + "description": "Build Dependencies with Clang as Release without Debug Symbols", + "inherits": [ "macos-release" ], + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/${presetName}" + } + }, + { + "name": "macos-debug", + "displayName": "MacOS Debug (clang)", + "description": "Build with Clang with Debug Symbols", + "inherits": [ "base", "debug", "clang" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos-debug-deps", + "displayName": "MacOS Dependencies (debug) (clang)", + "description": "Build Dependencies with Clang with Debug Symbols", + "inherits": [ "macos-debug" ], + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/${presetName}" + } + }, + { + "name": "windows-release", + "displayName": "Windows Release (msvc)", + "description": "Build with MSVC's CL as Release without Debug Symbols", + "inherits": [ "base", "msvc" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows-debug", + "displayName": "Windows Debug (msvc)", + "description": "Build with MSVC's CL with Debug Symbols", + "inherits": [ "base", "msvc" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "linux-release", + "displayName": "Linux Release (gcc)", + "description": "Build with GCC as Release without Debug Symbols", + "inherits": [ "base", "release", "gcc" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "linux-debug", + "displayName": "Linux Debug (gcc)", + "description": "Build with GCC with Debug Symbols", + "inherits": [ "base", "debug", "gcc" ], + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + } + ], + "buildPresets": [ + { + "name": "macos-release", + "displayName": "MacOS (release) (clang)", + "description": "Build with Clang as Release without Debug Symbols", + "targets": "all", + "configurePreset": "macos-release", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + + }, + { + "name": "macos-release-deps", + "displayName": "MacOS Dependencies (release) (clang)", + "description": "Build Dependencies with Clang as Release without Debug Symbols", + "targets": "dependencies/all", + "configurePreset": "macos-release-deps", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + + }, + { + "name": "macos-debug", + "displayName": "MacOS (debug) (clang)", + "description": "Build with Clang as Debug", + "targets": "all", + "configurePreset": "macos-debug", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + + }, + { + "name": "macos-debug-deps", + "displayName": "MacOS Dependencies (debug) (clang)", + "description": "Build Dependencies with Clang as Debug", + "targets": "dependencies/all", + "configurePreset": "macos-debug-deps", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "windows-release", + "displayName": "Windows (release) (msvc)", + "description": "Build with MSVC's CL as Release without Debug Symbols", + "configuration": "Release", + "targets": "ALL_BUILD", + "configurePreset": "windows-release", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + + }, + { + "name": "windows-debug", + "displayName": "Windows (debug) (msvc)", + "description": "Build with MSVC's CL with Debug Symbols", + "configuration": "Debug", + "targets": "ALL_BUILD", + "configurePreset": "windows-debug", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + + }, + { + "name": "linux-release", + "displayName": "Linux (release) (gcc)", + "description": "Build with GCC as Release without Debug Symbols", + "targets": "all", + "configurePreset": "linux-release", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + + }, + { + "name": "linux-debug", + "displayName": "Linux (debug) (gcc)", + "description": "Build with GCC with Debug Symbols", + "targets": "all", + "configurePreset": "linux-debug", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + } + ] +} diff --git a/CMakeSettings.json b/CMakeSettings.json deleted file mode 100644 index a6c02f164..000000000 --- a/CMakeSettings.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "configurations": [ - { - "name": "x64-Debug", - "generator": "Ninja", - "configurationType": "Debug", - "inheritEnvironments": [ "msvc_x64_x64" ], - "buildRoot": "${projectDir}\\build-${name}", - "installRoot": "${projectDir}\\install-${name}", - "cmakeCommandArgs": "", - "buildCommandArgs": "", - "ctestCommandArgs": "", - "variables": [ - // Replace path with your TurboJPEG installation path - //{ "name": "TurboJPEG_INCLUDE_DIRS", "type": "PATH", "value": "YourTurboJPEGPath/libjpeg-turbo64/include" }, - //{ "name": "TurboJPEG_LIBRARY", "value": "YourTurboJPEGPath/libjpeg-turbo64/lib/turbojpeg.lib", "type": "FILEPATH" } - ], - "environments": [ - // Replace path with your installation path - //{ "QTDIR": "C:/Qt/6.2.2/msvc2019_64/" }, - //{ "VULKAN_SDK": "C:/VulkanSDK/1.2.182.0" } - ] - }, - { - "name": "x64-Release", - "generator": "Ninja", - "configurationType": "Release", - "buildRoot": "${projectDir}\\build-${name}", - "installRoot": "${projectDir}\\install-${name}", - "cmakeCommandArgs": "", - "buildCommandArgs": "package", - "ctestCommandArgs": "", - "inheritEnvironments": [ "msvc_x64_x64" ], - "variables": [ - // Replace path with your TurboJPEG installation path - //{ "name": "TurboJPEG_INCLUDE_DIRS", "type": "PATH", "value": "YourTurboJPEGPath/libjpeg-turbo64/include" }, - //{ "name": "TurboJPEG_LIBRARY", "value": "YourTurboJPEGPath/libjpeg-turbo64/lib/turbojpeg.lib", "type": "FILEPATH" } - ], - "environments": [ - // Replace path with your installation path - //{ "QTDIR": "C:/Qt/6.2.2/msvc2019_64/" }, - //{ "VULKAN_SDK": "C:/VulkanSDK/1.2.182.0" } - ] - } - ] -} diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 3e35b1e4d..a72f619a9 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -1014,8 +1014,8 @@ "infoDialog_import_comperror_text": "Sad! Your browser doesn't support importing. Please try again with another browser.", "infoDialog_import_confirm_text": "Are you sure to import \"$1\"? This process can't be reverted!", "infoDialog_import_confirm_title": "Confirm import", - "infoDialog_import_hyperror_text": "The selected configuration file \"$1\" can't be imported. It's not compatible with Hyperion 2.0 and higher!", "infoDialog_import_jsonerror_text": "The selected configuration file \"$1\" is not a .json file, or it's corrupted. Error message: ($2)", + "infoDialog_import_version_error_text": "The selected configuration file \"$1\" can not be imported. It's not compatible with Hyperion 2.0.17 and higher!", "infoDialog_wizrgb_text": "Your RGB Byte Order is already well adjusted.", "infoDialog_writeconf_error_text": "Saving your configuration failed.", "infoDialog_writeimage_error_text": "The selected file \"$1\" is not an image file, or it's corrupted! Please select another image file.", diff --git a/assets/webconfig/js/content_general.js b/assets/webconfig/js/content_general.js index 3a25045ac..6b732e467 100644 --- a/assets/webconfig/js/content_general.js +++ b/assets/webconfig/js/content_general.js @@ -28,11 +28,6 @@ $(document).ready(function () { // Instance handling function handleInstanceRename(e) { - conf_editor.on('change', function () { - window.readOnlyMode ? $('#btn_cl_save').prop('disabled', true) : $('#btn_submit').prop('disabled', false); - window.readOnlyMode ? $('#btn_ma_save').prop('disabled', true) : $('#btn_submit').prop('disabled', false); - }); - var inst = e.currentTarget.id.split("_")[1]; showInfoDialog('renInst', $.i18n('conf_general_inst_renreq_t'), getInstanceNameByIndex(inst)); @@ -119,14 +114,14 @@ $(document).ready(function () { //check file is json var check = isJsonString(content); if (check.length != 0) { - showInfoDialog('error', "", $.i18n('infoDialog_import_jsonerror_text', f.name, JSON.stringify(check))); + showInfoDialog('error', "", $.i18n('infoDialog_import_jsonerror_text', f.name, JSON.stringify(check.message))); dis_imp_btn(true); } else { content = JSON.parse(content); //check for hyperion json - if (typeof content.leds === 'undefined' || typeof content.general === 'undefined') { - showInfoDialog('error', "", $.i18n('infoDialog_import_hyperror_text', f.name)); + if (typeof content.global === 'undefined' || typeof content.instances === 'undefined') { + showInfoDialog('error', "", $.i18n('infoDialog_import_version_error_text', f.name)); dis_imp_btn(true); } else { @@ -143,10 +138,10 @@ $(document).ready(function () { $('#btn_import_conf').off().on('click', function () { showInfoDialog('import', $.i18n('infoDialog_import_confirm_title'), $.i18n('infoDialog_import_confirm_text', confName)); - $('#id_btn_import').off().on('click', function () { + $('#id_btn_import').off().on('click', function () { requestRestoreConfig(importedConf); - setTimeout(initRestart, 100); }); + }); $('#select_import_conf').off().on('change', function (e) { @@ -157,18 +152,17 @@ $(document).ready(function () { }); //export - $('#btn_export_conf').off().on('click', function () { - var name = window.serverConfig.general.name; - - var d = new Date(); - var month = d.getMonth() + 1; - var day = d.getDate(); - - var timestamp = d.getFullYear() + '.' + - (month < 10 ? '0' : '') + month + '.' + - (day < 10 ? '0' : '') + day; - - download(JSON.stringify(window.serverConfig, null, "\t"), 'Hyperion-' + window.currentVersion + '-Backup (' + name + ') ' + timestamp + '.json', "application/json"); + $('#btn_export_conf').off().on('click', async () => + { + const d = new Date(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const timestamp = `${d.getFullYear()}-${month}-${day}`; + + const configBackup = await requestConfig(); + if (configBackup.success === true) { + download(JSON.stringify(configBackup.info, null, "\t"), 'HyperionBackup-' + timestamp + '_v' + window.currentVersion + '.json', "application/json"); + } }); //create introduction @@ -180,3 +174,8 @@ $(document).ready(function () { removeOverlay(); }); + +$(window.hyperion).on("cmd-config-restoreconfig", function (event) { + setTimeout(initRestart, 100); +}); + diff --git a/assets/webconfig/js/content_index.js b/assets/webconfig/js/content_index.js index 70d9ce184..1cb2060fe 100644 --- a/assets/webconfig/js/content_index.js +++ b/assets/webconfig/js/content_index.js @@ -128,7 +128,7 @@ $(document).ready(function () { requestSysInfo(); }); - $(window.hyperion).on("cmd-config-getconfig", function (event) { + $(window.hyperion).on("cmd-config-getconfig-old", function (event) { window.serverConfig = event.response.info; window.showOptHelp = window.serverConfig.general.showOptHelp; @@ -278,7 +278,7 @@ $(document).ready(function () { window.currentHyperionInstance = 0; window.currentHyperionInstanceName = getInstanceNameByIndex(0); - requestServerConfig(); + requestServerConfigOld(); setTimeout(requestServerInfo, 100) setTimeout(requestTokenInfo, 200) } @@ -296,7 +296,7 @@ $(document).ready(function () { }); $(window.hyperion).on("cmd-instance-switchTo", function (event) { - requestServerConfig(); + requestServerConfigOld(); setTimeout(requestServerInfo, 200) setTimeout(requestTokenInfo, 400) }); @@ -338,11 +338,6 @@ $(function () { }); }); -// hotfix body padding when bs modals overlap -$(document.body).on('hide.bs.modal,hidden.bs.modal', function () { - $('body').css('padding-right', '0'); -}); - //Dark Mode $("#btn_darkmode").off().on("click", function (e) { if (getStorage("darkMode") != "on") { diff --git a/assets/webconfig/js/hyperion.js b/assets/webconfig/js/hyperion.js index 8153e8273..a06776b60 100644 --- a/assets/webconfig/js/hyperion.js +++ b/assets/webconfig/js/hyperion.js @@ -37,9 +37,8 @@ const ENDLESS = -1; function initRestart() { $(window.hyperion).off(); - requestServerConfigReload(); window.watchdog = 10; - connectionLostDetection('restart'); + connectionLostDetection('restart'); } function connectionLostDetection(type) @@ -138,9 +137,10 @@ function initWebSocket() if (error == "Service Unavailable") { window.location.reload(); } else { - $(window.hyperion).trigger({type:"error",reason:error}); + $(window.hyperion).trigger({type:"error", reason:error}); } - console.log("[window.websocket::onmessage] ",error) + let errorData = response.hasOwnProperty("errorData")? response.errorData : ""; + console.log("[window.websocket::onmessage] ",error, ", Description:", errorData); } } } @@ -344,6 +344,11 @@ function requestServerConfig() sendToHyperion("config", "getconfig"); } +function requestServerConfigOld() +{ + sendToHyperion("config", "getconfig-old"); +} + function requestServerConfigReload() { sendToHyperion("config", "reload"); @@ -522,3 +527,12 @@ async function requestServiceDiscovery(type, params) { return sendAsyncToHyperion("service", "discover", data); } +async function requestConfig(globalTypes, instances, instanceTypes) { + let globalFilter = { "global": { "types": globalTypes } }; + let instanceFilter = { "instances": { "ids": instances, "types": instanceTypes } }; + let filter = { "configFilter" : globalFilter, instanceFilter }; + + return sendAsyncToHyperion("config", "getconfig", filter); +} + + diff --git a/assets/webconfig/js/ledsim.js b/assets/webconfig/js/ledsim.js index af633ba77..52a75cedf 100644 --- a/assets/webconfig/js/ledsim.js +++ b/assets/webconfig/js/ledsim.js @@ -129,7 +129,7 @@ $(document).ready(function () { } }); // apply new serverinfos - $(window.hyperion).on("cmd-config-getconfig", function (event) { + $(window.hyperion).on("cmd-config-getconfig-old", function (event) { leds = event.response.info.leds; grabberConfig = event.response.info.grabberV4L2; updateLedLayout(); diff --git a/assets/webconfig/js/ui_utils.js b/assets/webconfig/js/ui_utils.js index 578e31287..ace8b8300 100644 --- a/assets/webconfig/js/ui_utils.js +++ b/assets/webconfig/js/ui_utils.js @@ -239,7 +239,7 @@ function showInfoDialog(type, header, message) { $('#id_body').html(''); if (header == "") $('#id_body').append('

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

'); - $('#id_footer').html(''); + $('#id_footer').html(''); } else if (type == "select") { $('#id_body').html(''); @@ -256,9 +256,9 @@ function showInfoDialog(type, header, message) { $('#id_footer').html('' + $.i18n('InfoDialog_nowrite_foottext') + ''); } else if (type == "import") { - $('#id_body').html(''); - $('#id_footer').html(''); - $('#id_footer').append(''); + $('#id_body').html(''); + $('#id_footer').html(''); + $('#id_footer').append(''); } else if (type == "delInst") { $('#id_body').html(''); @@ -1222,7 +1222,7 @@ function getSystemInfo() { info += '- Avail Video Cap.: ' + window.serverInfo.grabbers.video.available + '\n'; info += '- Avail Audio Cap.: ' + window.serverInfo.grabbers.audio.available + '\n'; info += '- Avail Services: ' + window.serverInfo.services + '\n'; - info += '- Config path: ' + shy.rootPath + '\n'; + info += '- Config database: ' + shy.configDatabaseFile + '\n'; info += '- Database: ' + (shy.readOnlyMode ? "ready-only" : "read/write") + '\n'; info += '- Mode: ' + (shy.isGuiMode ? "GUI" : "Non-GUI") + '\n'; diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default deleted file mode 100644 index f126b638c..000000000 --- a/config/hyperion.config.json.default +++ /dev/null @@ -1,276 +0,0 @@ -{ - "general": { - "name": "My Hyperion Config", - "configVersion": "configVersionValue", - "previousVersion": "previousVersionValue", - "watchedVersionBranch": "Stable", - "showOptHelp": true - }, - "logger": { - "level": "warn" - }, - - "device": { - "type": "file", - "hardwareLedCount": 1, - "autoStart": true, - "output": "/dev/null", - "colorOrder": "rgb", - "latchTime": 0, - "rewriteTime": 0, - "enableAttempts": 6, - "enableAttemptsInterval": 15 - }, - - "schedEvents": { - "enable": false - }, - - "cecEvents": { - "actions": [ - { - "action": "Suspend", - "event": "standby" - }, - { - "action": "Resume", - "event": "set stream path" - } - ], - "enable": false - }, - - "color": { - "imageToLedMappingType": "multicolor_mean", - "channelAdjustment": [ - { - "id": "default", - "leds": "*", - "white": [ 255, 255, 255 ], - "red": [ 255, 0, 0 ], - "green": [ 0, 255, 0 ], - "blue": [ 0, 0, 255 ], - "cyan": [ 0, 255, 255 ], - "magenta": [ 255, 0, 255 ], - "yellow": [ 255, 255, 0 ], - "gammaRed": 2.2, - "gammaGreen": 2.2, - "gammaBlue": 2.2, - "backlightThreshold": 0, - "backlightColored": false, - "brightness": 100, - "brightnessCompensation": 100, - "saturationGain": 1.0, - "brightnessGain": 1.0 - } - ] - }, - - "smoothing": { - "enable": true, - "type": "linear", - "time_ms": 150, - "updateFrequency": 25.0000, - "interpolationRate": 25.0000, - "decay": 1, - "dithering": false, - "updateDelay": 0 - }, - - "grabberV4L2": { - "enable": false, - "device": "none", - "input": 0, - "encoding": "NO_CHANGE", - "width": 0, - "height": 0, - "fps": 15, - "flip": "NO_CHANGE", - "fpsSoftwareDecimation": 0, - "sizeDecimation": 8, - "cropLeft": 0, - "cropRight": 0, - "cropTop": 0, - "cropBottom": 0, - "redSignalThreshold": 0, - "greenSignalThreshold": 100, - "blueSignalThreshold": 0, - "signalDetection": false, - "noSignalCounterThreshold": 200, - "sDVOffsetMin": 0.1, - "sDVOffsetMax": 0.9, - "sDHOffsetMin": 0.4, - "sDHOffsetMax": 0.46, - "hardware_brightness": 0, - "hardware_contrast": 0, - "hardware_saturation": 0, - "hardware_hue": 0 - }, - - "grabberAudio": { - "enable": false, - "device": "auto", - "audioEffect": "vuMeter", - "vuMeter": { - "flip": "NO_CHANGE", - "hotColor": [ 255, 0, 0 ], - "multiplier": 1, - "safeColor": [ 0, 255, 0 ], - "safeValue": 45, - "tolerance": 5, - "warnColor": [ 255, 255, 0 ], - "warnValue": 80 - } - }, - - "framegrabber": { - "enable": false, - "device": "auto", - "input": 0, - "width": 80, - "height": 45, - "fps": 10, - "pixelDecimation": 8, - "cropLeft": 0, - "cropRight": 0, - "cropTop": 0, - "cropBottom": 0 - }, - - "blackborderdetector": { - "enable": true, - "threshold": 5, - "unknownFrameCnt": 600, - "borderFrameCnt": 50, - "maxInconsistentCnt": 10, - "blurRemoveCnt": 1, - "mode": "default" - }, - - "foregroundEffect": { - "enable": true, - "type": "effect", - "color": [ 0, 0, 255 ], - "effect": "Rainbow swirl fast", - "duration_ms": 3000 - }, - - "backgroundEffect": { - "enable": false, - "type": "effect", - "color": [ 255, 138, 0 ], - "effect": "Warm mood blobs" - }, - - "forwarder": { - "enable": false, - "jsonapi": [], - "flatbuffer": [] - }, - - "jsonServer": { - "port": 19444 - }, - - "flatbufServer": { - "enable": true, - "port": 19400, - "timeout": 5 - }, - - "protoServer": { - "enable": true, - "port": 19445, - "timeout": 5 - }, - - "boblightServer": { - "enable": false, - "port": 19333, - "priority": 128 - }, - - "webConfig": { - "document_root": "", - "port": 8090, - "sslPort": 8092, - "crtPath": "", - "keyPath": "", - "keyPassPhrase": "" - }, - - "effects": { - "paths": [ "$ROOT/custom-effects" ], - "disable": [ "" ] - }, - - "instCapture": { - "systemEnable": false, - "systemGrabberDevice": "NONE", - "systemPriority": 250, - "v4lEnable": false, - "v4lGrabberDevice": "NONE", - "v4lPriority": 240, - "audioEnable": false, - "audioGrabberDevice": "NONE", - "audioPriority": 230 - }, - - "network": { - "internetAccessAPI": false, - "restirctedInternetAccessAPI": false, - "ipWhitelist": [], - "localApiAuth": false - }, - - "ledConfig": { - "classic": { - "top": 1, - "bottom": 0, - "left": 0, - "right": 0, - "glength": 0, - "gpos": 0, - "position": 0, - "reverse": false, - "hdepth": 8, - "vdepth": 5, - "overlap": 0, - "edgegap": 0, - "ptlh": 0, - "ptlv": 0, - "ptrh": 100, - "ptrv": 0, - "pblh": 0, - "pblv": 100, - "pbrh": 100, - "pbrv": 100 - }, - - "matrix": { - "ledshoriz": 1, - "ledsvert": 1, - "cabling": "snake", - "direction": "horizontal", - "start": "top-left" - } - }, - - "leds": [ - { - "hmax": 1, - "hmin": 0, - "vmax": 0.08, - "vmin": 0 - } - ], - - "osEvents": { - "suspendEnable": true, - "lockEnable": true - }, - - "cecEvents": { - "enable": false - } -} diff --git a/include/api/API.h b/include/api/API.h index 96e1f9b8b..8b6476e60 100644 --- a/include/api/API.h +++ b/include/api/API.h @@ -239,18 +239,6 @@ class API : public QObject QString saveEffect(const QJsonObject &data); #endif - /// - /// @brief Save settings object. Requires ADMIN ACCESS - /// @param data The data object - /// - bool saveSettings(const QJsonObject &data); - - /// - /// @brief Restore settings object. Requires ADMIN ACCESS - /// @param data The data object - /// - bool restoreSettings(const QJsonObject &data); - /// /// @brief Set the authorizationn state /// @param authorized True, if authorized diff --git a/include/api/JsonAPI.h b/include/api/JsonAPI.h index 35d7e2043..60b092f5e 100644 --- a/include/api/JsonAPI.h +++ b/include/api/JsonAPI.h @@ -186,7 +186,7 @@ private slots: /// void handleSourceSelectCommand(const QJsonObject &message, const JsonApiCommand& cmd); - /// Handle an incoming JSON GetConfig message and check subcommand + /// Handle an incoming JSON Config message and check subcommand /// /// @param message the incoming message /// @@ -204,6 +204,12 @@ private slots: /// void handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd); + /// Handle an incoming JSON GetConfig message from handleConfigCommand() + /// + /// @param message the incoming message + /// + void handleConfigGetCommand(const QJsonObject &message, const JsonApiCommand& cmd); + /// Handle an incoming JSON RestoreConfig message from handleConfigCommand() /// /// @param message the incoming message diff --git a/include/api/JsonApiCommand.h b/include/api/JsonApiCommand.h index c63d968b6..4345b56a9 100644 --- a/include/api/JsonApiCommand.h +++ b/include/api/JsonApiCommand.h @@ -84,6 +84,7 @@ class SubCommand { DeleteToken, Discover, GetConfig, + GetConfigOld, GetInfo, GetPendingTokenRequests, GetProperties, @@ -134,6 +135,7 @@ class SubCommand { case DeleteToken: return "deleteToken"; case Discover: return "discover"; case GetConfig: return "getconfig"; + case GetConfigOld: return "getconfig-old"; case GetInfo: return "getInfo"; case GetPendingTokenRequests: return "getPendingTokenRequests"; case GetProperties: return "getProperties"; @@ -274,6 +276,7 @@ class ApiCommandRegister { { {"color", ""}, { Command::Color, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, { {"componentstate", ""}, { Command::ComponentState, SubCommand::Empty, Authorization::Yes, InstanceCmd::Multi, NoListenerCmd::Yes} }, { {"config", "getconfig"}, { Command::Config, SubCommand::GetConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, + { {"config", "getconfig-old"}, { Command::Config, SubCommand::GetConfigOld, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, { {"config", "getschema"}, { Command::Config, SubCommand::GetSchema, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, { {"config", "reload"}, { Command::Config, SubCommand::Reload, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, { {"config", "restoreconfig"}, { Command::Config, SubCommand::RestoreConfig, Authorization::Admin, InstanceCmd::Yes, NoListenerCmd::Yes} }, diff --git a/include/api/JsonInfo.h b/include/api/JsonInfo.h index 1346f97ee..f3a9922c5 100644 --- a/include/api/JsonInfo.h +++ b/include/api/JsonInfo.h @@ -31,6 +31,8 @@ class JsonInfo static QJsonObject getSystemInfo(const Hyperion* hyperion); QJsonObject discoverSources (const QString& sourceType, const QJsonObject& params); + static QJsonObject getConfiguration(const QList& instances = {}, const QStringList& instanceFilteredTypes = {}, const QStringList& globalFilterTypes = {} ); + private: template diff --git a/include/db/AuthTable.h b/include/db/AuthTable.h index 161c38562..f2ecb50e5 100644 --- a/include/db/AuthTable.h +++ b/include/db/AuthTable.h @@ -1,12 +1,7 @@ -#pragma once +#ifndef AUTHSTABLE_H +#define AUTHSTABLE_H -// hyperion #include -#include - -// qt -#include -#include namespace hyperion { const char DEFAULT_USER[] = "Hyperion"; @@ -21,69 +16,30 @@ class AuthTable : public DBManager public: /// construct wrapper with auth table - AuthTable(const QString& rootPath = "", QObject* parent = nullptr, bool readonlyMode = false) - : DBManager(parent) - { - setReadonlyMode(readonlyMode); - if(!rootPath.isEmpty()){ - // Init Hyperion database usage - setRootPath(rootPath); - setDatabaseName("hyperion"); - } - // init Auth table - setTable("auth"); - // create table columns - createTable(QStringList()<<"user TEXT"<<"password BLOB"<<"token BLOB"<<"salt BLOB"<<"comment TEXT"<<"id TEXT"<<"created_at TEXT"<<"last_use TEXT"); - }; + explicit AuthTable(QObject* parent = nullptr); /// /// @brief Create a user record, if called on a existing user the auth is recreated /// @param[in] user The username - /// @param[in] pw The password + /// @param[in] password The password /// @return true on success else false /// - inline bool createUser(const QString& user, const QString& pw) - { - // new salt - QByteArray salt = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); - QVariantMap map; - map["user"] = user; - map["salt"] = salt; - map["password"] = hashPasswordWithSalt(pw,salt); - map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("user",user)); - return createRecord(cond, map); - } + bool createUser(const QString& user, const QString& password); /// /// @brief Test if user record exists /// @param[in] user The user id /// @return true on success else false /// - inline bool userExist(const QString& user) - { - VectorPair cond; - cond.append(CPair("user",user)); - return recordExists(cond); - } + bool userExist(const QString& user); /// /// @brief Test if a user is authorized for access with given pw. - /// @param user The user name - /// @param pw The password - /// @return True on success else false + /// @param user The user name + /// @param password The password + /// @return True on success else false /// - inline bool isUserAuthorized(const QString& user, const QString& pw) - { - if(userExist(user) && (calcPasswordHashOfUser(user, pw) == getPasswordHashOfUser(user))) - { - updateUserUsed(user); - return true; - } - return false; - } + bool isUserAuthorized(const QString& user, const QString& password); /// /// @brief Test if a user token is authorized for access. @@ -91,197 +47,92 @@ class AuthTable : public DBManager /// @param token The token /// @return True on success else false /// - inline bool isUserTokenAuthorized(const QString& usr, const QString& token) - { - if(getUserToken(usr) == token.toUtf8()) - { - updateUserUsed(usr); - return true; - } - return false; - } + bool isUserTokenAuthorized(const QString& usr, const QString& token); /// /// @brief Update token of a user. It's an alternate login path which is replaced on startup. This token is NOT hashed(!) /// @param user The user name /// @return True on success else false /// - inline bool setUserToken(const QString& user) - { - QVariantMap map; - map["token"] = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); - - VectorPair cond; - cond.append(CPair("user", user)); - return updateRecord(cond, map); - } + bool setUserToken(const QString& user); /// /// @brief Get token of a user. This token is NOT hashed(!) /// @param user The user name /// @return The token /// - inline const QByteArray getUserToken(const QString& user) - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("user", user)); - getRecord(cond, results, QStringList()<<"token"); - - return results["token"].toByteArray(); - } + const QByteArray getUserToken(const QString& user); /// /// @brief update password of given user. The user should be tested (isUserAuthorized) to verify this change - /// @param user The user name - /// @param newPw The new password to set - /// @return True on success else false + /// @param user The user name + /// @param newassword The new password to set + /// @return True on success else false /// - inline bool updateUserPassword(const QString& user, const QString& newPw) - { - QVariantMap map; - map["password"] = calcPasswordHashOfUser(user, newPw); - - VectorPair cond; - cond.append(CPair("user", user)); - return updateRecord(cond, map); - } + bool updateUserPassword(const QString& user, const QString& newPassword); /// /// @brief Reset password of Hyperion user !DANGER! Used in Hyperion main.cpp /// @return True on success else false /// - inline bool resetHyperionUser() - { - QVariantMap map; - map["password"] = calcPasswordHashOfUser(hyperion::DEFAULT_USER, hyperion::DEFAULT_PASSWORD); - - VectorPair cond; - cond.append(CPair("user", hyperion::DEFAULT_USER)); - return updateRecord(cond, map); - } + bool resetHyperionUser(); /// /// @brief Update 'last_use' column entry for the corresponding user /// @param[in] user The user to search for /// - inline void updateUserUsed(const QString& user) - { - QVariantMap map; - map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("user", user)); - updateRecord(cond, map); - } + void updateUserUsed(const QString& user); /// /// @brief Test if token record exists, updates last_use on success /// @param[in] token The token id /// @return true on success else false /// - inline bool tokenExist(const QString& token) - { - QVariantMap map; - map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("token", hashToken(token))); - if(recordExists(cond)) - { - // update it - createRecord(cond,map); - return true; - } - return false; - } + bool tokenExist(const QString& token); /// /// @brief Create a new token record with comment /// @param[in] token The token id as plaintext /// @param[in] comment The comment for the token (eg a human readable identifier) - /// @param[in] id The id for the token + /// @param[in] identifier The identifier for the token /// @return true on success else false /// - inline bool createToken(const QString& token, const QString& comment, const QString& id) - { - QVariantMap map; - map["comment"] = comment; - map["id"] = idExist(id) ? QUuid::createUuid().toString().remove("{").remove("}").left(5) : id; - map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("token", hashToken(token))); - return createRecord(cond, map); - } + bool createToken(const QString& token, const QString& comment, const QString& identifier); /// - /// @brief Delete token record by id - /// @param[in] id The token id + /// @brief Delete token record by identifier + /// @param[in] identifier The token identifier /// @return true on success else false /// - inline bool deleteToken(const QString& id) - { - VectorPair cond; - cond.append(CPair("id", id)); - return deleteRecord(cond); - } + bool deleteToken(const QString& identifier); /// - /// @brief Rename token record by id - /// @param[in] id The token id + /// @brief Rename token record by identifier + /// @param[in] identifier The token identifier /// @param[in] comment The new comment /// @return true on success else false /// - inline bool renameToken(const QString &id, const QString &comment) - { - QVariantMap map; - map["comment"] = comment; - - VectorPair cond; - cond.append(CPair("id", id)); - return updateRecord(cond, map); - } + bool renameToken(const QString &identifier, const QString &comment); /// /// @brief Get all 'comment', 'last_use' and 'id' column entries /// @return A vector of all lists /// - inline const QVector getTokenList() - { - QVector results; - getRecords(results, QStringList() << "comment" << "id" << "last_use"); - - return results; - } + const QVector getTokenList(); /// - /// @brief Test if id exists - /// @param[in] id The id + /// @brief Test if identifier exists + /// @param[in] identifier The identifier /// @return true on success else false /// - inline bool idExist(const QString& id) - { - - VectorPair cond; - cond.append(CPair("id", id)); - return recordExists(cond); - } + bool identifierExist(const QString& identifier); /// /// @brief Get the passwort hash of a user from db /// @param user The user name /// @return password as hash /// - inline const QByteArray getPasswordHashOfUser(const QString& user) - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("user", user)); - getRecord(cond, results, QStringList()<<"password"); - - return results["password"].toByteArray(); - } + const QByteArray getPasswordHashOfUser(const QString& user); /// /// @brief Calc the password hash of a user based on user name and password @@ -289,36 +140,22 @@ class AuthTable : public DBManager /// @param pw The password /// @return The calced password hash /// - inline const QByteArray calcPasswordHashOfUser(const QString& user, const QString& pw) - { - // get salt - QVariantMap results; - VectorPair cond; - cond.append(CPair("user", user)); - getRecord(cond, results, QStringList()<<"salt"); - - // calc - return hashPasswordWithSalt(pw,results["salt"].toByteArray()); - } + const QByteArray calcPasswordHashOfUser(const QString& user, const QString& password); /// /// @brief Create a password hash of plaintex password + salt - /// @param pw The plaintext password + /// @param password The plaintext password /// @param salt The salt /// @return The password hash with salt /// - inline const QByteArray hashPasswordWithSalt(const QString& pw, const QByteArray& salt) - { - return QCryptographicHash::hash(pw.toUtf8().append(salt), QCryptographicHash::Sha512).toHex(); - } + const QByteArray hashPasswordWithSalt(const QString& password, const QByteArray& salt); /// /// @brief Create a token hash /// @param token The plaintext token /// @return The token hash /// - inline const QByteArray hashToken(const QString& token) - { - return QCryptographicHash::hash(token.toUtf8(), QCryptographicHash::Sha512).toHex(); - } + const QByteArray hashToken(const QString& token); }; + +#endif // AUTHSTABLE_H diff --git a/include/db/DBConfigManager.h b/include/db/DBConfigManager.h new file mode 100644 index 000000000..652e26ad6 --- /dev/null +++ b/include/db/DBConfigManager.h @@ -0,0 +1,42 @@ +#ifndef DBCONFGMANAGER_H +#define DBConfigManager_H + +#include +#include "db/SettingsTable.h" +#include "db/InstanceTable.h" + +class DBConfigManager : public DBManager +{ +public: + DBConfigManager(QObject* parent = nullptr); + + QPair importJson(const QString& configFile); + bool exportJson(const QString& path = "") const; + + QJsonObject getConfiguration(const QList& instances = {}, const QStringList& instanceFilteredTypes = {}, const QStringList& globalFilterTypes = {} ) const; + + QPair validateConfiguration(); + QPair validateConfiguration(QJsonObject& config, bool doCorrections = false); + + QPair addMissingDefaults(); + + QPair updateConfiguration(); + QPair updateConfiguration(QJsonObject& config, bool doCorrections = false); + + QPair migrateConfiguration(); + +private: + // Function to import global settings from the configuration + bool importGlobalSettings(const QJsonObject& config, QStringList& errorList); + + // Function to import all instances from the configuration + bool importInstances(const QJsonObject& config, QStringList& errorList); + + // Function to import a single instance + bool importInstance(InstanceTable& instanceTable, const QJsonObject& instanceConfig, quint8 instanceIdx, QStringList& errorList); + + // Function to import settings for a specific instance + bool importInstanceSettings(SettingsTable& settingsTable, const QJsonObject& instanceSettings, QStringList& errorList); +}; + +#endif // DBCONFGMANAGER_H diff --git a/include/db/DBManager.h b/include/db/DBManager.h index 4d36471d6..a5d858029 100644 --- a/include/db/DBManager.h +++ b/include/db/DBManager.h @@ -1,10 +1,17 @@ #pragma once +#ifdef Unsorted +#undef Unsorted +#endif + #include #include #include #include #include +#include +#include +#include class QSqlDatabase; class QSqlQuery; @@ -26,13 +33,22 @@ class DBManager : public QObject Q_OBJECT public: - DBManager(QObject* parent = nullptr); - ~DBManager() override; + explicit DBManager(QObject* parent = nullptr); + + static void initializeDatabase(const QDir& dataDirectory, bool isReadOnly); + + static QDir getDataDirectory() { return _dataDirectory;} + static QDir getDirectory() { return _databaseDirectory;} + static QFileInfo getFileInfo() { return _databaseFile;} + static bool isReadOnly() { return _isReadOnly; } + + /// + /// @brief Sets the database in read-only mode. + /// Updates will not written to the tables + /// @param[in] readOnly True read-only, false - read/write + /// + static void setReadonly(bool isReadOnly) { _isReadOnly = isReadOnly; } - /// set root path - void setRootPath(const QString& rootPath); - /// define the database to work with - void setDatabaseName(const QString& dbn) { _dbn = dbn; }; /// set a table to work with void setTable(const QString& table); @@ -61,6 +77,8 @@ class DBManager : public QObject /// bool recordExists(const VectorPair& conditions) const; + bool recordsNotExisting(const QVariantList& testValues,const QString& column, QStringList& nonExistingRecs, const QString& condition ) const; + /// /// @brief Create a new record in table when the conditions find no existing entry. Add additional key:value pairs in columns /// DO NOT repeat column keys between 'conditions' and 'columns' as they will be merged on creation @@ -98,6 +116,18 @@ class DBManager : public QObject /// bool getRecords(QVector& results, const QStringList& tColumns = QStringList(), const QStringList& tOrder = QStringList()) const; + /// + /// @brief Get data of multiple records, you need to specify the columns. This search is without conditions. Good to grab all data from db + /// @param[in] conditions condition to search for (WHERE) + /// @param[out] results results of query + /// @param[in] tColumns target columns to search in (optional) if not provided returns all columns + /// @param[in] tOrder target order columns with order by ASC/DESC (optional) + /// @return True on success else false + /// + bool getRecords(const VectorPair& conditions, QVector& results, const QStringList& tColumns = {}, const QStringList& tOrder = {}) const; + + bool getRecords(const QString& condition, const QVariantList& bindValues, QVector& results, const QStringList& tColumns = {}, const QStringList& tOrder = {}) const; + /// /// @brief Delete a record determined by conditions /// @param[in] conditions conditions of the row to delete it (WHERE) @@ -119,23 +149,36 @@ class DBManager : public QObject /// bool deleteTable(const QString& table) const; - /// - /// @brief Sets a table in read-only mode. - /// Updates will not written to the table - /// @param[in] readOnly True read-only, false - read/write - /// - void setReadonlyMode(bool readOnly) { _readonlyMode = readOnly; }; + bool executeQuery(QSqlQuery& query) const; + + bool startTransaction(QSqlDatabase& idb) const; + bool startTransaction(QSqlDatabase& idb, QStringList& errorList); + bool commiTransaction(QSqlDatabase& idb) const; + bool commiTransaction(QSqlDatabase& idb, QStringList& errorList); + bool rollbackTransaction(QSqlDatabase& idb) const; + bool rollbackTransaction(QSqlDatabase& idb, QStringList& errorList); + + // Utility function to log errors and append to error list + void logErrorAndAppend(const QString& errorText, QStringList& errorList); + +protected: + Logger* _log; private: + static QDir _dataDirectory; + static QDir _databaseDirectory; + static QFileInfo _databaseFile; + static QThreadStorage _databasePool; + static bool _isReadOnly; + + /// databse connection & file name, defaults to hyperion + QString _dbn = "hyperion"; - Logger* _log; - /// databse connection & file name, defaults to hyperion - QString _dbn = "hyperion"; - /// table in database - QString _table; + /// table in database + QString _table; - bool _readonlyMode; + /// addBindValues to query given by QVariantList + void addBindValues(QSqlQuery& query, const QVariantList& variants) const; - /// addBindValue to query given by QVariantList - void doAddBindValue(QSqlQuery& query, const QVariantList& variants) const; + QString constructExecutedQuery(const QSqlQuery& query) const; }; diff --git a/include/db/DBMigrationManager.h b/include/db/DBMigrationManager.h new file mode 100644 index 000000000..be315cd16 --- /dev/null +++ b/include/db/DBMigrationManager.h @@ -0,0 +1,33 @@ +#ifndef DBMIGRATIONMANAGER_H +#define DBMIGRATIONMANAGER_H + +#include +#include + +#include + +class DBMigrationManager : public DBManager +{ + Q_OBJECT +public: + explicit DBMigrationManager(QObject *parent = nullptr); + + bool isMigrationRequired(); + bool migrateSettings(QJsonObject& config); + +private: + + bool upgradeGlobalSettings(const semver::version& currentVersion, QJsonObject& config); + bool upgradeGlobalSettings_alpha_9(semver::version& currentVersion, QJsonObject& config); + bool upgradeGlobalSettings_2_0_12(semver::version& currentVersion, QJsonObject& config); + bool upgradeGlobalSettings_2_0_16(semver::version& currentVersion, QJsonObject& config); + bool upgradeGlobalSettings_2_1_0(semver::version& currentVersion, QJsonObject& config); + + bool upgradeInstanceSettings(const semver::version& currentVersion, quint8 instance, QJsonObject& config); + bool upgradeInstanceSettings_alpha_9(semver::version& currentVersion, quint8 instance, QJsonObject& config); + bool upgradeInstanceSettings_2_0_12(semver::version& currentVersion, quint8 instance, QJsonObject& config); + bool upgradeInstanceSettings_2_0_13(semver::version& currentVersion, quint8 instance, QJsonObject& config); + bool upgradeInstanceSettings_2_0_16(semver::version& currentVersion, quint8 instance, QJsonObject& config); +}; + +#endif // DBMIGRATIONMANAGER_H diff --git a/include/db/InstanceTable.h b/include/db/InstanceTable.h index 912ecbafb..7fbb580ee 100644 --- a/include/db/InstanceTable.h +++ b/include/db/InstanceTable.h @@ -1,11 +1,7 @@ -#pragma once +#ifndef INSTANCETABLE_H +#define INSTANCETABLE_H -// db #include -#include - -// qt -#include /// /// @brief Hyperion instance manager specific database interface. prepares also the Hyperion database for all follow up usage (Init QtSqlConnection) along with db name @@ -14,22 +10,7 @@ class InstanceTable : public DBManager { public: - InstanceTable(const QString& rootPath, QObject* parent = nullptr, bool readonlyMode = false) - : DBManager(parent) - { - - setReadonlyMode(readonlyMode); - // Init Hyperion database usage - setRootPath(rootPath); - setDatabaseName("hyperion"); - - // Init instance table - setTable("instances"); - createTable(QStringList()<<"instance INTEGER"<<"friendly_name TEXT"<<"enabled INTEGER DEFAULT 0"<<"last_use TEXT"); - - // start/create the first Hyperion instance index 0 - createInstance(); - }; + explicit InstanceTable(QObject* parent = nullptr); /// /// @brief Create a new Hyperion instance entry, the name needs to be unique @@ -37,53 +18,19 @@ class InstanceTable : public DBManager /// @param[out] inst The id that has been assigned /// @return True on success else false /// - inline bool createInstance(const QString& name, quint8& inst) - { - VectorPair fcond; - fcond.append(CPair("friendly_name",name)); - - // check duplicate - if(!recordExists(fcond)) - { - inst = 0; - VectorPair cond; - cond.append(CPair("instance",inst)); - - // increment to next avail index - while(recordExists(cond)) - { - inst++; - cond.removeFirst(); - cond.append(CPair("instance",inst)); - } - // create - QVariantMap data; - data["friendly_name"] = name; - data["instance"] = inst; - VectorPair lcond; - return createRecord(lcond, data); - } - return false; - } + bool createInstance(const QString& name, quint8& inst); + + /// + /// @brief Create first Hyperion instance entry, if index 0 is not found. + /// + void createDefaultInstance(); /// /// @brief Delete a Hyperion instance /// @param inst The id that has been assigned /// @return True on success else false /// - inline bool deleteInstance(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance",inst)); - if(deleteRecord(cond)) - { - // delete settings entries - SettingsTable settingsTable(inst); - settingsTable.deleteInstance(); - return true; - } - return false; - } + bool deleteInstance(quint8 inst); /// /// @brief Assign a new name for the given instance @@ -91,141 +38,59 @@ class InstanceTable : public DBManager /// @param name The new name of the instance /// @return True on success else false (instance not found) /// - inline bool saveName(quint8 inst, const QString& name) - { - VectorPair fcond; - fcond.append(CPair("friendly_name",name)); - - // check duplicate - if(!recordExists(fcond)) - { - if(instanceExist(inst)) - { - VectorPair cond; - cond.append(CPair("instance",inst)); - QVariantMap data; - data["friendly_name"] = name; - - return updateRecord(cond, data); - } - } - return false; - } - - + bool saveName(quint8 inst, const QString& name); /// /// @brief Get all instances with all columns - /// @param justEnabled return just enabled instances if true + /// @param onlyEnabled return only enabled instances if true /// @return The found instances /// - inline QVector getAllInstances(bool justEnabled = false) - { - QVector results; - getRecords(results, QStringList(), QStringList() << "instance ASC"); - if(justEnabled) - { - for (auto it = results.begin(); it != results.end();) - { - if( ! (*it)["enabled"].toBool()) - { - it = results.erase(it); - continue; - } - ++it; - } - } - return results; - } + QVector getAllInstances(bool onlyEnabled = false); + + /// + /// @brief Get all instance IDs + /// @param onlyEnabled return only enabled instance IDs if true + /// @return The found instances + /// + QList getAllInstanceIDs (bool onlyEnabled = false); /// /// @brief Test if instance record exists /// @param[in] user The user id /// @return true on success else false /// - inline bool instanceExist(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance",inst)); - return recordExists(cond); - } + bool instanceExist(quint8 inst); /// /// @brief Get instance name by instance index /// @param index The index to search for /// @return The name of this index, may return NOT FOUND if not found /// - inline const QString getNamebyIndex(quint8 index) - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("instance", index)); - getRecord(cond, results, QStringList("friendly_name")); - - QString name = results["friendly_name"].toString(); - return name.isEmpty() ? "NOT FOUND" : name; - } + QString getNamebyIndex(quint8 index); /// /// @brief Update 'last_use' timestamp /// @param inst The instance to update + /// @return True on success else false /// - inline void setLastUse(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance", inst)); - QVariantMap map; - map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - updateRecord(cond, map); - } + bool setLastUse(quint8 inst); /// /// @brief Update 'enabled' column by instance index /// @param inst The instance to update /// @param newState True when enabled else false + /// @return True on success else false /// - inline void setEnable(quint8 inst, bool newState) - { - VectorPair cond; - cond.append(CPair("instance", inst)); - QVariantMap map; - map["enabled"] = newState; - updateRecord(cond, map); - } + bool setEnable(quint8 inst, bool newState); /// /// @brief Get state of 'enabled' column by instance index /// @param inst The instance to get /// @return True when enabled else false /// - inline bool isEnabled(quint8 inst) - { - VectorPair cond; - cond.append(CPair("instance", inst)); - QVariantMap results; - getRecord(cond, results); - - return results["enabled"].toBool(); - } + bool isEnabled(quint8 inst); private: - /// - /// @brief Create first Hyperion instance entry, if index 0 is not found. - /// - inline void createInstance() - { - if(instanceExist(0)) - setEnable(0, true); - else - { - QVariantMap data; - data["friendly_name"] = "First LED Hardware instance"; - VectorPair cond; - cond.append(CPair("instance", 0)); - if(createRecord(cond, data)) - setEnable(0, true); - else - throw std::runtime_error("Failed to create Hyperion root instance in db! This should never be the case..."); - } - } }; + +#endif // INSTANCETABLE_H diff --git a/include/db/MetaTable.h b/include/db/MetaTable.h index e18615689..3da719044 100644 --- a/include/db/MetaTable.h +++ b/include/db/MetaTable.h @@ -1,14 +1,9 @@ -#pragma once +#ifndef METATABLE_H +#define METATABLE_H // hyperion #include -// qt -#include -#include -#include -#include - /// /// @brief meta table specific database interface /// @@ -17,47 +12,13 @@ class MetaTable : public DBManager public: /// construct wrapper with plugins table and columns - MetaTable(QObject* parent = nullptr, bool readonlyMode = false) - : DBManager(parent) - { - setReadonlyMode(readonlyMode); - - setTable("meta"); - createTable(QStringList()<<"uuid TEXT"<<"created_at TEXT"); - }; + explicit MetaTable(QObject* parent = nullptr); /// /// @brief Get the uuid, if the uuid is not set it will be created /// @return The uuid /// - inline QString getUUID() const - { - QVector results; - getRecords(results, QStringList() << "uuid"); - - for(const auto & entry : results) - { - if(!entry["uuid"].toString().isEmpty()) - return entry["uuid"].toString(); - } - - // create new uuidv5 based on net adapter MAC, save to db and return - QString hash; - foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces()) - { - if (!(interface.flags() & QNetworkInterface::IsLoopBack)) - { - hash = QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit(),QCryptographicHash::Sha1).toHex(); - break; - } - } - const QString newUuid = QUuid::createUuidV5(QUuid(), hash).toString().mid(1, 36); - VectorPair cond; - cond.append(CPair("uuid",newUuid)); - QVariantMap map; - map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - createRecord(cond, map); - - return newUuid; - } + QString getUUID() const; }; + +#endif // METATABLE_H diff --git a/include/db/SettingsTable.h b/include/db/SettingsTable.h index d1b31c213..66c6634da 100644 --- a/include/db/SettingsTable.h +++ b/include/db/SettingsTable.h @@ -1,12 +1,20 @@ -#pragma once +#ifndef SETTINGSTABLE_H +#define SETTINGSTABLE_H + +#include + +#ifdef WIN32 + #undef max +#endif -// hyperion #include +#include -// qt -#include #include +const int GLOABL_INSTANCE_ID = std::numeric_limits::max();; +const char DEFAULT_CONFIG_VERSION[] = "2.0.0-alpha.8"; + /// /// @brief settings table db interface /// @@ -15,14 +23,7 @@ class SettingsTable : public DBManager public: /// construct wrapper with settings table - SettingsTable(quint8 instance, QObject* parent = nullptr) - : DBManager(parent) - , _hyperion_inst(instance) - { - setTable("settings"); - // create table columns - createTable(QStringList()<<"type TEXT"<<"config TEXT"<<"hyperion_inst INTEGER"<<"updated_at TEXT"); - }; + SettingsTable(quint8 instance = GLOABL_INSTANCE_ID, QObject* parent = nullptr); /// /// @brief Create or update a settings record @@ -30,19 +31,7 @@ class SettingsTable : public DBManager /// @param[in] config The configuration data /// @return true on success else false /// - inline bool createSettingsRecord(const QString& type, const QString& config) const - { - QVariantMap map; - map["config"] = config; - map["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - return createRecord(cond, map); - } + bool createSettingsRecord(const QString& type, const QString& config) const; /// /// @brief Test if record exist, type can be global setting or local (instance) @@ -50,76 +39,64 @@ class SettingsTable : public DBManager /// @param[in] hyperion_inst The instance of hyperion assigned (might be empty) /// @return true on success else false /// - inline bool recordExist(const QString& type) const - { - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - return recordExists(cond); - } + bool recordExist(const QString& type) const; /// /// @brief Get 'config' column of settings entry as QJsonDocument /// @param[in] type The settings type /// @return The QJsonDocument /// - inline QJsonDocument getSettingsRecord(const QString& type) const - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - getRecord(cond, results, QStringList("config")); - return QJsonDocument::fromJson(results["config"].toByteArray()); - } + QJsonDocument getSettingsRecord(const QString& type) const; /// /// @brief Get 'config' column of settings entry as QString /// @param[in] type The settings type /// @return The QString /// - inline QString getSettingsRecordString(const QString& type) const - { - QVariantMap results; - VectorPair cond; - cond.append(CPair("type",type)); - // when a setting is not global we are searching also for the instance - if(!isSettingGlobal(type)) - cond.append(CPair("AND hyperion_inst",_hyperion_inst)); - getRecord(cond, results, QStringList("config")); - return results["config"].toString(); - } + QString getSettingsRecordString(const QString& type) const; + + QJsonObject getSettings(const QStringList& filteredTypes = {} ) const; + QJsonObject getSettings(const QVariant& instance, const QStringList& filteredTypes = {} ) const; + + QStringList nonExtingTypes() const; + QPair addMissingDefaults(); /// /// @brief Delete all settings entries associated with this instance, called from InstanceTable of HyperionIManager /// - inline void deleteInstance() const - { - VectorPair cond; - cond.append(CPair("hyperion_inst",_hyperion_inst)); - deleteRecord(cond); - } - - inline bool isSettingGlobal(const QString& type) const - { - // list of global settings - QStringList list; - // server port services - list << "jsonServer" << "protoServer" << "flatbufServer" << "forwarder" << "webConfig" << "network" - // capture - << "framegrabber" << "grabberV4L2" << "grabberAudio" - //Events - << "osEvents" << "cecEvents" << "schedEvents" - // other - << "logger" << "general"; - - return list.contains(type); - } + void deleteInstance() const; + + const QVector& getGlobalSettingTypes() const; + bool isGlobalSettingType(const QString& type) const; + + const QVector& getInstanceSettingTypes() const; + bool isInstanceSettingType(const QString& type) const; + + const QJsonObject& getDefaultSettings() const; + + semver::version getConfigVersion(); + QString getConfigVersionString(); + + bool resolveConfigVersion(); + bool resolveConfigVersion(QJsonObject generalConfig); private: - const quint8 _hyperion_inst; + QString fixVersion(const QString& version); + + QVector initializeGlobalSettingTypes() const; + static QVector globalSettingTypes; + static bool areGlobalSettingTypesInitialised; + + QVector initializeInstanceSettingTypes() const; + static QVector instanceSettingTypes; + static bool areInstanceSettingTypesInitialised; + + QJsonObject initializeDefaultSettings() const; + static QJsonObject defaultSettings; + static bool areDefaultSettingsInitialised; + + const quint8 _instance; + semver::version _configVersion; }; + +#endif // SETTINGSTABLE_H diff --git a/include/hyperion/AuthManager.h b/include/hyperion/AuthManager.h index 790f70b65..a4b021541 100644 --- a/include/hyperion/AuthManager.h +++ b/include/hyperion/AuthManager.h @@ -23,7 +23,7 @@ class AuthManager : public QObject private: friend class HyperionDaemon; /// constructor is private, can be called from HyperionDaemon - AuthManager(QObject *parent = nullptr, bool readonlyMode = false); + AuthManager(QObject *parent = nullptr); public: struct AuthDefinition diff --git a/include/hyperion/Hyperion.h b/include/hyperion/Hyperion.h index a15eda8ea..6a3772055 100644 --- a/include/hyperion/Hyperion.h +++ b/include/hyperion/Hyperion.h @@ -108,8 +108,6 @@ class Hyperion : public QObject /// QString getActiveDeviceType() const; - bool getReadOnlyMode() const {return _readOnlyMode; } - public slots: /// @@ -335,18 +333,9 @@ public slots: /// /// @brief Save a complete json config /// @param config The entire config object - /// @param correct If true will correct json against schema before save /// @return True on success else false /// - bool saveSettings(const QJsonObject& config, bool correct = false); - - /// - /// @brief Restore a complete json config - /// @param config The entire config object - /// @param correct If true will correct json against schema before save - /// @return True on success else false - /// - bool restoreSettings(const QJsonObject& config, bool correct = false); + QPair saveSettings(const QJsonObject& config); /// ############ /// COMPONENTREGISTER @@ -552,7 +541,7 @@ private slots: /// @brief Constructs the Hyperion instance, just accessible for HyperionIManager /// @param instance The instance index /// - Hyperion(quint8 instance, bool readonlyMode = false); + Hyperion(quint8 instance); /// instance index const quint8 _instIndex; @@ -615,6 +604,4 @@ private slots: /// Boblight instance BoblightServer* _boblightServer; #endif - - bool _readOnlyMode; }; diff --git a/include/hyperion/HyperionIManager.h b/include/hyperion/HyperionIManager.h index d7610d5b4..60a170764 100644 --- a/include/hyperion/HyperionIManager.h +++ b/include/hyperion/HyperionIManager.h @@ -59,12 +59,18 @@ public slots: /// QVector getInstanceData() const; + QString getInstanceName(quint8 inst = 0); /// /// @brief Get all instance indicies of running instances /// QList getRunningInstanceIdx() const; + /// + /// @brief Get all instance indicies configured + /// + QList getInstanceIds() const; + /// /// @brief Start a Hyperion instance /// @param instance Instance index @@ -115,8 +121,6 @@ public slots: /// bool saveName(quint8 inst, const QString& name); - QString getRootPath() const { return _rootPath; } - signals: /// /// @brief Emits whenever the state of a instance changes according to enum instanceState @@ -195,9 +199,8 @@ private slots: friend class HyperionDaemon; /// /// @brief Construct the Manager - /// @param The root path of all userdata /// - HyperionIManager(const QString& rootPath, QObject* parent = nullptr, bool readonlyMode = false); + HyperionIManager(QObject* parent = nullptr); /// /// @brief Start all instances that are marked as enabled in db. Non blocking @@ -218,12 +221,9 @@ private slots: private: Logger* _log; InstanceTable* _instanceTable; - const QString _rootPath; QMap _runningInstances; - QList _startQueue; - - bool _readonlyMode; + QList _startQueue; /// All pending requests QMap _pendingRequests; }; diff --git a/include/hyperion/SettingsManager.h b/include/hyperion/SettingsManager.h index 9d525cf61..ddb922a99 100644 --- a/include/hyperion/SettingsManager.h +++ b/include/hyperion/SettingsManager.h @@ -3,14 +3,11 @@ #include #include -#include -using namespace semver; +#include // qt includes #include -const int GLOABL_INSTANCE_ID = 255; - class Hyperion; class SettingsTable; @@ -26,23 +23,21 @@ class SettingsManager : public QObject /// @params instance Instance index of HyperionInstanceManager /// @params parent The parent hyperion instance /// - SettingsManager(quint8 instance, QObject* parent = nullptr, bool readonlyMode = false); + SettingsManager(quint8 instance = GLOABL_INSTANCE_ID, QObject* parent = nullptr); /// - /// @brief Save a complete json configuration + /// @brief Save a complete JSON configuration /// @param config The entire config object - /// @param correct If true will correct json against schema before save - /// @return True on success else false + /// @return True on success else false, plus validation errors /// - bool saveSettings(QJsonObject config, bool correct = false); + QPair saveSettings(const QJsonObject& config); /// - /// @brief Restore a complete json configuration + /// @brief Correct a complete JSON configuration /// @param config The entire config object - /// @param correct If true will correct json against schema before save - /// @return True on success else false + /// @return True on success else false, plus correction details /// - bool restoreSettings(QJsonObject config, bool correct = false); + QPair correctSettings(QJsonObject& config); /// /// @brief get a single setting json from configuration @@ -52,10 +47,18 @@ class SettingsManager : public QObject QJsonDocument getSetting(settings::type type) const; /// - /// @brief get the full settings object of this instance (with global settings) + /// @brief get a single setting json from configuration + /// @param type The type as string + /// @return The requested json data as QJsonDocument + /// + QJsonDocument getSetting(const QString& type) const; + + /// + /// @brief get the selected settings objects of this instance (including global settings) /// @return The requested json /// - QJsonObject getSettings() const; + QJsonObject getSettings(const QStringList& filteredTypes = {}) const; + QJsonObject getSettings(const QVariant& instance, const QStringList& filteredTypes = {} ) const; signals: /// @@ -71,31 +74,19 @@ class SettingsManager : public QObject /// @param config The configuration object /// @return True when a migration has been triggered /// - bool handleConfigUpgrade(QJsonObject& config); + bool upgradeConfig(QJsonObject& config); - bool resolveConfigVersion(QJsonObject& config); - /// Logger instance Logger* _log; - /// Hyperion instance - Hyperion* _hyperion; - /// Instance number quint8 _instance; /// instance of database table interface SettingsTable* _sTable; - /// the schema - static QJsonObject schemaJson; - /// the current configuration of this instance QJsonObject _qconfig; - semver::version _configVersion; - semver::version _previousVersion; - - bool _readonlyMode; }; diff --git a/include/utils/JsonUtils.h b/include/utils/JsonUtils.h index 10b89d905..9629525a6 100644 --- a/include/utils/JsonUtils.h +++ b/include/utils/JsonUtils.h @@ -3,13 +3,14 @@ #include #include +#include #include #include #include namespace JsonUtils { /// - /// @brief read a json file and get the parsed result on success + /// @brief read a JSON file and get the parsed result on success /// @param[in] path The file path to read /// @param[out] obj Returns the parsed QJsonObject /// @param[in] log The logger of the caller to print errors @@ -17,6 +18,7 @@ namespace JsonUtils { /// @return true on success else false /// QPair readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError=false); + QPair readFile(const QString& path, QJsonValue& obj, Logger* log, bool ignError=false); /// /// @brief read a schema file and resolve $refs @@ -28,18 +30,19 @@ namespace JsonUtils { bool readSchema(const QString& path, QJsonObject& obj, Logger* log); /// - /// @brief parse a json QString and get a QJsonObject. Overloaded funtion - /// @param[in] path The file path/name just used for log messages + /// @brief parse a JSON QString and get a QJsonObject. Overloaded funtion + /// @param[in] path The file path/name context used for log messages /// @param[in] data Data to parse /// @param[out] obj Retuns the parsed QJsonObject /// @param[in] log The logger of the caller to print errors /// @return true on success else false /// QPair parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log); + QPair parse(const QString& path, const QString& data, QJsonValue& value, Logger* log); /// - /// @brief parse a json QString and get a QJsonArray. Overloaded function - /// @param[in] path The file path/name just used for log messages + /// @brief parse a JSON QString and get a QJsonArray. Overloaded function + /// @param[in] path The file path/name context used for log messages /// @param[in] data Data to parse /// @param[out] arr Retuns the parsed QJsonArray /// @param[in] log The logger of the caller to print errors @@ -48,8 +51,8 @@ namespace JsonUtils { QPair parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log); /// - /// @brief parse a json QString and get a QJsonDocument - /// @param[in] path The file path/name just used for log messages + /// @brief parse a JSON QString and get a QJsonDocument + /// @param[in] path The file path/name context used for log messages /// @param[in] data Data to parse /// @param[out] doc Retuns the parsed QJsonDocument /// @param[in] log The logger of the caller to print errors @@ -58,29 +61,39 @@ namespace JsonUtils { QPair parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log); /// - /// @brief Validate json data against a schema - /// @param[in] file The path/name of json file just used for log messages - /// @param[in] json The json data + /// @brief Validate JSON data against a schema + /// @param[in] file The path/name of JSON file context used for log messages + /// @param[in] json The JSON data /// @param[in] schemaP The schema path /// @param[in] log The logger of the caller to print errors - /// @return true on success else false + /// @return true on success else false, plus validation errors /// - QPair validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log); + QPair validate(const QString& file, const QJsonValue& json, const QString& schemaPath, Logger* log); /// - /// @brief Validate json data against a schema - /// @param[in] file The path/name of json file just used for log messages - /// @param[in] json The json data + /// @brief Validate JSON data against a schema + /// @param[in] file The path/name of JSON file context used for log messages + /// @param[in] json The JSON data /// @param[in] schema The schema object /// @param[in] log The logger of the caller to print errors - /// @return true on success else false + /// @return true on success else false, plus validation errors /// - QPair validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log); + QPair validate(const QString& file, const QJsonValue& json, const QJsonObject& schema, Logger* log); /// - /// @brief Write json data to file + /// @brief Validate JSON data against a schema + /// @param[in] file The path/name of JSON file context used for log messages + /// @param[in/out] json The JSON data + /// @param[in] schema The schema object + /// @param[in] log The logger of the caller to print errors + /// @return true on success else false, plus correction messages + /// + QPair correct(const QString& file, QJsonValue& json, const QJsonObject& schema, Logger* log); + + /// + /// @brief Write JSON data to file /// @param[in] filenameThe file path to write - /// @param[in] json The json data to write + /// @param[in] json The JSON data to write /// @param[in] log The logger of the caller to print errors /// @return true on success else false /// @@ -94,4 +107,15 @@ namespace JsonUtils { /// @return true on success else false /// bool resolveRefs(const QJsonObject& schema, QJsonObject& obj, Logger* log); + + + /// + /// @brief Function to convert QJsonValue to QString using QJsonDocument + /// + QString jsonValueToQString(const QJsonValue &value, QJsonDocument::JsonFormat format = QJsonDocument::Compact); + + /// + /// @brief Function to merge two QJsonObjects + /// + QJsonObject mergeJsonObjects(const QJsonObject &obj1, const QJsonObject &obj2, bool overrideObj1 = false); } diff --git a/include/utils/jsonschema/QJsonSchemaChecker.h b/include/utils/jsonschema/QJsonSchemaChecker.h index dbf70683f..0ec1f8777 100644 --- a/include/utils/jsonschema/QJsonSchemaChecker.h +++ b/include/utils/jsonschema/QJsonSchemaChecker.h @@ -44,7 +44,7 @@ class QJsonSchemaChecker /// @return The first boolean is true when the arguments is valid according to the schema. The second is true when the schema contains no errors /// @return TODO: Check the Schema in SetSchema() function and remove the QPair result /// - QPair validate(const QJsonObject& value, bool ignoreRequired = false); + QPair validate(const QJsonValue& value, bool ignoreRequired = false); /// /// @brief Auto correct a JSON structure @@ -52,7 +52,7 @@ class QJsonSchemaChecker /// @param ignoreRequired Ignore the "required" keyword in hyperion schema. Default is false /// @return The corrected JSON structure /// - QJsonObject getAutoCorrectedConfig(const QJsonObject& value, bool ignoreRequired = false); + QJsonValue getAutoCorrectedConfig(const QJsonValue& value, bool ignoreRequired = false); /// /// @return A list of error messages @@ -207,7 +207,7 @@ class QJsonSchemaChecker /// Auto correction variable QString _correct; /// The auto corrected json-configuration - QJsonObject _autoCorrected; + QJsonValue _autoCorrected; /// The current location into a json-configuration structure being checked QStringList _currentPath; /// The result messages collected during the schema verification diff --git a/include/utils/jsonschema/QJsonUtils.h b/include/utils/jsonschema/QJsonUtils.h index 98498a2b2..274934c02 100644 --- a/include/utils/jsonschema/QJsonUtils.h +++ b/include/utils/jsonschema/QJsonUtils.h @@ -11,7 +11,7 @@ class QJsonUtils { public: - static void modify(QJsonObject& value, QStringList path, const QJsonValue& newValue = QJsonValue::Null, QString propertyName = "") + static void modify(QJsonValue& value, QStringList path, const QJsonValue& newValue = QJsonValue::Null, QString propertyName = "") { QJsonObject result; @@ -27,7 +27,7 @@ class QJsonUtils *it = current.mid(1, current.size()-1); } - if (!value.isEmpty()) + if (! (value.toObject().isEmpty() && value.toArray().isEmpty()) ) modifyValue(value, result, path, newValue, propertyName); else if (newValue != QJsonValue::Null && !propertyName.isEmpty()) result[propertyName] = newValue; diff --git a/include/utils/version.hpp b/include/utils/version.hpp index 11c3388ca..4b5f6b4ec 100644 --- a/include/utils/version.hpp +++ b/include/utils/version.hpp @@ -385,7 +385,7 @@ namespace semver { return -1; } - version& operator= (version& rgt) + version& operator= (const version& rgt) { if ((*this) != rgt) { @@ -404,17 +404,17 @@ namespace semver { return *this; } - friend bool operator== (version &lft, version &rgt) + friend bool operator== (const version &lft, const version &rgt) { return !(lft != rgt); } - friend bool operator!= (version &lft, version &rgt) + friend bool operator!= (const version &lft, const version &rgt) { return (lft > rgt) || (lft < rgt); } - friend bool operator> (version &lft, version &rgt) + friend bool operator> (const version &lft, const version &rgt) { // Major if (lft.getMajor() < 0 && rgt.getMajor() >= 0) @@ -522,17 +522,17 @@ namespace semver { return false; } - friend bool operator>= (version &lft, version &rgt) + friend bool operator>= (const version &lft, const version &rgt) { return (lft > rgt) || (lft == rgt); } - friend bool operator< (version &lft, version &rgt) + friend bool operator< (const version &lft, const version &rgt) { return (rgt > lft); } - friend bool operator<= (version &lft, version &rgt) + friend bool operator<= (const version &lft, const version &rgt) { return (lft < rgt) || (lft == rgt); } diff --git a/libsrc/api/API.cpp b/libsrc/api/API.cpp index 961b415c3..b34ec5083 100644 --- a/libsrc/api/API.cpp +++ b/libsrc/api/API.cpp @@ -391,34 +391,6 @@ QString API::saveEffect(const QJsonObject &data) } #endif -bool API::saveSettings(const QJsonObject &data) -{ - bool isSaved {true}; - if (!_adminAuthorized) - { - isSaved = false; - } - else - { - QMetaObject::invokeMethod(_hyperion, "saveSettings", Qt::DirectConnection, Q_RETURN_ARG(bool, isSaved), Q_ARG(QJsonObject, data), Q_ARG(bool, true)); - } - return isSaved; -} - -bool API::restoreSettings(const QJsonObject &data) -{ - bool isRestored {true}; - if (!_adminAuthorized) - { - isRestored = false; - } - else - { - QMetaObject::invokeMethod(_hyperion, "restoreSettings", Qt::DirectConnection, Q_RETURN_ARG(bool, isRestored), Q_ARG(QJsonObject, data), Q_ARG(bool, true)); - } - return isRestored; -} - bool API::updateHyperionPassword(const QString &password, const QString &newPassword) { bool isPwUpdated {true}; diff --git a/libsrc/api/JSONRPC_schema/schema-config.json b/libsrc/api/JSONRPC_schema/schema-config.json index 204661cff..c5e09eb99 100644 --- a/libsrc/api/JSONRPC_schema/schema-config.json +++ b/libsrc/api/JSONRPC_schema/schema-config.json @@ -10,18 +10,42 @@ "subcommand": { "type" : "string", "required" : true, - "enum" : ["getconfig","getschema","setconfig","restoreconfig","reload"] - }, - "instance" : { - "type" : "integer", - "minimum": 0, - "maximum": 255 + "enum" : ["getconfig","getconfig-old","getschema","setconfig","restoreconfig","reload"] }, "tan" : { "type" : "integer" }, + "configFilter": { + "global" : { + "types": { + "type": "array", + "required": false, + "items" : { + "type" : "string" + } + } + }, + "instances" : { + "ids" : { + "type": "array", + "required": true, + "items" : {}, + "minItems": 1 + }, + "types": { + "type": "array", + "required": false, + "items" :{ + "type" : "string" + } + } + } + }, "config": { - "type" : "object" + "required": false, + "$ref": "schema-settings-full-relaxed.json", + "required": false, + "$ref": "schema-settings-ui.json" } }, "additionalProperties": false diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index d92eab799..7b7fc757c 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -39,6 +39,7 @@ // auth manager #include +#include #ifdef ENABLE_MDNS // mDNS discover @@ -67,6 +68,8 @@ constexpr int BEARER_TOKEN_TAG_LENGTH = sizeof(BEARER_TOKEN_TAG) - 1; const int MIN_PASSWORD_LENGTH = 8; const int APP_TOKEN_LENGTH = 36; +const char SETTINGS_UI_SCHEMA_FILE[] = ":/schema-settings-ui.json"; + const bool verbose = false; } @@ -697,7 +700,11 @@ void JsonAPI::handleConfigCommand(const QJsonObject& message, const JsonApiComma handleSchemaGetCommand(message, cmd); break; - case SubCommand::GetConfig: + case SubCommand::GetConfig: + handleConfigGetCommand(message, cmd); + break; + + case SubCommand::GetConfigOld: sendSuccessDataReply(_hyperion->getQJsonConfig(), cmd); break; @@ -722,45 +729,191 @@ void JsonAPI::handleConfigCommand(const QJsonObject& message, const JsonApiComma void JsonAPI::handleConfigSetCommand(const QJsonObject &message, const JsonApiCommand& cmd) { - if (message.contains("config")) + if (DBManager::isReadOnly()) + { + sendErrorReply("Database Error", {"Hyperion is running in read-only mode","Configuration updates are not possible"}, cmd); + return; + } + + QJsonObject config = message["config"].toObject(); + if (config.contains("global") || config.contains("instances")) { - QJsonObject config = message["config"].toObject(); - if (API::isHyperionEnabled()) + QStringList errorDetails; + + QMap instancesNewConfigs; + + const QJsonArray instances = config["instances"].toArray(); + if (!instances.isEmpty()) { - if ( API::saveSettings(config) ) { - sendSuccessReply(cmd); - } else { - sendErrorReply("Save settings failed", cmd); + QList configuredInstanceIds = _instanceManager->getInstanceIds(); + for (const auto &instance : instances) + { + QJsonObject instanceObject = instance.toObject(); + const QJsonValue idx = instanceObject["id"]; + if (idx.isDouble()) + { + quint8 instanceId = static_cast(idx.toInt()); + if (configuredInstanceIds.contains(instanceId)) + { + instancesNewConfigs.insert(instanceId,instanceObject.value("settings").toObject()); + } + else + { + errorDetails.append(QString("Given instance id '%1' does not exist. Configuration item will be ignored").arg(instanceId)); + } + } } } - else + + const QJsonObject globalSettings = config["global"].toObject().value("settings").toObject(); + if (!globalSettings.isEmpty()) + { + const QJsonObject instanceZeroConfig = instancesNewConfigs.value(0); + instancesNewConfigs.insert(0, JsonUtils::mergeJsonObjects(instanceZeroConfig, globalSettings)); + } + + QMapIterator i (instancesNewConfigs); + while (i.hasNext()) { + i.next(); + + quint8 idx = i.key(); + Hyperion* instance = HyperionIManager::getInstance()->getHyperionInstance(idx); + + QPair isSaved = instance->saveSettings(i.value()); + errorDetails.append(isSaved.second); + } + + if (!errorDetails.isEmpty()) + { + sendErrorReply("Update configuration failed", errorDetails, cmd); + return; + } + + sendSuccessReply(cmd); + + return; + } + + if (config.isEmpty()) + { + sendErrorReply("Update configuration failed", {"No configuration data provided!"}, cmd); + return; + } + + //Backward compatability until UI mesages are updated + if (API::isHyperionEnabled()) + { + QStringList errorDetails; + + QPair isSaved = _hyperion->saveSettings(config); + errorDetails.append(isSaved.second); + + if (!errorDetails.isEmpty()) { - sendErrorReply("It is not possible saving a configuration while Hyperion is disabled", cmd); + sendErrorReply("Save settings failed", errorDetails, cmd); + return; } + + sendSuccessReply(cmd); + } + else + { + sendErrorReply("Updating the configuration while Hyperion is disabled is not possible", cmd); } } -void JsonAPI::handleConfigRestoreCommand(const QJsonObject &message, const JsonApiCommand& cmd) +void JsonAPI::handleConfigGetCommand(const QJsonObject &message, const JsonApiCommand& cmd) { - if (message.contains("config")) + QJsonObject settings; + QStringList errorDetails; + + QJsonObject filter = message["configFilter"].toObject(); + if (!filter.isEmpty()) { - QJsonObject config = message["config"].toObject(); - if (API::isHyperionEnabled()) + QStringList globalFilterTypes; + + const QJsonObject globalConfig = filter["global"].toObject(); + if (!globalConfig.isEmpty()) { - if ( API::restoreSettings(config) ) - { - sendSuccessReply(cmd); + const QJsonArray globalTypes = globalConfig["types"].toArray(); + for (const auto &type : globalTypes) { + if (type.isString()) { + globalFilterTypes.append(type.toString()); + } } - else - { - sendErrorReply("Restore settings failed", cmd); + } + + QList instanceListFilter; + QStringList instanceFilterTypes; + + const QJsonObject instances = filter["instances"].toObject(); + if (!instances.isEmpty()) + { + QList configuredInstanceIds = _instanceManager->getInstanceIds(); + const QJsonArray instanceIds = instances["ids"].toArray(); + for (const auto &idx : instanceIds) { + if (idx.isDouble()) { + quint8 instanceId = static_cast(idx.toInt()); + if (configuredInstanceIds.contains(instanceId)) + { + instanceListFilter.append(instanceId); + } + else + { + errorDetails.append(QString("Given instance number '%1' does not exist.").arg(instanceId)); + } + } + } + + const QJsonArray instanceTypes = instances["types"].toArray(); + for (const auto &type : instanceTypes) { + if (type.isString()) { + instanceFilterTypes.append(type.toString()); + } } } + + settings = JsonInfo::getConfiguration(instanceListFilter, instanceFilterTypes, globalFilterTypes); + } + else + { + //Get complete configuration + settings = JsonInfo::getConfiguration(); + } + + if (!settings.empty()) + { + sendSuccessDataReplyWithError(settings, cmd, errorDetails); + } + else + { + sendErrorReply("Generating full config failed", cmd); + } +} + +void JsonAPI::handleConfigRestoreCommand(const QJsonObject &message, const JsonApiCommand& cmd) +{ + QJsonObject config = message["config"].toObject(); + if (API::isHyperionEnabled()) + { + DBConfigManager configManager; + QPair result = configManager.updateConfiguration(config, false); + if (result.first) + { + QString infoMsg {"Restarting after importing configuration successfully."}; + sendSuccessDataReply(infoMsg, cmd); + Info(_log, "%s", QSTRING_CSTR(infoMsg)); + emit signalEvent(Event::Restart); + } else { - sendErrorReply("Restoring configuration while Hyperion is disabled is not possible", cmd); + sendErrorReply("Restore configuration failed", result.second, cmd); } } + else + { + sendErrorReply("Restoring configuration while Hyperion is disabled is not possible", cmd); + } } void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonApiCommand& cmd) @@ -774,7 +927,7 @@ void JsonAPI::handleSchemaGetCommand(const QJsonObject& /*message*/, const JsonA Q_INIT_RESOURCE(resource); // read the hyperion json schema from the resource - QString schemaFile = ":/hyperion-schema"; + QString schemaFile = SETTINGS_UI_SCHEMA_FILE; try { @@ -1156,7 +1309,7 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom { QString replyMsg; - const quint8 &inst = static_cast(message["instance"].toInt()); + const quint8 inst = static_cast(message["instance"].toInt()); const QString &name = message["name"].toString(); switch (cmd.subCommand) { @@ -1191,7 +1344,6 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom break; case SubCommand::DeleteInstance: - handleConfigRestoreCommand(message, cmd); if (API::deleteInstance(inst, replyMsg)) { sendSuccessReply(cmd); @@ -1213,7 +1365,7 @@ void JsonAPI::handleInstanceCommand(const QJsonObject &message, const JsonApiCom if (cmd.subCommand == SubCommand::CreateInstance) { replyMsg = API::createInstance(name); } else { - replyMsg = setInstanceName(inst, name); + replyMsg = API::setInstanceName(inst, name); } if (replyMsg.isEmpty()) { diff --git a/libsrc/api/JsonInfo.cpp b/libsrc/api/JsonInfo.cpp index 7c3f8129e..ab0a2dc3d 100644 --- a/libsrc/api/JsonInfo.cpp +++ b/libsrc/api/JsonInfo.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -498,8 +499,8 @@ QJsonObject JsonInfo::getSystemInfo(const Hyperion* hyperion) hyperionInfo["gitremote"] = QString(HYPERION_GIT_REMOTE); hyperionInfo["time"] = QString(__DATE__ " " __TIME__); hyperionInfo["id"] = AuthManager::getInstance()->getID(); - hyperionInfo["rootPath"] = HyperionIManager::getInstance()->getRootPath(); - hyperionInfo["readOnlyMode"] = hyperion->getReadOnlyMode(); + hyperionInfo["configDatabaseFile"] = DBManager::getFileInfo().absoluteFilePath(); + hyperionInfo["readOnlyMode"] = DBManager::isReadOnly(); QCoreApplication* app = QCoreApplication::instance(); hyperionInfo["isGuiMode"] = qobject_cast(app) != nullptr; @@ -622,3 +623,9 @@ QJsonArray JsonInfo::discoverScreenInputs(const QJsonObject& params) const return screenInputs; } + +QJsonObject JsonInfo::getConfiguration(const QList& instancesfilter, const QStringList& instanceFilteredTypes, const QStringList& globalFilterTypes ) +{ + DBConfigManager configManager; + return configManager.getConfiguration(instancesfilter, instanceFilteredTypes, globalFilterTypes ); +} diff --git a/libsrc/db/AuthTable.cpp b/libsrc/db/AuthTable.cpp new file mode 100644 index 000000000..32ce8e55f --- /dev/null +++ b/libsrc/db/AuthTable.cpp @@ -0,0 +1,176 @@ + +// hyperion +#include +#include + +// qt +#include +#include + +/// construct wrapper with auth table +AuthTable::AuthTable(QObject* parent) + : DBManager(parent) +{ + // init Auth table + setTable("auth"); + // create table columns + createTable(QStringList()<<"user TEXT"<<"password BLOB"<<"token BLOB"<<"salt BLOB"<<"comment TEXT"<<"id TEXT"<<"created_at TEXT"<<"last_use TEXT"); +}; + +bool AuthTable::createUser(const QString& user, const QString& password) +{ + // new salt + QByteArray salt = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); + QVariantMap map; + map["user"] = user; + map["salt"] = salt; + map["password"] = hashPasswordWithSalt(password,salt); + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + return createRecord({{"user",user}}, map); +} + +bool AuthTable::userExist(const QString& user) +{ + return recordExists({{"user",user}}); +} + +bool AuthTable::isUserAuthorized(const QString& user, const QString& password) +{ + if(userExist(user) && (calcPasswordHashOfUser(user, password) == getPasswordHashOfUser(user))) + { + updateUserUsed(user); + return true; + } + return false; +} + +bool AuthTable::isUserTokenAuthorized(const QString& usr, const QString& token) +{ + if(getUserToken(usr) == token.toUtf8()) + { + updateUserUsed(usr); + return true; + } + return false; +} + +bool AuthTable::setUserToken(const QString& user) +{ + QVariantMap map; + map["token"] = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha512).toHex(); + + return updateRecord({{"user",user}}, map); +} + +const QByteArray AuthTable::getUserToken(const QString& user) +{ + QVariantMap results; + getRecord({{"user",user}}, results, QStringList()<<"token"); + + return results["token"].toByteArray(); +} + +bool AuthTable::updateUserPassword(const QString& user, const QString& newPassword) +{ + QVariantMap map; + map["password"] = calcPasswordHashOfUser(user, newPassword); + + return updateRecord({{"user",user}}, map); +} + +bool AuthTable::resetHyperionUser() +{ + QVariantMap map; + map["password"] = calcPasswordHashOfUser(hyperion::DEFAULT_USER, hyperion::DEFAULT_PASSWORD); + + return updateRecord({{"user", hyperion::DEFAULT_USER}}, map); +} + +void AuthTable::updateUserUsed(const QString& user) +{ + QVariantMap map; + map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + updateRecord({{"user",user}}, map); +} + +bool AuthTable::tokenExist(const QString& token) +{ + QVariantMap map; + map["last_use"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("token", hashToken(token))); + if(recordExists(cond)) + { + // update it + createRecord(cond,map); + return true; + } + return false; +} + +bool AuthTable::createToken(const QString& token, const QString& comment, const QString& identifier) +{ + QVariantMap map; + map["comment"] = comment; + map["id"] = identifierExist(identifier) ? QUuid::createUuid().toString().remove("{").remove("}").left(5) : identifier; + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + return createRecord({{"token", hashToken(token)}}, map); +} + +bool AuthTable::deleteToken(const QString& identifier) +{ + return deleteRecord({{"id", identifier}}); +} + +bool AuthTable::renameToken(const QString &identifier, const QString &comment) +{ + QVariantMap map; + map["comment"] = comment; + + return updateRecord({{"id", identifier}}, map); +} + +const QVector AuthTable::getTokenList() +{ + QVector results; + getRecords(results, QStringList() << "comment" << "id" << "last_use"); + + return results; +} + +bool AuthTable::identifierExist(const QString& identifier) +{ + return recordExists({{"id", identifier}}); +} + +const QByteArray AuthTable::getPasswordHashOfUser(const QString& user) +{ + QVariantMap results; + getRecord({{"user",user}}, results, QStringList()<<"password"); + + return results["password"].toByteArray(); +} + +const QByteArray AuthTable::calcPasswordHashOfUser(const QString& user, const QString& password) +{ + // get salt + QVariantMap results; + getRecord({{"user",user}}, results, QStringList()<<"salt"); + + // calc + return hashPasswordWithSalt(password,results["salt"].toByteArray()); +} + +const QByteArray AuthTable::hashPasswordWithSalt(const QString& password, const QByteArray& salt) +{ + return QCryptographicHash::hash(password.toUtf8().append(salt), QCryptographicHash::Sha512).toHex(); +} + +const QByteArray AuthTable::hashToken(const QString& token) +{ + return QCryptographicHash::hash(token.toUtf8(), QCryptographicHash::Sha512).toHex(); +} diff --git a/libsrc/db/CMakeLists.txt b/libsrc/db/CMakeLists.txt index 1beb3fe5d..8b83df591 100644 --- a/libsrc/db/CMakeLists.txt +++ b/libsrc/db/CMakeLists.txt @@ -1,10 +1,18 @@ add_library(database ${CMAKE_SOURCE_DIR}/include/db/AuthTable.h ${CMAKE_SOURCE_DIR}/include/db/DBManager.h + ${CMAKE_SOURCE_DIR}/include/db/DBConfigManager.h + ${CMAKE_SOURCE_DIR}/include/db/DBMigrationManager.h ${CMAKE_SOURCE_DIR}/include/db/InstanceTable.h ${CMAKE_SOURCE_DIR}/include/db/MetaTable.h ${CMAKE_SOURCE_DIR}/include/db/SettingsTable.h + ${CMAKE_SOURCE_DIR}/libsrc/db/AuthTable.cpp ${CMAKE_SOURCE_DIR}/libsrc/db/DBManager.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/DBConfigManager.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/DBMigrationManager.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/InstanceTable.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/MetaTable.cpp + ${CMAKE_SOURCE_DIR}/libsrc/db/SettingsTable.cpp ) target_link_libraries(database diff --git a/libsrc/db/DBConfigManager.cpp b/libsrc/db/DBConfigManager.cpp new file mode 100644 index 000000000..24f22b2ed --- /dev/null +++ b/libsrc/db/DBConfigManager.cpp @@ -0,0 +1,382 @@ +#include + +#include +#include "db/SettingsTable.h" +#include + +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace { +const char SETTINGS_FULL_SCHEMA_FILE[] = ":/schema-settings-full.json"; +} + +DBConfigManager::DBConfigManager(QObject* parent) + : DBManager(parent) +{ +} + +QPair DBConfigManager::importJson(const QString& configFile) +{ + Info(_log,"Import configuration file '%s'", QSTRING_CSTR(configFile)); + + QJsonObject config; + QPair result = JsonUtils::readFile(configFile, config, _log, false); + + if (!result.first) + { + QString errorText = QString("Import configuration file '%1' failed!").arg(configFile); + result.second.prepend(errorText); + Error(_log, "%s", QSTRING_CSTR(errorText)); + return result; + } + + DBMigrationManager migtrationManger; + migtrationManger.migrateSettings(config); + + return updateConfiguration(config, true); +} + +bool DBConfigManager::exportJson(const QString& path) const +{ + bool isExported {false}; + + QDir exportPath{path}; + if (path.isEmpty()) + { + exportPath.setPath(getDataDirectory().absoluteFilePath("archive")); + } + + QString jsonFile; + if (QDir().mkpath(exportPath.absolutePath())) + { + const QJsonObject configurtion = getConfiguration(); + if (!configurtion.isEmpty()) + { + const QJsonObject generalSettings = configurtion.value("global").toObject().value("settings").toObject().value("general").toObject(); + const QString configVersion = generalSettings.value("configVersion").toString(); + + jsonFile = exportPath.absoluteFilePath(QString("HyperionBackup_%1_v%2.json").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_hh:mm:ss:zzz"), configVersion )); + if (FileUtils::writeFile(jsonFile, QJsonDocument(configurtion).toJson(QJsonDocument::Indented), _log)) + { + isExported = true; + } + } + } + + if (isExported) + { + Info(_log, "Successfully exported configuration to '%s'", QSTRING_CSTR(jsonFile)); + } + else + { + Error(_log, "Failed to export configuration to '%s'", QSTRING_CSTR(jsonFile)); + } + + return isExported; +} + +QPair DBConfigManager::validateConfiguration() +{ + QJsonObject config = getConfiguration(); + return validateConfiguration(config, false); +} + +QPair DBConfigManager::validateConfiguration(QJsonObject& config, bool doCorrections) +{ + Info(_log, "Validate configuration%s", doCorrections ? " and apply corrections, if required" : ""); + + QStringList errorList; + if (config.isEmpty()) + { + QString errorText {"No configuration data provided!"}; + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + return qMakePair (false, errorList ); + } + + QJsonObject schema = QJsonFactory::readSchema(SETTINGS_FULL_SCHEMA_FILE); + + bool wasCorrected {false}; + if (doCorrections) + { + QJsonValue configValue(config); + QPair correctionResult = JsonUtils::correct(__FUNCTION__, configValue, schema, _log); + + wasCorrected = correctionResult.first; + if (wasCorrected) + { + config = configValue.toObject(); + } + } + else + { + QPair validationResult = JsonUtils::validate(__FUNCTION__, config, schema, _log); + if (!validationResult.first) + { + Error(_log, "Configuration has errors!"); + return qMakePair (false, validationResult.second ); + } + } + + Info(_log, "Configuration is valid%s", wasCorrected ? ", but had to be corrected" : ""); + return qMakePair (true, errorList ); +} + +QPair DBConfigManager::updateConfiguration() +{ + QJsonObject config = getConfiguration(); + return updateConfiguration(config, true); +} + + +QPair DBConfigManager::addMissingDefaults() +{ + Debug(_log, "Add default settings for missing configuration items"); + + QStringList errorList; + + SettingsTable globalSettingsTable; + QPair result = globalSettingsTable.addMissingDefaults(); + errorList.append(result.second); + + InstanceTable instanceTable; + + //Ensure that first instance as default one exists + instanceTable.createDefaultInstance(); + + const QList instances = instanceTable.getAllInstanceIDs(); + for (const auto &instanceIdx : instances) + { + SettingsTable instanceSettingsTable(instanceIdx); + result = instanceSettingsTable.addMissingDefaults(); + errorList.append(result.second); + } + + if(errorList.isEmpty()) + { + Debug(_log, "Successfully defaulted settings for missing configuration items"); + } + + return qMakePair (errorList.isEmpty(), errorList ); +} + +QPair DBConfigManager::updateConfiguration(QJsonObject& config, bool doCorrections) +{ + Info(_log, "Update configuration database"); + + QPair validationResult = validateConfiguration(config, doCorrections); + if (!validationResult.first) + { + return validationResult; + } + + Info(_log, "Create backup of current configuration"); + if (!exportJson()) + { + Warning(_log, "Backup of current configuration failed"); + } + + QStringList errorList; + QSqlDatabase idb = getDB(); + + if (!startTransaction(idb, errorList)) + { + return qMakePair(false, errorList); + } + + // Clear existing tables and import the new configuration. + bool errorOccurred = false; + if (!deleteTable("instances") && deleteTable("settings")) + { + errorOccurred = true; + logErrorAndAppend("Failed to clear tables before import", errorList); + } + else + { + errorOccurred = !importGlobalSettings(config, errorList) || !importInstances(config, errorList); + } + + // Rollback if any error occurred during the import process. + if (errorOccurred) + { + if (!rollbackTransaction(idb, errorList)) + { + return qMakePair(false, errorList); + } + } + + commiTransaction(idb, errorList); + + if (errorList.isEmpty()) + { + Info(_log, "Successfully imported new configuration"); + } + + return qMakePair(errorList.isEmpty(), errorList); +} + +// Function to import global settings +bool DBConfigManager::importGlobalSettings(const QJsonObject& config, QStringList& errorList) +{ + SettingsTable settingsTableGlobal; + const QJsonObject globalConfig = config.value("global").toObject(); + const QJsonObject globalSettings = globalConfig.value("settings").toObject(); + + bool errorOccurred = false; + for (QJsonObject::const_iterator it = globalSettings.constBegin(); it != globalSettings.constEnd(); ++it) + { + if (!settingsTableGlobal.createSettingsRecord(it.key(), JsonUtils::jsonValueToQString(it.value()))) + { + errorOccurred = true; + logErrorAndAppend("Failed to import global setting", errorList); + } + } + + return !errorOccurred; +} + +// Function to import instances +bool DBConfigManager::importInstances(const QJsonObject& config, QStringList& errorList) +{ + InstanceTable instanceTable; + const QJsonArray instancesConfig = config.value("instances").toArray(); + + bool errorOccurred = false; + quint8 instanceIdx = 0; + for (const auto& instanceItem : instancesConfig) + { + if (!importInstance(instanceTable, instanceItem.toObject(), instanceIdx, errorList)) + { + errorOccurred = true; + } + ++instanceIdx; + } + + return !errorOccurred; +} + +// Function to import a single instance +bool DBConfigManager::importInstance(InstanceTable& instanceTable, const QJsonObject& instanceConfig, quint8 instanceIdx, QStringList& errorList) +{ + QString instanceName = instanceConfig.value("name").toString(QString("Instance %1").arg(instanceIdx)); + bool isInstanceEnabled = instanceConfig.value("enabled").toBool(true); + + if (instanceIdx == 0) + { + isInstanceEnabled = true; // The first instance must be enabled. + } + + if (!instanceTable.createInstance(instanceName, instanceIdx) || + !instanceTable.setEnable(instanceIdx, isInstanceEnabled)) + { + logErrorAndAppend("Failed to import instance", errorList); + return false; + } + + SettingsTable settingsTableInstance(instanceIdx); + const QJsonObject instanceSettings = instanceConfig.value("settings").toObject(); + return importInstanceSettings(settingsTableInstance, instanceSettings, errorList); +} + +// Function to import instance settings +bool DBConfigManager::importInstanceSettings(SettingsTable& settingsTable, const QJsonObject& instanceSettings, QStringList& errorList) +{ + bool errorOccurred = false; + for (QJsonObject::const_iterator it = instanceSettings.constBegin(); it != instanceSettings.constEnd(); ++it) + { + if (!settingsTable.createSettingsRecord(it.key(), JsonUtils::jsonValueToQString(it.value()))) + { + errorOccurred = true; + logErrorAndAppend("Failed to import instance setting", errorList); + } + } + + return !errorOccurred; +} + +QJsonObject DBConfigManager::getConfiguration(const QList& instancesFilter, const QStringList& instanceFilteredTypes, const QStringList& globalFilterTypes ) const +{ + QSqlDatabase idb = getDB(); + + if (!startTransaction(idb)) + { + return {}; + } + + InstanceTable instanceTable; + SettingsTable settingsTable; + + QJsonObject config; + + QJsonObject globalConfig; + MetaTable metaTable; + globalConfig.insert("uuid", metaTable.getUUID()); + globalConfig.insert("settings", settingsTable.getSettings(globalFilterTypes)); + config.insert("global", globalConfig); + + QList instances {instancesFilter}; + if (instances.isEmpty()) + { + instances = instanceTable.getAllInstanceIDs(); + } + + QList sortedInstances = instances; + std::sort(sortedInstances.begin(), sortedInstances.end()); + + QJsonArray instanceIdList; + QJsonArray configInstanceList; + for (const quint8 instanceIdx : sortedInstances) + { + QJsonObject instanceConfig; + instanceConfig.insert("id",instanceIdx); + instanceConfig.insert("name", instanceTable.getNamebyIndex(instanceIdx)); + instanceConfig.insert("enabled", instanceTable.isEnabled(instanceIdx)); + instanceConfig.insert("settings", settingsTable.getSettings(static_cast(instanceIdx), instanceFilteredTypes)); + configInstanceList.append(instanceConfig); + + instanceIdList.append(instanceIdx); + } + + config.insert("instanceIds", instanceIdList); + config.insert("instances", configInstanceList); + + if (!commiTransaction(idb)) + { + return {}; + } + + return config; +} + +QPair DBConfigManager::migrateConfiguration() +{ + Info(_log, "Check, if configuration database is required to be migrated"); + + DBMigrationManager migtrationManger; + if (migtrationManger.isMigrationRequired()) + { + QJsonObject config = getConfiguration(); + + if (migtrationManger.migrateSettings(config)) + { + return updateConfiguration(config, true); + } + } + + Info(_log, "Database migration is not required"); + return qMakePair (true, QStringList{} ); +} + diff --git a/libsrc/db/DBManager.cpp b/libsrc/db/DBManager.cpp index f4494967a..6163e2f5e 100644 --- a/libsrc/db/DBManager.cpp +++ b/libsrc/db/DBManager.cpp @@ -1,38 +1,48 @@ +#include "utils/settings.h" #include #include #include #include #include -#include #include #include #include +#include #ifdef _WIN32 #include #endif -// not in header because of linking -static QString _rootPath; -static QThreadStorage _databasePool; +#define NO_SQLQUERY_LOGGING + +// Constants +namespace { + const char DATABASE_DIRECTORYNAME[] = "db"; + const char DATABASE_FILENAME[] = "hyperion.db"; + +} //End of constants + + +QDir DBManager::_dataDirectory; +QDir DBManager::_databaseDirectory; +QFileInfo DBManager::_databaseFile; +QThreadStorage DBManager::_databasePool; +bool DBManager::_isReadOnly {false}; DBManager::DBManager(QObject* parent) : QObject(parent) , _log(Logger::getInstance("DB")) - , _readonlyMode (false) -{ -} - -DBManager::~DBManager() { } -void DBManager::setRootPath(const QString& rootPath) +void DBManager::initializeDatabase(const QDir& dataDirectory, bool isReadOnly) { - _rootPath = rootPath; - // create directory - QDir().mkpath(_rootPath+"/db"); + _dataDirectory = dataDirectory; + _databaseDirectory.setPath(_dataDirectory.absoluteFilePath(DATABASE_DIRECTORYNAME)); + QDir().mkpath(_databaseDirectory.absolutePath()); + _databaseFile.setFile(_databaseDirectory,DATABASE_FILENAME); + _isReadOnly = isReadOnly; } void DBManager::setTable(const QString& table) @@ -43,38 +53,42 @@ void DBManager::setTable(const QString& table) QSqlDatabase DBManager::getDB() const { if(_databasePool.hasLocalData()) - return _databasePool.localData(); - else { - auto db = QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()); - _databasePool.setLocalData(db); - db.setDatabaseName(_rootPath+"/db/"+_dbn+".db"); - if(!db.open()) + return _databasePool.localData(); + } + auto database = QSqlDatabase::addDatabase("QSQLITE", QUuid::createUuid().toString()); + + if (isReadOnly()) + { + database.setConnectOptions("QSQLITE_OPEN_READONLY"); + } + +#ifdef SQLQUERY_LOGGING + Debug(Logger::getInstance("DB"), "Database is opened in %s mode", _isReadOnly ? "read-only" : "read/write"); +#endif + + _databasePool.setLocalData(database); + database.setDatabaseName(_databaseFile.absoluteFilePath()); + if(!database.open()) { - Error(_log, "%s", QSTRING_CSTR(db.lastError().text())); + Error(_log, "%s", QSTRING_CSTR(database.lastError().text())); throw std::runtime_error("Failed to open database connection!"); } - return db; - } + + return database; } bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& columns) const { - if ( _readonlyMode ) - { - return false; - } - if(recordExists(conditions)) { // if there is no column data, return if(columns.isEmpty()) + { return true; + } - if(!updateRecord(conditions, columns)) - return false; - - return true; + return updateRecord(conditions, columns); } QSqlDatabase idb = getDB(); @@ -84,14 +98,15 @@ bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& co QVariantList cValues; QStringList prep; QStringList placeh; + // prep merge columns & condition - QVariantMap::const_iterator i = columns.constBegin(); - while (i != columns.constEnd()) { - prep.append(i.key()); - cValues += i.value(); + QVariantMap::const_iterator columnIter = columns.constBegin(); + while (columnIter != columns.constEnd()) { + prep.append(columnIter.key()); + cValues += columnIter.value(); placeh.append("?"); - ++i; + ++columnIter; } for(const auto& pair : conditions) { @@ -101,21 +116,19 @@ bool DBManager::createRecord(const VectorPair& conditions, const QVariantMap& co cValues << pair.second; placeh.append("?"); } - query.prepare(QString("INSERT INTO %1 ( %2 ) VALUES ( %3 )").arg(_table,prep.join(", ")).arg(placeh.join(", "))); + query.prepare(QString("INSERT INTO %1 ( %2 ) VALUES ( %3 )").arg(_table,prep.join(", "), placeh.join(", "))); // add column & condition values - doAddBindValue(query, cValues); - if(!query.exec()) - { - Error(_log, "Failed to create record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prep.join(", ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + addBindValues(query, cValues); + + return executeQuery(query); } bool DBManager::recordExists(const VectorPair& conditions) const { if(conditions.isEmpty()) + { return false; + } QSqlDatabase idb = getDB(); QSqlQuery query(idb); @@ -127,33 +140,66 @@ bool DBManager::recordExists(const VectorPair& conditions) const for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; + prepCond << pair.first+"= ?"; bindVal << pair.second; } query.prepare(QString("SELECT * FROM %1 %2").arg(_table,prepCond.join(" "))); - doAddBindValue(query, bindVal); - if(!query.exec()) + addBindValues(query, bindVal); + + if (!executeQuery(query)) { - Error(_log, "Failed recordExists(): '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); return false; } int entry = 0; - while (query.next()) { + while (query.next()) + { entry++; } - if(entry) - return true; + return entry > 0; +} - return false; +bool DBManager::recordsNotExisting(const QVariantList& testValues,const QString& column, QStringList& nonExistingRecs, const QString& condition ) const +{ + QSqlDatabase idb = getDB(); + + QSqlQuery query(idb); + query.setForwardOnly(true); + + // prep conditions + QString prepCond; + if(!condition.isEmpty()) + { + prepCond = QString("WHERE %1").arg(condition); + } + + QVector valueItem(testValues.size(), "(?)"); + QString values = QStringList::fromVector(valueItem).join(","); + query.prepare( + QString("SELECT v.[column1] [%1] FROM ( VALUES %2 ) [v] WHERE %1 NOT IN ( SELECT %1 from settings %3 )") + .arg(column,values, prepCond) + ); + + addBindValues(query, testValues); + + if (!executeQuery(query)) + { + return false; + } + + while (query.next()) { + nonExistingRecs << query.value(0).toString(); + } + + return true; } bool DBManager::updateRecord(const VectorPair& conditions, const QVariantMap& columns) const { - if ( _readonlyMode ) + if (isReadOnly()) { - return false; + return true; } QSqlDatabase idb = getDB(); @@ -164,88 +210,75 @@ bool DBManager::updateRecord(const VectorPair& conditions, const QVariantMap& co QStringList prep; // prepare columns valus - QVariantMap::const_iterator i = columns.constBegin(); - while (i != columns.constEnd()) { - prep += i.key()+"=?"; - values += i.value(); + QVariantMap::const_iterator columnIter = columns.constBegin(); + while (columnIter != columns.constEnd()) { + prep += columnIter.key()+"= ?"; + values += columnIter.value(); - ++i; + ++columnIter; } // prepare condition values QStringList prepCond; QVariantList prepBindVal; - if(!conditions.isEmpty()) + if(!conditions.isEmpty()) { prepCond << "WHERE"; + } for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; + prepCond << pair.first+"= ?"; prepBindVal << pair.second; } - query.prepare(QString("UPDATE %1 SET %2 %3").arg(_table,prep.join(", ")).arg(prepCond.join(" "))); + query.prepare(QString("UPDATE %1 SET %2 %3").arg(_table,prep.join(", "), prepCond.join(" "))); // add column values - doAddBindValue(query, values); + addBindValues(query, values); // add condition values - doAddBindValue(query, prepBindVal); - if(!query.exec()) - { - Error(_log, "Failed to update record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + addBindValues(query, prepBindVal); + + return executeQuery(query); } bool DBManager::getRecord(const VectorPair& conditions, QVariantMap& results, const QStringList& tColumns, const QStringList& tOrder) const { - QSqlDatabase idb = getDB(); - QSqlQuery query(idb); - query.setForwardOnly(true); + QVector resultVector{}; + bool success = getRecords(conditions, resultVector, tColumns, tOrder); + if (success && !resultVector.isEmpty()) { + results = resultVector.first(); + } + return success; +} - QString sColumns("*"); - if(!tColumns.isEmpty()) - sColumns = tColumns.join(", "); +bool DBManager::getRecords(QVector& results, const QStringList& tColumns, const QStringList& tOrder) const +{ + return getRecords({}, results, tColumns, tOrder); +} - QString sOrder(""); - if(!tOrder.isEmpty()) - { - sOrder = " ORDER BY "; - sOrder.append(tOrder.join(", ")); - } +bool DBManager::getRecords(const VectorPair& conditions, QVector& results, const QStringList& tColumns, const QStringList& tOrder) const +{ // prep conditions - QStringList prepCond; - QVariantList bindVal; - if(!conditions.isEmpty()) - prepCond << " WHERE"; + QStringList conditionList; + QVariantList bindValues; for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; - bindVal << pair.second; - } - query.prepare(QString("SELECT %1 FROM %2%3%4").arg(sColumns,_table).arg(prepCond.join(" ")).arg(sOrder)); - doAddBindValue(query, bindVal); - - if(!query.exec()) - { - Error(_log, "Failed to get record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - - // go to first row - query.next(); - - QSqlRecord rec = query.record(); - for(int i = 0; i& results, const QStringList& tColumns, const QStringList& tOrder) const +bool DBManager::getRecords(const QString& condition, const QVariantList& bindValues, QVector& results, const QStringList& tColumns, const QStringList& tOrder) const { QSqlDatabase idb = getDB(); QSqlQuery query(idb); @@ -253,20 +286,28 @@ bool DBManager::getRecords(QVector& results, const QStringList& tCo QString sColumns("*"); if(!tColumns.isEmpty()) + { sColumns = tColumns.join(", "); + } QString sOrder(""); if(!tOrder.isEmpty()) { - sOrder = " ORDER BY "; + sOrder = "ORDER BY "; sOrder.append(tOrder.join(", ")); } - query.prepare(QString("SELECT %1 FROM %2%3").arg(sColumns,_table,sOrder)); + // prep conditions + QString prepCond; + if(!condition.isEmpty()) + { + prepCond = QString("WHERE %1").arg(condition); + } - if(!query.exec()) + query.prepare(QString("SELECT %1 FROM %2 %3 %4").arg(sColumns,_table, prepCond, sOrder)); + addBindValues(query, bindValues); + if (!executeQuery(query)) { - Error(_log, "Failed to get records: '%s' in table: '%s' Error: %s", QSTRING_CSTR(sColumns), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); return false; } @@ -285,12 +326,11 @@ bool DBManager::getRecords(QVector& results, const QStringList& tCo return true; } - bool DBManager::deleteRecord(const VectorPair& conditions) const { - if ( _readonlyMode ) + if (_isReadOnly) { - return false; + return true; } if(conditions.isEmpty()) @@ -310,29 +350,20 @@ bool DBManager::deleteRecord(const VectorPair& conditions) const for(const auto& pair : conditions) { - prepCond << pair.first+"=?"; + prepCond << pair.first+"= ?"; bindValues << pair.second; } query.prepare(QString("DELETE FROM %1 %2").arg(_table,prepCond.join(" "))); - doAddBindValue(query, bindValues); - if(!query.exec()) - { - Error(_log, "Failed to delete record: '%s' in table: '%s' Error: %s", QSTRING_CSTR(prepCond.join(" ")), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); - return false; - } - return true; + addBindValues(query, bindValues); + + return executeQuery(query); } return false; } bool DBManager::createTable(QStringList& columns) const { - if ( _readonlyMode ) - { - return false; - } - if(columns.isEmpty()) { Error(_log,"Empty tables aren't supported!"); @@ -347,9 +378,9 @@ bool DBManager::createTable(QStringList& columns) const // empty tables aren't supported by sqlite, add one column QString tcolumn = columns.takeFirst(); // default CURRENT_TIMESTAMP is not supported by ALTER TABLE - if(!query.exec(QString("CREATE TABLE %1 ( %2 )").arg(_table,tcolumn))) + query.prepare(QString("CREATE TABLE %1 ( %2 )").arg(_table,tcolumn)); + if (!executeQuery(query)) { - Error(_log, "Failed to create table: '%s' Error: %s", QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); return false; } } @@ -358,8 +389,8 @@ bool DBManager::createTable(QStringList& columns) const int err = 0; for(const auto& column : columns) { - QStringList id = column.split(' '); - if(rec.indexOf(id.at(0)) == -1) + QStringList columName = column.split(' '); + if(rec.indexOf(columName.at(0)) == -1) { if(!createColumn(column)) { @@ -367,79 +398,168 @@ bool DBManager::createTable(QStringList& columns) const } } } - if(err) + return err == 0; +} + +bool DBManager::createColumn(const QString& column) const +{ + QSqlDatabase idb = getDB(); + QSqlQuery query(idb); + + query.prepare(QString("ALTER TABLE %1 ADD COLUMN %2").arg(_table,column)); + return executeQuery(query); +} + +bool DBManager::tableExists(const QString& table) const +{ + QSqlDatabase idb = getDB(); + QStringList tables = idb.tables(); + return tables.contains(table); +} + +bool DBManager::deleteTable(const QString& table) const +{ + if(tableExists(table)) + { + QSqlDatabase idb = getDB(); + QSqlQuery query(idb); + + query.prepare(QString("DROP TABLE %1").arg(table)); + return executeQuery(query); + } + return true; +} + +void DBManager::addBindValues(QSqlQuery& query, const QVariantList& bindValues) const +{ + if (!bindValues.isEmpty()) + { + for(const auto& value : bindValues) + { + query.addBindValue(value); + } + } +} + +QString DBManager::constructExecutedQuery(const QSqlQuery& query) const +{ + QString executedQuery = query.executedQuery(); + + // Check if the query uses positional placeholders + if (executedQuery.contains('?')) { +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + QVariantList boundValues = query.boundValues(); // Get bound values as a list +#else + QVariantMap boundValues = query.boundValues(); // Get bound values as a list +#endif + // Iterate through the bound values and replace placeholders + for (const QVariant &value : boundValues) { + // Replace the first occurrence of '?' with the actual value + QString valueStr; + if (value.canConvert()) + { + valueStr = value.toString(); + } + else + { + valueStr = "Unkown"; + } + executedQuery.replace(executedQuery.indexOf('?'), 1, valueStr); + } + } + return executedQuery; +} + +bool DBManager::executeQuery(QSqlQuery& query) const +{ + if( !query.exec()) + { + QString finalQuery = constructExecutedQuery(query); + QString errorText = query.lastError().text(); + + Debug(_log, "Database Error: '%s', SqlQuery: '%s'", QSTRING_CSTR(errorText), QSTRING_CSTR(finalQuery)); + Error(_log, "Database Error: '%s'", QSTRING_CSTR(errorText)); + return false; + } + +#ifdef SQLQUERY_LOGGING + QString finalQuery = constructExecutedQuery(query); + Debug(_log, "SqlQuery executed: '%s'", QSTRING_CSTR(finalQuery)); +#endif return true; } -bool DBManager::createColumn(const QString& column) const +bool DBManager::startTransaction(QSqlDatabase& idb) const { - if ( _readonlyMode ) + if (!idb.transaction()) { + QString errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); return false; } + return true; +} - QSqlDatabase idb = getDB(); - QSqlQuery query(idb); - if(!query.exec(QString("ALTER TABLE %1 ADD COLUMN %2").arg(_table,column))) +bool DBManager::startTransaction(QSqlDatabase& idb, QStringList& errorList) +{ + if (!idb.transaction()) { - Error(_log, "Failed to create column: '%s' in table: '%s' Error: %s", QSTRING_CSTR(column), QSTRING_CSTR(_table), QSTRING_CSTR(idb.lastError().text())); + QString errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text()); + logErrorAndAppend(errorText, errorList); return false; } return true; } -bool DBManager::tableExists(const QString& table) const +bool DBManager::commiTransaction(QSqlDatabase& idb) const { - QSqlDatabase idb = getDB(); - QStringList tables = idb.tables(); - if(tables.contains(table)) - return true; - return false; + if (!idb.commit()) + { + QString errorText = QString("Could not finalize the database changes. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + return false; + } + return true; } -bool DBManager::deleteTable(const QString& table) const +bool DBManager::commiTransaction(QSqlDatabase& idb, QStringList& errorList) { - if ( _readonlyMode ) + if (!idb.commit()) { + QString errorText = QString("Could not finalize the database changes. Error: %1").arg(idb.lastError().text()); + logErrorAndAppend(errorText, errorList); return false; } + return true; +} - if(tableExists(table)) +bool DBManager::rollbackTransaction(QSqlDatabase& idb) const +{ + if (!idb.rollback()) { - QSqlDatabase idb = getDB(); - QSqlQuery query(idb); - if(!query.exec(QString("DROP TABLE %1").arg(table))) - { - Error(_log, "Failed to delete table: '%s' Error: %s", QSTRING_CSTR(table), QSTRING_CSTR(idb.lastError().text())); - return false; - } + QString errorText = QString("Could not rollback the database transaction. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + return false; } return true; } -void DBManager::doAddBindValue(QSqlQuery& query, const QVariantList& variants) const +bool DBManager::rollbackTransaction(QSqlDatabase& idb, QStringList& errorList) { - for(const auto& variant : variants) + if (!idb.rollback()) { - auto t = variant.userType(); - switch(t) - { - case QMetaType::UInt: - case QMetaType::Int: - case QMetaType::Bool: - query.addBindValue(variant.toInt()); - break; - case QMetaType::Double: - query.addBindValue(variant.toFloat()); - break; - case QMetaType::QByteArray: - query.addBindValue(variant.toByteArray()); - break; - default: - query.addBindValue(variant.toString()); - break; - } + QString errorText = QString("Could not rollback the database transaction. Error: %1").arg(idb.lastError().text()); + logErrorAndAppend(errorText, errorList); + return false; } + return true; +} + +// Function to log error and append it to the error list +void DBManager::logErrorAndAppend(const QString& errorText, QStringList& errorList) +{ + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); } diff --git a/libsrc/db/DBMigrationManager.cpp b/libsrc/db/DBMigrationManager.cpp new file mode 100644 index 000000000..c2c92e3ff --- /dev/null +++ b/libsrc/db/DBMigrationManager.cpp @@ -0,0 +1,767 @@ +#include + +#include "db/SettingsTable.h" +#include +#include + +#include + +#include + +DBMigrationManager::DBMigrationManager(QObject *parent) + : DBManager{parent} +{ +} + +bool DBMigrationManager::isMigrationRequired() +{ + bool isNewRelease = false; + + SettingsTable settingsTableGlobal; + + if (settingsTableGlobal.resolveConfigVersion()) + { + semver::version BUILD_VERSION(HYPERION_VERSION); + + if (!BUILD_VERSION.isValid()) + { + Error(_log, "Current Hyperion version [%s] is invalid. Exiting...", BUILD_VERSION.getVersion().c_str()); + exit(1); + } + + const semver::version& currentVersion = settingsTableGlobal.getConfigVersion(); + if (currentVersion > BUILD_VERSION) + { + Error(_log, "Database version [%s] is greater than current Hyperion version [%s]. Exiting...", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); + exit(1); + } + + if (currentVersion < BUILD_VERSION) + { + isNewRelease = true; + } + } + return isNewRelease; +} + +bool DBMigrationManager::migrateSettings(QJsonObject& config) +{ + bool migrated = false; + semver::version BUILD_VERSION(HYPERION_VERSION); + + SettingsTable settingsTableGlobal; + QJsonObject generalConfig = config.value("global").toObject().value("settings").toObject().value("general").toObject(); + + if (settingsTableGlobal.resolveConfigVersion(generalConfig)) + { + semver::version currentVersion = settingsTableGlobal.getConfigVersion(); + + if (currentVersion < BUILD_VERSION) + { + Info(_log, "Migration from current version [%s] to new version [%s] started", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); + + // Extract, modify, and reinsert the global settings + QJsonObject globalSettings = config.value("global").toObject().value("settings").toObject(); + upgradeGlobalSettings(currentVersion, globalSettings); + + QJsonObject globalConfig = config.value("global").toObject(); + globalConfig.insert("settings", globalSettings); + config.insert("global", globalConfig); + + // Update each instance directly within the config + QJsonArray instancesConfig = config.value("instances").toArray(); + for (int i = 0; i < instancesConfig.size(); ++i) + { + QJsonObject instanceConfig = instancesConfig[i].toObject(); + QJsonObject instanceSettings = instanceConfig.value("settings").toObject(); + + upgradeInstanceSettings(currentVersion, static_cast(i), instanceSettings); + + // Reinsert the modified instance settings back into the instanceConfig + instanceConfig.insert("settings", instanceSettings); + instancesConfig.replace(i, instanceConfig); + } + config.insert("instances", instancesConfig); + + Info(_log, "Migration from current version [%s] to new version [%s] finished", currentVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); + migrated = true; + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings(const semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + + semver::version migratedVersion = currentVersion; + + //Migration step for versions < alpha 9 + upgradeGlobalSettings_alpha_9(migratedVersion, config); + //Migration step for versions < 2.0.12 + upgradeGlobalSettings_2_0_12(migratedVersion, config); + //Migration step for versions < 2.0.16 + upgradeGlobalSettings_2_0_16(migratedVersion, config); + //Migration step for versions < 2.0.17 + upgradeGlobalSettings_2_1_0(migratedVersion, config); + + // Set the daqtabase version to the current build version + QJsonObject generalConfig = config["general"].toObject(); + // Update the configVersion if necessary + if (generalConfig["configVersion"].toString() != HYPERION_VERSION) { + generalConfig["configVersion"] = HYPERION_VERSION; + migrated = true; + } + // Re-insert the modified "general" object back into the config + config["general"] = generalConfig; + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings(const semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + semver::version migratedVersion = currentVersion; + + //Migration step for versions < alpha 9 + upgradeInstanceSettings_alpha_9(migratedVersion, instance, config); + //Migration step for versions < 2.0.12 + upgradeInstanceSettings_2_0_12(migratedVersion, instance, config); + //Migration step for versions < 2.0.13 + upgradeInstanceSettings_2_0_13(migratedVersion, instance, config); + //Migration step for versions < 2.0.16 + upgradeInstanceSettings_2_0_16(migratedVersion, instance, config); + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings_alpha_9(semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.0-alpha.9" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + if (config.contains("grabberV4L2")) + { + QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject(); + + if (newGrabberV4L2Config.contains("encoding_format")) + { + newGrabberV4L2Config.remove("encoding_format"); + newGrabberV4L2Config["grabberV4L2"] = newGrabberV4L2Config; + migrated = true; + } + + //Add new element enable + if (!newGrabberV4L2Config.contains("enable")) + { + newGrabberV4L2Config["enable"] = false; + migrated = true; + } + config["grabberV4L2"] = newGrabberV4L2Config; + Debug(_log, "GrabberV4L2 records migrated"); + } + + if (config.contains("grabberAudio")) + { + QJsonObject newGrabberAudioConfig = config["grabberAudio"].toObject(); + + //Add new element enable + if (!newGrabberAudioConfig.contains("enable")) + { + newGrabberAudioConfig["enable"] = false; + migrated = true; + } + config["grabberAudio"] = newGrabberAudioConfig; + Debug(_log, "GrabberAudio records migrated"); + } + + if (config.contains("framegrabber")) + { + QJsonObject newFramegrabberConfig = config["framegrabber"].toObject(); + + //Align element namings with grabberV4L2 + //Rename element type -> device + if (newFramegrabberConfig.contains("type")) + { + newFramegrabberConfig["device"] = newFramegrabberConfig["type"].toString(); + newFramegrabberConfig.remove("type"); + migrated = true; + } + //Rename element frequency_Hz -> fps + if (newFramegrabberConfig.contains("frequency_Hz")) + { + newFramegrabberConfig["fps"] = newFramegrabberConfig["frequency_Hz"].toInt(25); + newFramegrabberConfig.remove("frequency_Hz"); + migrated = true; + } + + //Rename element display -> input + if (newFramegrabberConfig.contains("display")) + { + newFramegrabberConfig["input"] = newFramegrabberConfig["display"]; + newFramegrabberConfig.remove("display"); + migrated = true; + } + + //Add new element enable + if (!newFramegrabberConfig.contains("enable")) + { + newFramegrabberConfig["enable"] = false; + migrated = true; + } + + config["framegrabber"] = newFramegrabberConfig; + Debug(_log, "Framegrabber records migrated"); + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings_2_0_12(semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.12" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + // Have Hostname/IP-address separate from port for Forwarder + if (config.contains("forwarder")) + { + QJsonObject newForwarderConfig = config["forwarder"].toObject(); + + QJsonArray json; + if (newForwarderConfig.contains("json")) + { + const QJsonArray oldJson = newForwarderConfig["json"].toArray(); + QJsonObject newJsonConfig; + + for (const QJsonValue& value : oldJson) + { + if (value.isString()) + { + QString oldHost = value.toString(); + // Resolve hostname and port + QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); + QString host = addressparts[0]; + + if (host != "127.0.0.1") + { + newJsonConfig["host"] = host; + + if (addressparts.size() > 1) + { + newJsonConfig["port"] = addressparts[1].toInt(); + } + else + { + newJsonConfig["port"] = 19444; + } + newJsonConfig["name"] = host; + + json.append(newJsonConfig); + migrated = true; + } + } + } + + if (!json.isEmpty()) + { + newForwarderConfig["jsonapi"] = json; + } + newForwarderConfig.remove("json"); + migrated = true; + } + + QJsonArray flatbuffer; + if (newForwarderConfig.contains("flat")) + { + const QJsonArray oldFlatbuffer = newForwarderConfig["flat"].toArray(); + QJsonObject newFlattbufferConfig; + + for (const QJsonValue& value : oldFlatbuffer) + { + if (value.isString()) + { + QString oldHost = value.toString(); + // Resolve hostname and port + QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); + QString host = addressparts[0]; + + if (host != "127.0.0.1") + { + newFlattbufferConfig["host"] = host; + + if (addressparts.size() > 1) + { + newFlattbufferConfig["port"] = addressparts[1].toInt(); + } + else + { + newFlattbufferConfig["port"] = 19400; + } + newFlattbufferConfig["name"] = host; + + flatbuffer.append(newFlattbufferConfig); + } + } + + if (!flatbuffer.isEmpty()) + { + newForwarderConfig["flatbuffer"] = flatbuffer; + } + newForwarderConfig.remove("flat"); + migrated = true; + } + } + + if (json.isEmpty() && flatbuffer.isEmpty()) + { + newForwarderConfig["enable"] = false; + } + + if (migrated) + { + config["forwarder"] = newForwarderConfig; + Debug(_log, "Forwarder records migrated"); + currentVersion = targetVersion; + } + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings_2_0_16(semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.16" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + if (config.contains("cecEvents")) + { + bool isCECEnabled {false}; + if (config.contains("grabberV4L2")) + { + QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject(); + if (newGrabberV4L2Config.contains("cecDetection")) + { + isCECEnabled = newGrabberV4L2Config.value("cecDetection").toBool(false); + newGrabberV4L2Config.remove("cecDetection"); + config["grabberV4L2"] = newGrabberV4L2Config; + + QJsonObject newGCecEventsConfig = config["cecEvents"].toObject(); + newGCecEventsConfig["enable"] = isCECEnabled; + if (!newGCecEventsConfig.contains("actions")) + { + QJsonObject action1 + { + {"action", "Suspend"}, + {"event", "standby"} + }; + QJsonObject action2 + { + {"action", "Resume"}, + {"event", "set stream path"} + }; + + QJsonArray actions { action1, action2 }; + newGCecEventsConfig.insert("actions",actions); + } + config["cecEvents"] = newGCecEventsConfig; + + migrated = true; + Debug(_log, "CEC configuration records migrated"); + } + } + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeGlobalSettings_2_1_0(semver::version& currentVersion, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.17-beta.2" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Global settings: Migrate from version [%s] to version [%s] or later", currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + if (config.contains("general")) + { + QJsonObject newGeneralConfig = config["general"].toObject(); + newGeneralConfig.remove("previousVersion"); + config.insert("general", newGeneralConfig); + + Debug(_log, "General settings migrated"); + migrated = true; + } + + if (config.contains("network")) + { + QJsonObject newNetworkConfig = config["network"].toObject(); + newNetworkConfig.remove("apiAuth"); + newNetworkConfig.remove("localAdminAuth"); + config.insert("network", newNetworkConfig); + + Debug(_log, "Network settings migrated"); + migrated = true; + } + + } + + //Remove wrong instance 255 configuration records, created by the global instance #255 + SettingsTable globalSettingsTable(255); + globalSettingsTable.deleteInstance(); + migrated = true; + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings_alpha_9(semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.0-alpha.9" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + // LED LAYOUT UPGRADE + // from { hscan: { minimum: 0.2, maximum: 0.3 }, vscan: { minimum: 0.2, maximum: 0.3 } } + // from { h: { min: 0.2, max: 0.3 }, v: { min: 0.2, max: 0.3 } } + // to { hmin: 0.2, hmax: 0.3, vmin: 0.2, vmax: 0.3} + if (config.contains("leds")) + { + const QJsonArray ledarr = config["leds"].toArray(); + const QJsonObject firstLed = ledarr[0].toObject(); + + if (firstLed.contains("hscan") || firstLed.contains("h")) + { + const bool whscan = firstLed.contains("hscan"); + QJsonArray newLedarr; + + for (const auto& entry : ledarr) + { + const QJsonObject led = entry.toObject(); + QJsonObject hscan; + QJsonObject vscan; + QJsonValue hmin; + QJsonValue hmax; + QJsonValue vmin; + QJsonValue vmax; + QJsonObject nL; + + if (whscan) + { + hscan = led["hscan"].toObject(); + vscan = led["vscan"].toObject(); + hmin = hscan["minimum"]; + hmax = hscan["maximum"]; + vmin = vscan["minimum"]; + vmax = vscan["maximum"]; + } + else + { + hscan = led["h"].toObject(); + vscan = led["v"].toObject(); + hmin = hscan["min"]; + hmax = hscan["max"]; + vmin = vscan["min"]; + vmax = vscan["max"]; + } + // append to led object + nL["hmin"] = hmin; + nL["hmax"] = hmax; + nL["vmin"] = vmin; + nL["vmax"] = vmax; + newLedarr.append(nL); + } + // replace + config["leds"] = newLedarr; + migrated = true; + Info(_log, "Instance [%u]: LED Layout migrated", instance); + } + } + + if (config.contains("ledConfig")) + { + QJsonObject oldLedConfig = config["ledConfig"].toObject(); + if (!oldLedConfig.contains("classic")) + { + QJsonObject newLedConfig; + newLedConfig.insert("classic", oldLedConfig); + QJsonObject defaultMatrixConfig{ {"ledshoriz", 1} + ,{"ledsvert", 1} + ,{"cabling","snake"} + ,{"start","top-left"} + }; + newLedConfig.insert("matrix", defaultMatrixConfig); + + config["ledConfig"] = newLedConfig; + migrated = true; + Info(_log, "Instance [%u]: LED-Config migrated", instance); + } + } + + // LED Hardware count is leading for versions after alpha 9 + // Setting Hardware LED count to number of LEDs configured via layout, if layout number is greater than number of hardware LEDs + if (config.contains("device")) + { + QJsonObject newDeviceConfig = config["device"].toObject(); + + if (newDeviceConfig.contains("hardwareLedCount")) + { + int hwLedcount = newDeviceConfig["hardwareLedCount"].toInt(); + if (config.contains("leds")) + { + const QJsonArray ledarr = config["leds"].toArray(); + int layoutLedCount = ledarr.size(); + + if (hwLedcount < layoutLedCount) + { + Warning(_log, "Instance [%u]: HwLedCount/Layout mismatch! Setting Hardware LED count to number of LEDs configured via layout", instance); + hwLedcount = layoutLedCount; + newDeviceConfig["hardwareLedCount"] = hwLedcount; + migrated = true; + } + } + } + + if (newDeviceConfig.contains("type")) + { + QString type = newDeviceConfig["type"].toString(); + if (type == "atmoorb" || type == "fadecandy" || type == "philipshue") + { + if (newDeviceConfig.contains("output")) + { + newDeviceConfig["host"] = newDeviceConfig["output"].toString(); + newDeviceConfig.remove("output"); + migrated = true; + } + } + } + + if (migrated) + { + config["device"] = newDeviceConfig; + Debug(_log, "LED-Device records migrated"); + } + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings_2_0_12(semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.12" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + // Have Hostname/IP-address separate from port for LED-Devices + if (config.contains("device")) + { + QJsonObject newDeviceConfig = config["device"].toObject(); + + if (newDeviceConfig.contains("host")) + { + QString oldHost = newDeviceConfig["host"].toString(); + + // Resolve hostname and port + QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); + + newDeviceConfig["host"] = addressparts[0]; + + if (addressparts.size() > 1) + { + if (!newDeviceConfig.contains("port")) + { + newDeviceConfig["port"] = addressparts[1].toInt(); + } + migrated = true; + } + } + + if (newDeviceConfig.contains("type")) + { + QString type = newDeviceConfig["type"].toString(); + if (type == "apa102") + { + if (newDeviceConfig.contains("colorOrder")) + { + QString colorOrder = newDeviceConfig["colorOrder"].toString(); + if (colorOrder == "bgr") + { + newDeviceConfig["colorOrder"] = "rgb"; + migrated = true; + } + } + } + } + + if (migrated) + { + config["device"] = newDeviceConfig; + Debug(_log, "LED-Device records migrated"); + } + } + + } + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings_2_0_13(semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.13" }; + + if (currentVersion < targetVersion) + { + Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + // Have Hostname/IP-address separate from port for LED-Devices + if (config.contains("device")) + { + QJsonObject newDeviceConfig = config["device"].toObject(); + + if (newDeviceConfig.contains("type")) + { + QString type = newDeviceConfig["type"].toString(); + + const QStringList serialDevices{ "adalight", "dmx", "atmo", "sedu", "tpm2", "karate" }; + if (serialDevices.contains(type)) + { + if (!newDeviceConfig.contains("rateList")) + { + newDeviceConfig["rateList"] = "CUSTOM"; + migrated = true; + } + } + + if (type == "adalight") + { + if (newDeviceConfig.contains("lightberry_apa102_mode")) + { + bool lightberry_apa102_mode = newDeviceConfig["lightberry_apa102_mode"].toBool(); + if (lightberry_apa102_mode) + { + newDeviceConfig["streamProtocol"] = "1"; + } + else + { + newDeviceConfig["streamProtocol"] = "0"; + } + newDeviceConfig.remove("lightberry_apa102_mode"); + migrated = true; + } + } + } + + if (migrated) + { + config["device"] = newDeviceConfig; + Debug(_log, "LED-Device records migrated"); + } + } + } + + return migrated; +} + +bool DBMigrationManager::upgradeInstanceSettings_2_0_16(semver::version& currentVersion, quint8 instance, QJsonObject& config) +{ + bool migrated = false; + const semver::version targetVersion{ "2.0.16" }; + + if (currentVersion >= targetVersion) return migrated; + + Info(_log, "Settings instance [%u]: Migrate from version [%s] to version [%s] or later", instance, currentVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); + currentVersion = targetVersion; + + if (config.contains("device")) + { + QJsonObject newDeviceConfig = config["device"].toObject(); + + auto convertIntToString = [&](const QString& key) { + if (newDeviceConfig.contains(key) && newDeviceConfig[key].isDouble()) { + int value = newDeviceConfig[key].toInt(); + newDeviceConfig[key] = QString::number(value); + migrated = true; + } + }; + + if (newDeviceConfig.contains("type")) + { + QString type = newDeviceConfig["type"].toString(); + + if (type == "philipshue") + { + convertIntToString("groupId"); + + if (newDeviceConfig.contains("lightIds")) + { + QJsonArray lightIds = newDeviceConfig["lightIds"].toArray(); + for (int i = 0; i < lightIds.size(); ++i) + { + if (lightIds[i].isDouble()) + { + lightIds[i] = QString::number(lightIds[i].toInt()); + migrated = true; + } + } + newDeviceConfig["lightIds"] = lightIds; + } + } + else if (type == "nanoleaf") + { + const auto updatePanelOrder = [&](const QString& key, const QString& zeroStr, const QString& oneStr) { + if (newDeviceConfig.contains(key)) + { + int order = newDeviceConfig[key].isDouble() ? newDeviceConfig[key].toInt() : newDeviceConfig[key].toString().toInt(); + newDeviceConfig[key] = (order == 0) ? zeroStr : oneStr; + migrated = true; + } + }; + + newDeviceConfig.remove("panelStartPos"); + migrated = true; + + updatePanelOrder("panelOrderTopDown", "top2down", "bottom2up"); + updatePanelOrder("panelOrderLeftRight", "left2right", "right2left"); + } + } + + if (migrated) + { + config["device"] = newDeviceConfig; + Debug(_log, "LED-Device records migrated"); + } + } + + return migrated; +} + diff --git a/libsrc/db/InstanceTable.cpp b/libsrc/db/InstanceTable.cpp new file mode 100644 index 000000000..702e124a0 --- /dev/null +++ b/libsrc/db/InstanceTable.cpp @@ -0,0 +1,142 @@ + +// db +#include +#include + +// qt +#include + +InstanceTable::InstanceTable(QObject* parent) + : DBManager(parent) +{ + // Init instance table + setTable("instances"); + createTable(QStringList()<<"instance INTEGER"<<"friendly_name TEXT"<<"enabled INTEGER DEFAULT 0"<<"last_use TEXT"); +} + +bool InstanceTable::createInstance(const QString& name, quint8& inst) +{ + // check duplicate + if(!recordExists({{"friendly_name", name}})) + { + QList instanceList = getAllInstanceIDs(false); + + inst = 0; + while (instanceList.contains(inst)) + { + ++inst; + } + + // create + QVariantMap data; + data["friendly_name"] = name; + data["instance"] = inst; + return createRecord({}, data); + } + + return false; +} + +bool InstanceTable::deleteInstance(quint8 inst) +{ + Debug(_log,""); + if(deleteRecord({{"instance",inst}})) + { + // delete settings entries + SettingsTable settingsTable(inst); + settingsTable.deleteInstance(); + return true; + } + return false; +} + +bool InstanceTable::saveName(quint8 inst, const QString& name) +{ + // check duplicate + if(!recordExists({{"friendly_name", name}})) + { + if(instanceExist(inst)) + { + return updateRecord({{"instance",inst}}, {{"friendly_name", name}}); + } + } + return false; +} + +QVector InstanceTable::getAllInstances(bool onlyEnabled) +{ + QVector results; + + VectorPair onlyEnabledCondition {}; + if (onlyEnabled) + { + onlyEnabledCondition = {{"enabled", true}}; + } + getRecords(onlyEnabledCondition, results, {}, {"instance ASC"}); + return results; +} + +QList InstanceTable::getAllInstanceIDs (bool onlyEnabled) +{ + QVector instanceList = getAllInstances(onlyEnabled); + QList instanceIds; + for (const QVariantMap &idx : std::as_const(instanceList)) + { + instanceIds.append(static_cast(idx.value("instance").toInt())); + } + + return instanceIds; +} + +bool InstanceTable::instanceExist(quint8 inst) +{ + return recordExists({{"instance",inst}}); +} + +QString InstanceTable::getNamebyIndex(quint8 index) +{ + QVariantMap results; + getRecord({{"instance", index}}, results, {"friendly_name"}); + + QString name = results["friendly_name"].toString(); + return name.isEmpty() ? "NOT FOUND" : name; +} + +bool InstanceTable::setLastUse(quint8 inst) +{ + return updateRecord({{"instance", inst}}, {{"last_use", QDateTime::currentDateTimeUtc().toString(Qt::ISODate)}}); +} + +bool InstanceTable::setEnable(quint8 inst, bool newState) +{ + return updateRecord({{"instance", inst}}, {{"enabled", newState}}); +} + +bool InstanceTable::isEnabled(quint8 inst) +{ + QVariantMap results; + getRecord({{"instance", inst}}, results); + + return results["enabled"].toBool(); +} + +void InstanceTable::createDefaultInstance() +{ + if(instanceExist(0)) + { + setEnable(0, true); + } + else + { + if(createRecord({{"instance", 0}}, {{"friendly_name", "First LED Hardware instance"}})) + { + setEnable(0, true); + } + else + { + throw std::runtime_error("Failed to create Hyperion root instance in db! This should never be the case..."); + } + } +} + + diff --git a/libsrc/db/MetaTable.cpp b/libsrc/db/MetaTable.cpp new file mode 100644 index 000000000..1ee723e6d --- /dev/null +++ b/libsrc/db/MetaTable.cpp @@ -0,0 +1,47 @@ + +#include + +// qt +#include +#include +#include +#include + +MetaTable::MetaTable(QObject* parent) + : DBManager(parent) +{ + setTable("meta"); + createTable(QStringList()<<"uuid TEXT"<<"created_at TEXT"); +}; + +QString MetaTable::getUUID() const +{ + QVector results; + getRecords(results, QStringList() << "uuid"); + + for(const auto & entry : std::as_const(results)) + { + if(!entry["uuid"].toString().isEmpty()) + { + return entry["uuid"].toString(); + } + } + + // create new uuidv5 based on net adapter MAC, save to db and return + QString hash; + foreach(QNetworkInterface interface, QNetworkInterface::allInterfaces()) + { + if (!(interface.flags() & QNetworkInterface::IsLoopBack)) + { + hash = QCryptographicHash::hash(interface.hardwareAddress().toLocal8Bit(),QCryptographicHash::Sha1).toHex(); + break; + } + } + const QString newUuid = QUuid::createUuidV5(QUuid(), hash).toString().mid(1, 36); + QVariantMap map; + map["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + createRecord({{"uuid",newUuid}}, map); + + return newUuid; +} diff --git a/libsrc/db/SettingsTable.cpp b/libsrc/db/SettingsTable.cpp new file mode 100644 index 000000000..573524da5 --- /dev/null +++ b/libsrc/db/SettingsTable.cpp @@ -0,0 +1,389 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace { +const char DEFAULT_INSTANCE_SETTINGS_SCHEMA_FILE[] = ":/schema-settings-instance.json"; +const char GLOBAL_SETTINGS_SCHEMA_FILE[] = ":/schema-settings-global.json"; +const char DEFAULT_SETTINGS_SCHEMA_FILE[] = ":/schema-settings-default.json"; +const char DEFAULT_SETTINGS_CONFIGURATION_FILE[] = ":/hyperion_default.settings"; +} + +QVector SettingsTable::globalSettingTypes; +bool SettingsTable::areGlobalSettingTypesInitialised = false; + +QVector SettingsTable::instanceSettingTypes; +bool SettingsTable::areInstanceSettingTypesInitialised = false; + +QJsonObject SettingsTable::defaultSettings; +bool SettingsTable::areDefaultSettingsInitialised = false; + + +SettingsTable::SettingsTable(quint8 instance, QObject* parent) + : DBManager(parent) + , _instance(instance) + , _configVersion(DEFAULT_CONFIG_VERSION) +{ + setTable("settings"); + // create table columns + createTable(QStringList()<<"type TEXT"<<"config TEXT"<<"hyperion_inst INTEGER"<<"updated_at TEXT"); +} + +const QVector& SettingsTable::getGlobalSettingTypes() const +{ + if (!areGlobalSettingTypesInitialised) { + globalSettingTypes = initializeGlobalSettingTypes(); + areGlobalSettingTypesInitialised = true; + } + return globalSettingTypes; +} + +QVector SettingsTable::initializeGlobalSettingTypes() const +{ + QJsonObject schemaJson; + try + { + schemaJson = QJsonFactory::readSchema(GLOBAL_SETTINGS_SCHEMA_FILE); + } + catch (const std::runtime_error& error) + { + throw std::runtime_error(error.what()); + } + + const QVector types = schemaJson.value("properties").toObject().keys().toVector(); + return types; +} + +bool SettingsTable::isGlobalSettingType(const QString& type) const { + return getGlobalSettingTypes().contains(type); +} + +bool SettingsTable::isInstanceSettingType(const QString& type) const { + return getInstanceSettingTypes().contains(type); +} + +const QVector& SettingsTable::getInstanceSettingTypes() const +{ + if (!areInstanceSettingTypesInitialised) { + instanceSettingTypes = initializeInstanceSettingTypes(); + areInstanceSettingTypesInitialised = true; + } + return instanceSettingTypes; +} + +QVector SettingsTable::initializeInstanceSettingTypes() const +{ + QJsonObject schemaJson; + try + { + schemaJson = QJsonFactory::readSchema(DEFAULT_INSTANCE_SETTINGS_SCHEMA_FILE); + } + catch (const std::runtime_error& error) + { + throw std::runtime_error(error.what()); + } + + const QVector types = schemaJson.value("properties").toObject().keys().toVector(); + return types; +} + +const QJsonObject& SettingsTable::getDefaultSettings() const +{ + if (!areDefaultSettingsInitialised) { + defaultSettings = initializeDefaultSettings(); + areDefaultSettingsInitialised = true; + } + return defaultSettings; +} + +QJsonObject SettingsTable::initializeDefaultSettings() const +{ + QJsonObject defaultConfig; + if ( QJsonFactory::load(DEFAULT_SETTINGS_SCHEMA_FILE, DEFAULT_SETTINGS_CONFIGURATION_FILE, defaultConfig) < 0) + { + Error(_log,"Failed to read default config"); + } + + return defaultConfig; +} + +bool SettingsTable::createSettingsRecord(const QString& type, const QString& config) const +{ + QVariantMap map; + map["config"] = config; + map["updated_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isGlobalSettingType(type)) + { + cond.append(CPair("AND hyperion_inst",_instance)); + } + return createRecord(cond, map); +} + +bool SettingsTable::recordExist(const QString& type) const +{ + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isGlobalSettingType(type)) + { + cond.append(CPair("AND hyperion_inst",_instance)); + } + return recordExists(cond); +} + +QJsonDocument SettingsTable::getSettingsRecord(const QString& type) const +{ + QVariantMap results; + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isGlobalSettingType(type)) + { + cond.append(CPair("AND hyperion_inst",_instance)); + } + getRecord(cond, results, QStringList("config")); + return QJsonDocument::fromJson(results["config"].toByteArray()); +} + +QString SettingsTable::getSettingsRecordString(const QString& type) const +{ + QVariantMap results; + VectorPair cond; + cond.append(CPair("type",type)); + // when a setting is not global we are searching also for the instance + if(!isGlobalSettingType(type)) + { + cond.append(CPair("AND hyperion_inst",_instance)); + } + getRecord(cond, results, QStringList("config")); + return results["config"].toString(); +} + +QJsonObject SettingsTable::getSettings(const QStringList& filteredTypes ) const +{ + return getSettings(_instance, filteredTypes); +} + +QJsonObject SettingsTable::getSettings(const QVariant& instance, const QStringList& filteredTypes ) const +{ + QJsonObject settingsObject; + QStringList settingsKeys({ "type", "config" }); + QString settingsCondition; + QVariantList conditionValues; + + if (instance.isNull() || instance == GLOABL_INSTANCE_ID ) + { + settingsCondition = "hyperion_inst IS NULL"; + } + else + { + settingsCondition = "hyperion_inst = ?"; + conditionValues.append(instance); + } + + if (!filteredTypes.isEmpty()) + { + QStringList seletedSettingTypes; + for (const auto &type : filteredTypes) { + seletedSettingTypes << QString("%1=?").arg("type"); + conditionValues.append(type); + } + settingsCondition += QString (" AND (%1)").arg(seletedSettingTypes.join(" OR ")); + } + + QVector settingsList; + if (getRecords(settingsCondition, conditionValues, settingsList, settingsKeys)) + { + for (const QVariantMap &setting : std::as_const(settingsList)) + { + QString type = setting.value("type").toString(); + QByteArray configObject = setting.value("config").toByteArray(); + QJsonDocument jsonDoc = QJsonDocument::fromJson(configObject); + + if (!jsonDoc.isNull()) + { + QJsonValue config; + + if (jsonDoc.isArray()) + { + config = jsonDoc.array(); + } + else if (jsonDoc.isObject()) + { + config = jsonDoc.object(); + } + settingsObject.insert(type, config); + } + } + } + return settingsObject; +} + +QStringList SettingsTable::nonExtingTypes() const +{ + QStringList testTypes; + QString condition {"hyperion_inst"}; + if(_instance == GLOABL_INSTANCE_ID) + { + condition += " IS NULL"; + testTypes = getGlobalSettingTypes().toList(); + } + else + { + condition += QString(" = %1").arg(_instance); + testTypes = getInstanceSettingTypes().toList(); + } + + QVariantList testTypesList; + testTypesList.reserve(testTypes.size()); + + for (const QString &str : std::as_const(testTypes)) { + testTypesList.append(QVariant(str)); + } + + QStringList nonExistingRecs; + recordsNotExisting(testTypesList, "type", nonExistingRecs, condition ); + + return nonExistingRecs; +} + +QPair SettingsTable::addMissingDefaults() +{ + QStringList errorList; + + QJsonObject defaultSettings; + if (_instance == GLOABL_INSTANCE_ID) + { + defaultSettings = getDefaultSettings().value("global").toObject(); + } + else + { + defaultSettings = getDefaultSettings().value("instance").toObject(); + } + + const QStringList missingTypes = nonExtingTypes(); + if (missingTypes.empty()) + { + Debug(_log, "Instance [%u]: No missing configuration items identified", _instance); + return qMakePair (true, errorList ); + } + + QSqlDatabase idb = getDB(); + if (!startTransaction(idb, errorList)) + { + return qMakePair(false, errorList); + } + + + bool errorOccured {false}; + + Info(_log, "Instance [%u]: Add default settings for %d missing configuration items",_instance, missingTypes.size()); + for (const auto &missingType: missingTypes) + { + if (!createSettingsRecord(missingType, JsonUtils::jsonValueToQString(defaultSettings.value(missingType)))) + { + errorOccured = true; + } + } + + if (errorOccured) + { + QString errorText = "Errors occured while adding missing settings to instance configuration items"; + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + + if (!idb.rollback()) + { + errorText = QString("Could not create a database transaction. Error: %1").arg(idb.lastError().text()); + Error(_log, "'%s'", QSTRING_CSTR(errorText)); + errorList.append(errorText); + } + } + + commiTransaction(idb, errorList); + + if(errorList.isEmpty()) + { + Debug(_log, "Instance [%u]: Successfully defaulted settings for %d missing configuration items", _instance, missingTypes.size()); + } + + return qMakePair (errorList.isEmpty(), errorList ); +} + +void SettingsTable::deleteInstance() const +{ + deleteRecord({{"hyperion_inst",_instance}}); +} + +QString SettingsTable::fixVersion(const QString& version) +{ + QString newVersion; + + // Use a static QRegularExpression to avoid re-creating it every time + static const QRegularExpression regEx( + "(\\d+\\.\\d+\\.\\d+-?[a-zA-Z-\\d]*\\.?[\\d]*)", + QRegularExpression::CaseInsensitiveOption | QRegularExpression::MultilineOption + ); + + // Try fixing version number, remove dot-separated pre-release identifiers not supported + QRegularExpressionMatch match = regEx.match(version); + + if (match.hasMatch()) + { + newVersion = match.captured(1); + } + + return newVersion; +} + +bool SettingsTable::resolveConfigVersion() +{ + QJsonObject generalConfig = getSettingsRecord({"general"}).object(); + return resolveConfigVersion(generalConfig); +} + +bool SettingsTable::resolveConfigVersion(QJsonObject generalConfig) +{ + bool isValid = false; + + QString configVersion = generalConfig["configVersion"].toString(); + if (!configVersion.isEmpty()) + { + isValid = _configVersion.setVersion(configVersion.toStdString()); + if (!isValid) + { + isValid = _configVersion.setVersion(fixVersion(configVersion).toStdString()); + if (isValid) + { + Info(_log, "Invalid config version [%s] fixed. Updated to [%s]", QSTRING_CSTR(configVersion), _configVersion.getVersion().c_str()); + } + } + } + else + { + isValid = true; + } + + return isValid; +} + +QString SettingsTable::getConfigVersionString() +{ + return _configVersion.getVersion().data(); +} + +semver::version SettingsTable::getConfigVersion() +{ + return _configVersion; +} diff --git a/libsrc/effectengine/EffectFileHandler.cpp b/libsrc/effectengine/EffectFileHandler.cpp index 734c4da21..5d441b7de 100644 --- a/libsrc/effectengine/EffectFileHandler.cpp +++ b/libsrc/effectengine/EffectFileHandler.cpp @@ -103,7 +103,7 @@ QString EffectFileHandler::saveEffect(const QJsonObject& message) if (it != effectsSchemas.end()) { - if (!JsonUtils::validate("EffectFileHandler", message["args"].toObject(), it->schemaFile, _log).first) + if (!JsonUtils::validate("EffectFileHandler", message["args"], it->schemaFile, _log).first) { return "Error during arg validation against schema, please consult the Hyperion Log"; } diff --git a/libsrc/hyperion/AuthManager.cpp b/libsrc/hyperion/AuthManager.cpp index 4e60807ef..a89ab8d60 100644 --- a/libsrc/hyperion/AuthManager.cpp +++ b/libsrc/hyperion/AuthManager.cpp @@ -7,14 +7,15 @@ // qt #include #include +#include +#include AuthManager *AuthManager::manager = nullptr; -AuthManager::AuthManager(QObject *parent, bool readonlyMode) +AuthManager::AuthManager(QObject *parent) : QObject(parent) - , _authTable(new AuthTable("", this, readonlyMode)) - , _metaTable(new MetaTable(this, readonlyMode)) - , _pendingRequests() + , _authTable(new AuthTable(this)) + , _metaTable(new MetaTable(this)) , _timer(new QTimer(this)) , _authBlockTimer(new QTimer(this)) { @@ -209,7 +210,7 @@ QVector AuthManager::getPendingRequests() const bool AuthManager::renameToken(const QString &id, const QString &comment) { - if (_authTable->idExist(id)) + if (_authTable->identifierExist(id)) { if (_authTable->renameToken(id, comment)) { @@ -222,7 +223,7 @@ bool AuthManager::renameToken(const QString &id, const QString &comment) bool AuthManager::deleteToken(const QString &id) { - if (_authTable->idExist(id)) + if (_authTable->identifierExist(id)) { if (_authTable->deleteToken(id)) { diff --git a/libsrc/hyperion/Hyperion.cpp b/libsrc/hyperion/Hyperion.cpp index de7195785..db9703141 100644 --- a/libsrc/hyperion/Hyperion.cpp +++ b/libsrc/hyperion/Hyperion.cpp @@ -6,6 +6,7 @@ #include #include #include +#include // hyperion include #include @@ -21,6 +22,7 @@ #include #include #include +#include // LedDevice includes #include @@ -47,10 +49,10 @@ #include #endif -Hyperion::Hyperion(quint8 instance, bool readonlyMode) +Hyperion::Hyperion(quint8 instance) : QObject() , _instIndex(instance) - , _settingsManager(new SettingsManager(instance, this, readonlyMode)) + , _settingsManager(new SettingsManager(instance, this)) , _componentRegister(nullptr) , _ledString(LedString::createLedString(getSetting(settings::LEDS).array(), hyperion::createColorOrder(getSetting(settings::DEVICE).object()))) , _imageProcessor(nullptr) @@ -73,7 +75,6 @@ Hyperion::Hyperion(quint8 instance, bool readonlyMode) #if defined(ENABLE_BOBLIGHT_SERVER) , _boblightServer(nullptr) #endif - , _readOnlyMode(readonlyMode) { qRegisterMetaType("ComponentList"); @@ -320,14 +321,17 @@ QJsonDocument Hyperion::getSetting(settings::type type) const return _settingsManager->getSetting(type); } -bool Hyperion::saveSettings(const QJsonObject& config, bool correct) +// TODO: Remove function, if UI is able to handle full configuration +QJsonObject Hyperion::getQJsonConfig() const { - return _settingsManager->saveSettings(config, correct); + const QJsonObject instanceConfig = _settingsManager->getSettings(); + const QJsonObject globalConfig = _settingsManager->getSettings({},QStringList()); + return JsonUtils::mergeJsonObjects(instanceConfig, globalConfig); } -bool Hyperion::restoreSettings(const QJsonObject& config, bool correct) +QPair Hyperion::saveSettings(const QJsonObject& config) { - return _settingsManager->restoreSettings(config, correct); + return _settingsManager->saveSettings(config); } int Hyperion::getLatchTime() const @@ -597,11 +601,6 @@ int Hyperion::setEffect(const QString &effectName, const QJsonObject &args, int } #endif -QJsonObject Hyperion::getQJsonConfig() const -{ - return _settingsManager->getSettings(); -} - void Hyperion::setLedMappingType(int mappingType) { if(mappingType != _imageProcessor->getUserLedMappingType()) diff --git a/libsrc/hyperion/HyperionIManager.cpp b/libsrc/hyperion/HyperionIManager.cpp index 268cbf752..254d1bf09 100644 --- a/libsrc/hyperion/HyperionIManager.cpp +++ b/libsrc/hyperion/HyperionIManager.cpp @@ -9,15 +9,14 @@ HyperionIManager* HyperionIManager::HIMinstance; -HyperionIManager::HyperionIManager(const QString& rootPath, QObject* parent, bool readonlyMode) +HyperionIManager::HyperionIManager(QObject* parent) : QObject(parent) , _log(Logger::getInstance("HYPERION-INSTMGR")) - , _instanceTable( new InstanceTable(rootPath, this, readonlyMode) ) - , _rootPath( rootPath ) - , _readonlyMode(readonlyMode) + , _instanceTable( new InstanceTable()) { HIMinstance = this; qRegisterMetaType("InstanceState"); + _instanceTable->createDefaultInstance(); } Hyperion* HyperionIManager::getHyperionInstance(quint8 instance) @@ -45,14 +44,32 @@ QVector HyperionIManager::getInstanceData() const return instances; } +QString HyperionIManager::getInstanceName(quint8 inst) +{ + return _instanceTable->getNamebyIndex(inst); +} + QList HyperionIManager::getRunningInstanceIdx() const { return _runningInstances.keys(); } +QList HyperionIManager::getInstanceIds() const +{ + return _instanceTable->getAllInstanceIDs(); +} + + void HyperionIManager::startAll() { - for(const auto & entry : _instanceTable->getAllInstances(true)) + const QVector instances = _instanceTable->getAllInstances(true); + if (instances.isEmpty()) + { + Error(_log, "No enabled instances found to be started"); + return; + } + + for(const auto & entry : instances) { startInstance(entry["instance"].toInt()); } @@ -62,7 +79,7 @@ void HyperionIManager::stopAll() { // copy the instances due to loop corruption, even with .erase() return next iter QMap instCopy = _runningInstances; - for(const auto instance : instCopy) + for(auto *const instance : instCopy) { instance->stop(); } @@ -131,7 +148,7 @@ bool HyperionIManager::startInstance(quint8 inst, bool block, QObject* caller, i { QThread* hyperionThread = new QThread(); hyperionThread->setObjectName("HyperionThread"); - Hyperion* hyperion = new Hyperion(inst, _readonlyMode); + Hyperion* hyperion = new Hyperion(inst); hyperion->moveToThread(hyperionThread); // setup thread management connect(hyperionThread, &QThread::started, hyperion, &Hyperion::start); @@ -156,7 +173,7 @@ bool HyperionIManager::startInstance(quint8 inst, bool block, QObject* caller, i if(block) { - while(!hyperionThread->isRunning()){}; + while(!hyperionThread->isRunning()){} } if (!_pendingRequests.contains(inst) && caller != nullptr) @@ -203,10 +220,10 @@ bool HyperionIManager::stopInstance(quint8 inst) bool HyperionIManager::createInstance(const QString& name, bool start) { - quint8 inst; + quint8 inst = 0; if(_instanceTable->createInstance(name, inst)) { - Info(_log,"New Hyperion instance created with name '%s'",QSTRING_CSTR(name)); + Info(_log,"New Hyperion instance [%d] created with name '%s'", inst, QSTRING_CSTR(name)); emit instanceStateChanged(InstanceState::H_CREATED, inst, name); emit change(); @@ -221,7 +238,9 @@ bool HyperionIManager::deleteInstance(quint8 inst) { // inst 0 can't be deleted if(!isInstAllowed(inst)) + { return false; + } // stop it if required as blocking and wait stopInstance(inst); diff --git a/libsrc/hyperion/SettingsManager.cpp b/libsrc/hyperion/SettingsManager.cpp index cd5e8727c..eb37308f6 100644 --- a/libsrc/hyperion/SettingsManager.cpp +++ b/libsrc/hyperion/SettingsManager.cpp @@ -1,939 +1,59 @@ // proj #include -// util -#include -#include - #include -#include "HyperionConfig.h" - -// json schema process -#include -#include - -// write config to filesystem #include +#include -#include +#include using namespace semver; -// Constants -namespace { - const char DEFAULT_VERSION[] = "2.0.0-alpha.8"; -} //End of constants - -QJsonObject SettingsManager::schemaJson; - -SettingsManager::SettingsManager(quint8 instance, QObject* parent, bool readonlyMode) +SettingsManager::SettingsManager(quint8 instance, QObject* parent) : QObject(parent) , _log(Logger::getInstance("SETTINGSMGR", "I" + QString::number(instance))) , _instance(instance) , _sTable(new SettingsTable(instance, this)) - , _configVersion(DEFAULT_VERSION) - , _previousVersion(DEFAULT_VERSION) - , _readonlyMode(readonlyMode) { - _sTable->setReadonlyMode(_readonlyMode); - // get schema - if (schemaJson.isEmpty()) - { - Q_INIT_RESOURCE(resource); - try - { - schemaJson = QJsonFactory::readSchema(":/hyperion-schema"); - } - catch (const std::runtime_error& error) - { - throw std::runtime_error(error.what()); - } - } - - // get default config - QJsonObject defaultConfig; - if (!JsonUtils::readFile(":/hyperion_default.config", defaultConfig, _log).first) - { - throw std::runtime_error("Failed to read default config"); - } - - // transform json to string lists - const QStringList keyList = defaultConfig.keys(); - QStringList defValueList; - for (const auto& key : keyList) - { - if (defaultConfig[key].isObject()) - { - defValueList << QString(QJsonDocument(defaultConfig[key].toObject()).toJson(QJsonDocument::Compact)); - } - else if (defaultConfig[key].isArray()) - { - defValueList << QString(QJsonDocument(defaultConfig[key].toArray()).toJson(QJsonDocument::Compact)); - } - } - - // fill database with default data if required - for (const auto& key : keyList) - { - QString val = defValueList.takeFirst(); - // prevent overwrite - if (!_sTable->recordExist(key)) - { - _sTable->createSettingsRecord(key, val); - } - } - - // need to validate all data in database construct the entire data object - // TODO refactor schemaChecker to accept QJsonArray in validate(); QJsonDocument container? To validate them per entry... - QJsonObject dbConfig; - for (const auto& key : keyList) - { - QJsonDocument doc = _sTable->getSettingsRecord(key); - if (doc.isArray()) - { - dbConfig[key] = doc.array(); - } - else - { - dbConfig[key] = doc.object(); - } - } - - //Check, if database requires migration - bool isNewRelease = false; - // Use instance independent SettingsManager to track migration status - if (_instance == GLOABL_INSTANCE_ID) - { - if (resolveConfigVersion(dbConfig)) - { - QJsonObject newGeneralConfig = dbConfig["general"].toObject(); - - semver::version BUILD_VERSION(HYPERION_VERSION); - - if (!BUILD_VERSION.isValid()) - { - Error(_log, "Current Hyperion version [%s] is invalid. Exiting...", BUILD_VERSION.getVersion().c_str()); - exit(1); - } - - if (_configVersion > BUILD_VERSION) - { - Error(_log, "Database version [%s] is greater than current Hyperion version [%s]", _configVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); - // TODO: Remove version checking and Settingsmanager from components' constructor to be able to stop hyperion. - } - else - { - if (_previousVersion < BUILD_VERSION) - { - if (_configVersion == BUILD_VERSION) - { - newGeneralConfig["previousVersion"] = BUILD_VERSION.getVersion().c_str(); - dbConfig["general"] = newGeneralConfig; - isNewRelease = true; - Info(_log, "Migration completed to version [%s]", BUILD_VERSION.getVersion().c_str()); - } - else - { - Info(_log, "Migration from current version [%s] to new version [%s] started", _previousVersion.getVersion().c_str(), BUILD_VERSION.getVersion().c_str()); - - newGeneralConfig["previousVersion"] = _configVersion.getVersion().c_str(); - newGeneralConfig["configVersion"] = BUILD_VERSION.getVersion().c_str(); - dbConfig["general"] = newGeneralConfig; - isNewRelease = true; - } - } - } - } - } - - // possible data upgrade steps to prevent data loss - bool migrated = handleConfigUpgrade(dbConfig); - if (isNewRelease || migrated) - { - saveSettings(dbConfig, true); - } - - // validate full dbconfig against schema, on error we need to rewrite entire table - QJsonSchemaChecker schemaChecker; - schemaChecker.setSchema(schemaJson); - QPair valid = schemaChecker.validate(dbConfig); - // check if our main schema syntax is IO - if (!valid.second) - { - for (auto& schemaError : schemaChecker.getMessages()) - { - Error(_log, "Schema Syntax Error: %s", QSTRING_CSTR(schemaError)); - } - throw std::runtime_error("The config schema has invalid syntax. This should never happen! Go fix it!"); - } - if (!valid.first) - { - Info(_log, "Table upgrade required..."); - dbConfig = schemaChecker.getAutoCorrectedConfig(dbConfig); - - for (auto& schemaError : schemaChecker.getMessages()) - { - Warning(_log, "Config Fix: %s", QSTRING_CSTR(schemaError)); - } - - saveSettings(dbConfig, true); - } - else - { - _qconfig = dbConfig; - } - - Debug(_log, "Settings database initialized"); + _sTable->addMissingDefaults(); } QJsonDocument SettingsManager::getSetting(settings::type type) const { - return _sTable->getSettingsRecord(settings::typeToString(type)); + return getSetting(settings::typeToString(type)); } -QJsonObject SettingsManager::getSettings() const +QJsonDocument SettingsManager::getSetting(const QString& type) const { - QJsonObject config; - for (const auto& key : _qconfig.keys()) - { - //Read all records from database to ensure that global settings are read across instances - QJsonDocument doc = _sTable->getSettingsRecord(key); - if (doc.isArray()) - { - config.insert(key, doc.array()); - } - else - { - config.insert(key, doc.object()); - } - } - return config; + return _sTable->getSettingsRecord(type); } -bool SettingsManager::restoreSettings(QJsonObject config, bool correct) +QJsonObject SettingsManager::getSettings(const QStringList& filteredTypes ) const { - // optional data upgrades e.g. imported legacy/older configs - handleConfigUpgrade(config); - return saveSettings(config, correct); + return _sTable->getSettings(filteredTypes); } -bool SettingsManager::saveSettings(QJsonObject config, bool correct) + QJsonObject SettingsManager::getSettings(const QVariant& instance, const QStringList& filteredTypes ) const { - // we need to validate data against schema - QJsonSchemaChecker schemaChecker; - schemaChecker.setSchema(schemaJson); - if (!schemaChecker.validate(config).first) - { - if (!correct) - { - Error(_log, "Failed to save configuration, errors during validation"); - return false; - } - Warning(_log, "Fixing json data!"); - config = schemaChecker.getAutoCorrectedConfig(config); - - for (const auto& schemaError : schemaChecker.getMessages()) - { - Warning(_log, "Config Fix: %s", QSTRING_CSTR(schemaError)); - } - } - - // store the new config - _qconfig = config; - - // extract keys and data - const QStringList keyList = config.keys(); - QStringList newValueList; - for (const auto& key : keyList) - { - if (config[key].isObject()) - { - newValueList << QString(QJsonDocument(config[key].toObject()).toJson(QJsonDocument::Compact)); - } - else if (config[key].isArray()) - { - newValueList << QString(QJsonDocument(config[key].toArray()).toJson(QJsonDocument::Compact)); - } - } - - bool rc = true; - // compare database data with new data to emit/save changes accordingly - for (const auto& key : keyList) - { - QString data = newValueList.takeFirst(); - if (_sTable->getSettingsRecordString(key) != data) - { - if (!_sTable->createSettingsRecord(key, data)) - { - rc = false; - } - else - { - QJsonParseError error; - QJsonDocument jsonDocument = QJsonDocument::fromJson(data.toUtf8(), &error); - if (error.error != QJsonParseError::NoError) { - Error(_log, "Error parsing JSON: %s", QSTRING_CSTR(error.errorString())); - rc = false; - } - else { - emit settingsChanged(settings::stringToType(key), jsonDocument); - } - } - } - } - return rc; + return _sTable->getSettings(instance, filteredTypes); } -inline QString fixVersion(const QString& version) +QPair SettingsManager::saveSettings(const QJsonObject& config) { - QString newVersion; - //Try fixing version number, remove dot separated pre-release identifiers not supported - QRegularExpression regEx("(\\d+\\.\\d+\\.\\d+-?[a-zA-Z-\\d]*\\.?[\\d]*)", QRegularExpression::CaseInsensitiveOption | QRegularExpression::MultilineOption); - QRegularExpressionMatch match; - - match = regEx.match(version); - if (match.hasMatch()) + QStringList errorList; + for (auto &key : config.keys()) { - newVersion = match.captured(1); - } - return newVersion; -} - -bool SettingsManager::resolveConfigVersion(QJsonObject& config) -{ - bool isValid = false; - if (config.contains("general")) - { - QJsonObject generalConfig = config["general"].toObject(); - QString configVersion = generalConfig["configVersion"].toString(); - QString previousVersion = generalConfig["previousVersion"].toString(); - - if (!configVersion.isEmpty()) - { - isValid = _configVersion.setVersion(configVersion.toStdString()); - if (!isValid) - { - isValid = _configVersion.setVersion(fixVersion(configVersion).toStdString()); - if (isValid) - { - Info(_log, "Invalid config version [%s] fixed. Updated to [%s]", QSTRING_CSTR(configVersion), _configVersion.getVersion().c_str()); - } - } - } - else - { - isValid = true; - } - - if (!previousVersion.isEmpty() && isValid) - { - isValid = _previousVersion.setVersion(previousVersion.toStdString()); - if (!isValid) - { - isValid = _previousVersion.setVersion(fixVersion(previousVersion).toStdString()); - if (isValid) - { - Info(_log, "Invalid previous version [%s] fixed. Updated to [%s]", QSTRING_CSTR(previousVersion), _previousVersion.getVersion().c_str()); - } - } - } - else - { - _previousVersion.setVersion(DEFAULT_VERSION); - isValid = true; - } - } - return isValid; -} - -bool SettingsManager::handleConfigUpgrade(QJsonObject& config) -{ - bool migrated = false; - - //Only migrate, if valid versions are available - if (!resolveConfigVersion(config)) - { - Warning(_log, "Invalid version information found in configuration. No database migration executed."); - } - else - { - //Do only migrate, if configuration is not up to date - if (_previousVersion < _configVersion) + const QJsonValue configItem = config.value(key); + const QString data = JsonUtils::jsonValueToQString(configItem); + if (_sTable->getSettingsRecordString(key) != data) { - //Migration steps for versions <= alpha 9 - semver::version targetVersion{ "2.0.0-alpha.9" }; - if (_previousVersion <= targetVersion) - { - Info(_log, "Instance [%u]: Migrate from version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); - - // LED LAYOUT UPGRADE - // from { hscan: { minimum: 0.2, maximum: 0.3 }, vscan: { minimum: 0.2, maximum: 0.3 } } - // from { h: { min: 0.2, max: 0.3 }, v: { min: 0.2, max: 0.3 } } - // to { hmin: 0.2, hmax: 0.3, vmin: 0.2, vmax: 0.3} - if (config.contains("leds")) - { - const QJsonArray ledarr = config["leds"].toArray(); - const QJsonObject led = ledarr[0].toObject(); - - if (led.contains("hscan") || led.contains("h")) - { - const bool whscan = led.contains("hscan"); - QJsonArray newLedarr; - - for (const auto& entry : ledarr) - { - const QJsonObject led = entry.toObject(); - QJsonObject hscan; - QJsonObject vscan; - QJsonValue hmin; - QJsonValue hmax; - QJsonValue vmin; - QJsonValue vmax; - QJsonObject nL; - - if (whscan) - { - hscan = led["hscan"].toObject(); - vscan = led["vscan"].toObject(); - hmin = hscan["minimum"]; - hmax = hscan["maximum"]; - vmin = vscan["minimum"]; - vmax = vscan["maximum"]; - } - else - { - hscan = led["h"].toObject(); - vscan = led["v"].toObject(); - hmin = hscan["min"]; - hmax = hscan["max"]; - vmin = vscan["min"]; - vmax = vscan["max"]; - } - // append to led object - nL["hmin"] = hmin; - nL["hmax"] = hmax; - nL["vmin"] = vmin; - nL["vmax"] = vmax; - newLedarr.append(nL); - } - // replace - config["leds"] = newLedarr; - migrated = true; - Info(_log, "Instance [%u]: LED Layout migrated", _instance); - } - } - - if (config.contains("ledConfig")) - { - QJsonObject oldLedConfig = config["ledConfig"].toObject(); - if (!oldLedConfig.contains("classic")) - { - QJsonObject newLedConfig; - newLedConfig.insert("classic", oldLedConfig); - QJsonObject defaultMatrixConfig{ {"ledshoriz", 1} - ,{"ledsvert", 1} - ,{"cabling","snake"} - ,{"start","top-left"} - }; - newLedConfig.insert("matrix", defaultMatrixConfig); - - config["ledConfig"] = newLedConfig; - migrated = true; - Info(_log, "Instance [%u]: LED-Config migrated", _instance); - } - } - - // LED Hardware count is leading for versions after alpha 9 - // Setting Hardware LED count to number of LEDs configured via layout, if layout number is greater than number of hardware LEDs - if (config.contains("device")) - { - QJsonObject newDeviceConfig = config["device"].toObject(); - - if (newDeviceConfig.contains("hardwareLedCount")) - { - int hwLedcount = newDeviceConfig["hardwareLedCount"].toInt(); - if (config.contains("leds")) - { - const QJsonArray ledarr = config["leds"].toArray(); - int layoutLedCount = ledarr.size(); - - if (hwLedcount < layoutLedCount) - { - Warning(_log, "Instance [%u]: HwLedCount/Layout mismatch! Setting Hardware LED count to number of LEDs configured via layout", _instance); - hwLedcount = layoutLedCount; - newDeviceConfig["hardwareLedCount"] = hwLedcount; - migrated = true; - } - } - } - - if (newDeviceConfig.contains("type")) - { - QString type = newDeviceConfig["type"].toString(); - if (type == "atmoorb" || type == "fadecandy" || type == "philipshue") - { - if (newDeviceConfig.contains("output")) - { - newDeviceConfig["host"] = newDeviceConfig["output"].toString(); - newDeviceConfig.remove("output"); - migrated = true; - } - } - } - - if (migrated) - { - config["device"] = newDeviceConfig; - Debug(_log, "LED-Device records migrated"); - } - } - - if (config.contains("grabberV4L2")) - { - QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject(); - - if (newGrabberV4L2Config.contains("encoding_format")) - { - newGrabberV4L2Config.remove("encoding_format"); - newGrabberV4L2Config["grabberV4L2"] = newGrabberV4L2Config; - migrated = true; - } - - //Add new element enable - if (!newGrabberV4L2Config.contains("enable")) - { - newGrabberV4L2Config["enable"] = false; - migrated = true; - } - config["grabberV4L2"] = newGrabberV4L2Config; - Debug(_log, "GrabberV4L2 records migrated"); - } - - if (config.contains("grabberAudio")) - { - QJsonObject newGrabberAudioConfig = config["grabberAudio"].toObject(); - - //Add new element enable - if (!newGrabberAudioConfig.contains("enable")) - { - newGrabberAudioConfig["enable"] = false; - migrated = true; - } - config["grabberAudio"] = newGrabberAudioConfig; - Debug(_log, "GrabberAudio records migrated"); - } - - if (config.contains("framegrabber")) - { - QJsonObject newFramegrabberConfig = config["framegrabber"].toObject(); - - //Align element namings with grabberV4L2 - //Rename element type -> device - if (newFramegrabberConfig.contains("type")) - { - newFramegrabberConfig["device"] = newFramegrabberConfig["type"].toString(); - newFramegrabberConfig.remove("type"); - migrated = true; - } - //Rename element frequency_Hz -> fps - if (newFramegrabberConfig.contains("frequency_Hz")) - { - newFramegrabberConfig["fps"] = newFramegrabberConfig["frequency_Hz"].toInt(25); - newFramegrabberConfig.remove("frequency_Hz"); - migrated = true; - } - - //Rename element display -> input - if (newFramegrabberConfig.contains("display")) - { - newFramegrabberConfig["input"] = newFramegrabberConfig["display"]; - newFramegrabberConfig.remove("display"); - migrated = true; - } - - //Add new element enable - if (!newFramegrabberConfig.contains("enable")) - { - newFramegrabberConfig["enable"] = false; - migrated = true; - } - - config["framegrabber"] = newFramegrabberConfig; - Debug(_log, "Framegrabber records migrated"); - } - } - - //Migration steps for versions <= 2.0.12 - _previousVersion = targetVersion; - targetVersion.setVersion("2.0.12"); - if (_previousVersion <= targetVersion) - { - Info(_log, "Instance [%u]: Migrate from version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); - - // Have Hostname/IP-address separate from port for LED-Devices - if (config.contains("device")) - { - QJsonObject newDeviceConfig = config["device"].toObject(); - - if (newDeviceConfig.contains("host")) - { - QString oldHost = newDeviceConfig["host"].toString(); - - // Resolve hostname and port - QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); - - newDeviceConfig["host"] = addressparts[0]; - - if (addressparts.size() > 1) - { - if (!newDeviceConfig.contains("port")) - { - newDeviceConfig["port"] = addressparts[1].toInt(); - } - migrated = true; - } - } - - if (newDeviceConfig.contains("type")) - { - QString type = newDeviceConfig["type"].toString(); - if (type == "apa102") - { - if (newDeviceConfig.contains("colorOrder")) - { - QString colorOrder = newDeviceConfig["colorOrder"].toString(); - if (colorOrder == "bgr") - { - newDeviceConfig["colorOrder"] = "rgb"; - migrated = true; - } - } - } - } - - if (migrated) - { - config["device"] = newDeviceConfig; - Debug(_log, "LED-Device records migrated"); - } - } - - // Have Hostname/IP-address separate from port for Forwarder - if (config.contains("forwarder")) - { - QJsonObject newForwarderConfig = config["forwarder"].toObject(); - - QJsonArray json; - if (newForwarderConfig.contains("json")) - { - const QJsonArray oldJson = newForwarderConfig["json"].toArray(); - QJsonObject newJsonConfig; - - for (const QJsonValue& value : oldJson) - { - if (value.isString()) - { - QString oldHost = value.toString(); - // Resolve hostname and port - QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); - QString host = addressparts[0]; - - if (host != "127.0.0.1") - { - newJsonConfig["host"] = host; - - if (addressparts.size() > 1) - { - newJsonConfig["port"] = addressparts[1].toInt(); - } - else - { - newJsonConfig["port"] = 19444; - } - newJsonConfig["name"] = host; - - json.append(newJsonConfig); - migrated = true; - } - } - } - - if (!json.isEmpty()) - { - newForwarderConfig["jsonapi"] = json; - } - newForwarderConfig.remove("json"); - migrated = true; - } - - QJsonArray flatbuffer; - if (newForwarderConfig.contains("flat")) - { - const QJsonArray oldFlatbuffer = newForwarderConfig["flat"].toArray(); - QJsonObject newFlattbufferConfig; - - for (const QJsonValue& value : oldFlatbuffer) - { - if (value.isString()) - { - QString oldHost = value.toString(); - // Resolve hostname and port - QStringList addressparts = QStringUtils::split(oldHost, ":", QStringUtils::SplitBehavior::SkipEmptyParts); - QString host = addressparts[0]; - - if (host != "127.0.0.1") - { - newFlattbufferConfig["host"] = host; - - if (addressparts.size() > 1) - { - newFlattbufferConfig["port"] = addressparts[1].toInt(); - } - else - { - newFlattbufferConfig["port"] = 19400; - } - newFlattbufferConfig["name"] = host; - - flatbuffer.append(newFlattbufferConfig); - migrated = true; - } - } - - if (!flatbuffer.isEmpty()) - { - newForwarderConfig["flatbuffer"] = flatbuffer; - } - newForwarderConfig.remove("flat"); - migrated = true; - } - } - - if (json.isEmpty() && flatbuffer.isEmpty()) - { - newForwarderConfig["enable"] = false; - } - - if (migrated) - { - config["forwarder"] = newForwarderConfig; - Debug(_log, "Forwarder records migrated"); - } - } - } - - //Migration steps for versions <= 2.0.13 - _previousVersion = targetVersion; - targetVersion.setVersion("2.0.13"); - if (_previousVersion <= targetVersion) - { - Info(_log, "Instance [%u]: Migrate from version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); - - - // Have Hostname/IP-address separate from port for LED-Devices - if (config.contains("device")) - { - QJsonObject newDeviceConfig = config["device"].toObject(); - - if (newDeviceConfig.contains("type")) - { - QString type = newDeviceConfig["type"].toString(); - - const QStringList serialDevices{ "adalight", "dmx", "atmo", "sedu", "tpm2", "karate" }; - if (serialDevices.contains(type)) - { - if (!newDeviceConfig.contains("rateList")) - { - newDeviceConfig["rateList"] = "CUSTOM"; - migrated = true; - } - } - - if (type == "adalight") - { - if (newDeviceConfig.contains("lightberry_apa102_mode")) - { - bool lightberry_apa102_mode = newDeviceConfig["lightberry_apa102_mode"].toBool(); - if (lightberry_apa102_mode) - { - newDeviceConfig["streamProtocol"] = "1"; - } - else - { - newDeviceConfig["streamProtocol"] = "0"; - } - newDeviceConfig.remove("lightberry_apa102_mode"); - migrated = true; - } - } - } - - if (migrated) - { - config["device"] = newDeviceConfig; - Debug(_log, "LED-Device records migrated"); - } - } - } - - //Migration steps for versions <= 2.0.16 - _previousVersion = targetVersion; - targetVersion.setVersion("2.0.16"); - if (_previousVersion <= targetVersion) + if (!_sTable->createSettingsRecord(key, data)) { - Info(_log, "Instance [%u]: Migrate from version [%s] to version [%s] or later", _instance, _previousVersion.getVersion().c_str(), targetVersion.getVersion().c_str()); - - // Have Hostname/IP-address separate from port for LED-Devices - if (config.contains("device")) - { - QJsonObject newDeviceConfig = config["device"].toObject(); - - if (newDeviceConfig.contains("type")) - { - QString type = newDeviceConfig["type"].toString(); - - if (type == "philipshue") - { - if (newDeviceConfig.contains("groupId")) - { - if (newDeviceConfig["groupId"].isDouble()) - { - int groupID = newDeviceConfig["groupId"].toInt(); - newDeviceConfig["groupId"] = QString::number(groupID); - migrated = true; - } - } - - if (newDeviceConfig.contains("lightIds")) - { - QJsonArray lightIds = newDeviceConfig.value("lightIds").toArray(); - // Iterate through the JSON array and update integer values to strings - for (int i = 0; i < lightIds.size(); ++i) { - QJsonValue value = lightIds.at(i); - if (value.isDouble()) - { - int lightId = value.toInt(); - lightIds.replace(i, QString::number(lightId)); - migrated = true; - } - } - newDeviceConfig["lightIds"] = lightIds; - - } - } - - if (type == "nanoleaf") - { - if (newDeviceConfig.contains("panelStartPos")) - { - newDeviceConfig.remove("panelStartPos"); - migrated = true; - } - - if (newDeviceConfig.contains("panelOrderTopDown")) - { - int panelOrderTopDown; - if (newDeviceConfig["panelOrderTopDown"].isDouble()) - { - panelOrderTopDown = newDeviceConfig["panelOrderTopDown"].toInt(); - } - else - { - panelOrderTopDown = newDeviceConfig["panelOrderTopDown"].toString().toInt(); - } - - newDeviceConfig.remove("panelOrderTopDown"); - if (panelOrderTopDown == 0) - { - newDeviceConfig["panelOrderTopDown"] = "top2down"; - migrated = true; - } - else - { - if (panelOrderTopDown == 1) - { - newDeviceConfig["panelOrderTopDown"] = "bottom2up"; - migrated = true; - } - } - } - - if (newDeviceConfig.contains("panelOrderLeftRight")) - { - int panelOrderLeftRight; - if (newDeviceConfig["panelOrderLeftRight"].isDouble()) - { - panelOrderLeftRight = newDeviceConfig["panelOrderLeftRight"].toInt(); - } - else - { - panelOrderLeftRight = newDeviceConfig["panelOrderLeftRight"].toString().toInt(); - } - - newDeviceConfig.remove("panelOrderLeftRight"); - if (panelOrderLeftRight == 0) - { - newDeviceConfig["panelOrderLeftRight"] = "left2right"; - migrated = true; - } - else - { - if (panelOrderLeftRight == 1) - { - newDeviceConfig["panelOrderLeftRight"] = "right2left"; - migrated = true; - } - } - } - } - } - - if (migrated) - { - config["device"] = newDeviceConfig; - Debug(_log, "LED-Device records migrated"); - } - } - - if (config.contains("cecEvents")) - { - bool isCECEnabled {false}; - if (config.contains("grabberV4L2")) - { - QJsonObject newGrabberV4L2Config = config["grabberV4L2"].toObject(); - if (newGrabberV4L2Config.contains("cecDetection")) - { - isCECEnabled = newGrabberV4L2Config.value("cecDetection").toBool(false); - newGrabberV4L2Config.remove("cecDetection"); - config["grabberV4L2"] = newGrabberV4L2Config; - - QJsonObject newGCecEventsConfig = config["cecEvents"].toObject(); - newGCecEventsConfig["enable"] = isCECEnabled; - if (!newGCecEventsConfig.contains("actions")) - { - QJsonObject action1 - { - {"action", "Suspend"}, - {"event", "standby"} - }; - QJsonObject action2 - { - {"action", "Resume"}, - {"event", "set stream path"} - }; - - QJsonArray actions { action1, action2 }; - newGCecEventsConfig.insert("actions",actions); - } - config["cecEvents"] = newGCecEventsConfig; - - migrated = true; - Debug(_log, "CEC configuration records migrated"); - } - } - } + errorList.append(QString("Failed to save configuration item: %1").arg(key)); + return qMakePair (false, errorList); } + emit settingsChanged(settings::stringToType(key), QJsonDocument::fromVariant(configItem.toVariant())); } } - return migrated; + return qMakePair (true, errorList ); } diff --git a/libsrc/hyperion/hyperion.schema.json b/libsrc/hyperion/hyperion.schema.json deleted file mode 100644 index ae22cfe28..000000000 --- a/libsrc/hyperion/hyperion.schema.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "type" : "object", - "required" : true, - "properties" : - { - "general" : - { - "$ref": "schema-general.json" - }, - "logger" : - { - "$ref": "schema-logger.json" - }, - "device" : - { - "$ref": "schema-device.json" - }, - "color" : - { - "$ref": "schema-color.json" - }, - "smoothing": - { - "$ref": "schema-smoothing.json" - }, - "grabberV4L2" : - { - "$ref": "schema-grabberV4L2.json" - }, - "grabberAudio" : - { - "$ref": "schema-grabberAudio.json" - }, - "framegrabber" : - { - "$ref": "schema-framegrabber.json" - }, - "blackborderdetector" : - { - "$ref": "schema-blackborderdetector.json" - }, - "foregroundEffect" : - { - "$ref": "schema-foregroundEffect.json" - }, - "backgroundEffect" : - { - "$ref": "schema-backgroundEffect.json" - }, - "forwarder" : - { - "$ref": "schema-forwarder.json" - }, - "jsonServer" : - { - "$ref": "schema-jsonServer.json" - }, - "flatbufServer": - { - "$ref": "schema-flatbufServer.json" - }, - "protoServer" : - { - "$ref": "schema-protoServer.json" - }, - "boblightServer" : - { - "$ref": "schema-boblightServer.json" - }, - "webConfig" : - { - "$ref": "schema-webConfig.json" - }, - "effects" : - { - "$ref": "schema-effects.json" - }, - "instCapture": - { - "$ref": "schema-instCapture.json" - }, - "network": - { - "$ref": "schema-network.json" - }, - "ledConfig": - { - "$ref": "schema-ledConfig.json" - }, - "leds": - { - "$ref": "schema-leds.json" - }, - "osEvents": - { - "$ref": "schema-osEvents.json" - }, - "cecEvents": - { - "$ref": "schema-cecEvents.json" - }, - "schedEvents": - { - "$ref": "schema-schedEvents.json" - } - }, - "additionalProperties" : false -} diff --git a/libsrc/hyperion/resource.qrc b/libsrc/hyperion/resource.qrc index b1c52ea19..60eaaccb1 100644 --- a/libsrc/hyperion/resource.qrc +++ b/libsrc/hyperion/resource.qrc @@ -1,31 +1,39 @@ - hyperion.schema.json - schema/schema-general.json - schema/schema-logger.json - schema/schema-device.json + schema/schema-backgroundEffect.json + schema/schema-blackborderdetector.json + schema/schema-boblightServer.json + schema/schema-cecEvents.json schema/schema-color.json - schema/schema-smoothing.json - schema/schema-grabberV4L2.json - schema/schema-grabberAudio.json + schema/schema-device.json + schema/schema-effects.json + schema/schema-eventActions.json + schema/schema-flatbufServer.json + schema/schema-forwarder.json schema/schema-framegrabber.json - schema/schema-blackborderdetector.json schema/schema-foregroundEffect.json - schema/schema-backgroundEffect.json - schema/schema-forwarder.json + schema/schema-general.json + schema/schema-grabberAudio.json + schema/schema-grabberV4L2.json + schema/schema-instCapture.json schema/schema-jsonServer.json - schema/schema-flatbufServer.json - schema/schema-protoServer.json - schema/schema-boblightServer.json - schema/schema-webConfig.json - schema/schema-effects.json schema/schema-ledConfig.json schema/schema-leds.json - schema/schema-instCapture.json + schema/schema-logger.json schema/schema-network.json - schema/schema-eventActions.json - schema/schema-schedEvents.json schema/schema-osEvents.json - schema/schema-cecEvents.json + schema/schema-protoServer.json + schema/schema-schedEvents.json + schema/schema-smoothing.json + schema/schema-webConfig.json + + schema/schema-settings-default.json + schema/schema-settings-full.json + schema/schema-settings-full-relaxed.json + schema/schema-settings-global.json + schema/schema-settings-global-relaxed.json + schema/schema-settings-instance.json + schema/schema-settings-instance-relaxed.json + schema/schema-settings-ui.json diff --git a/libsrc/hyperion/schema/schema-cecEvents.json b/libsrc/hyperion/schema/schema-cecEvents.json index a5790a362..f0a1a9672 100644 --- a/libsrc/hyperion/schema/schema-cecEvents.json +++ b/libsrc/hyperion/schema/schema-cecEvents.json @@ -1,6 +1,5 @@ { "type": "object", - "required": true, "properties": { "enable": { "type": "boolean", diff --git a/libsrc/hyperion/schema/schema-color.json b/libsrc/hyperion/schema/schema-color.json index a56d3bed9..8068036b7 100644 --- a/libsrc/hyperion/schema/schema-color.json +++ b/libsrc/hyperion/schema/schema-color.json @@ -1,7 +1,6 @@ { "type":"object", "title" : "edt_conf_color_heading_title", - "required" : true, "properties": { "imageToLedMappingType" : diff --git a/libsrc/hyperion/schema/schema-flatbufServer.json b/libsrc/hyperion/schema/schema-flatbufServer.json index e86633d92..523bd45b4 100644 --- a/libsrc/hyperion/schema/schema-flatbufServer.json +++ b/libsrc/hyperion/schema/schema-flatbufServer.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_fbs_heading_title", "properties" : { diff --git a/libsrc/hyperion/schema/schema-foregroundEffect.json b/libsrc/hyperion/schema/schema-foregroundEffect.json index 71cf5d154..ad23fea65 100644 --- a/libsrc/hyperion/schema/schema-foregroundEffect.json +++ b/libsrc/hyperion/schema/schema-foregroundEffect.json @@ -7,6 +7,7 @@ { "type" : "boolean", "title" : "edt_conf_general_enable_title", + "required" : true, "default" : true, "propertyOrder" : 1 }, diff --git a/libsrc/hyperion/schema/schema-forwarder.json b/libsrc/hyperion/schema/schema-forwarder.json index 07b7d0507..307321b51 100644 --- a/libsrc/hyperion/schema/schema-forwarder.json +++ b/libsrc/hyperion/schema/schema-forwarder.json @@ -1,7 +1,6 @@ { "type": "object", "title": "edt_conf_fw_heading_title", - "required": true, "properties": { "enable": { "type": "boolean", diff --git a/libsrc/hyperion/schema/schema-general.json b/libsrc/hyperion/schema/schema-general.json index 422054b5a..5b75b9f85 100644 --- a/libsrc/hyperion/schema/schema-general.json +++ b/libsrc/hyperion/schema/schema-general.json @@ -1,7 +1,6 @@ { "type" : "object", "title" : "edt_conf_gen_heading_title", - "required" : true, "properties" : { "name" : @@ -44,15 +43,6 @@ }, "access" : "expert", "propertyOrder" : 4 - }, - "previousVersion" : - { - "type" : "string", - "options" : { - "hidden":true - }, - "access" : "expert", - "propertyOrder" : 5 } }, "additionalProperties" : false diff --git a/libsrc/hyperion/schema/schema-grabberAudio.json b/libsrc/hyperion/schema/schema-grabberAudio.json index 0fa780ee5..183ddca6b 100644 --- a/libsrc/hyperion/schema/schema-grabberAudio.json +++ b/libsrc/hyperion/schema/schema-grabberAudio.json @@ -1,6 +1,5 @@ { "type": "object", - "required": true, "title": "edt_conf_audio_heading_title", "properties": { "enable": { diff --git a/libsrc/hyperion/schema/schema-grabberV4L2.json b/libsrc/hyperion/schema/schema-grabberV4L2.json index 396f8868c..e5ac3102a 100644 --- a/libsrc/hyperion/schema/schema-grabberV4L2.json +++ b/libsrc/hyperion/schema/schema-grabberV4L2.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_v4l2_heading_title", "properties": { @@ -24,6 +23,7 @@ "device": { "type": "string", "title": "edt_conf_enum_custom", + "default": "none", "options": { "hidden": true }, diff --git a/libsrc/hyperion/schema/schema-instCapture.json b/libsrc/hyperion/schema/schema-instCapture.json index a4076a6a5..c07fd550d 100644 --- a/libsrc/hyperion/schema/schema-instCapture.json +++ b/libsrc/hyperion/schema/schema-instCapture.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_instC_heading_title", "properties": { "systemEnable": { diff --git a/libsrc/hyperion/schema/schema-jsonServer.json b/libsrc/hyperion/schema/schema-jsonServer.json index 798acf7c0..e25229ae0 100644 --- a/libsrc/hyperion/schema/schema-jsonServer.json +++ b/libsrc/hyperion/schema/schema-jsonServer.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_js_heading_title", "properties" : { diff --git a/libsrc/hyperion/schema/schema-leds.json b/libsrc/hyperion/schema/schema-leds.json index b663f6456..34f165bff 100644 --- a/libsrc/hyperion/schema/schema-leds.json +++ b/libsrc/hyperion/schema/schema-leds.json @@ -1,6 +1,5 @@ { "type": "array", - "required": true, "minItems": 1, "items": { "type": "object", @@ -61,4 +60,4 @@ }, "additionalProperties": false } -} \ No newline at end of file +} diff --git a/libsrc/hyperion/schema/schema-logger.json b/libsrc/hyperion/schema/schema-logger.json index 11243b057..d80e1686c 100644 --- a/libsrc/hyperion/schema/schema-logger.json +++ b/libsrc/hyperion/schema/schema-logger.json @@ -6,6 +6,7 @@ "level" : { "type" : "string", + "required" : true, "enum" : ["silent", "warn", "verbose", "debug"], "title" : "edt_conf_log_level_title", "options" : { diff --git a/libsrc/hyperion/schema/schema-network.json b/libsrc/hyperion/schema/schema-network.json index b5bca5ae8..0d72f7bec 100644 --- a/libsrc/hyperion/schema/schema-network.json +++ b/libsrc/hyperion/schema/schema-network.json @@ -1,7 +1,6 @@ { "type" : "object", "title" : "edt_conf_net_heading_title", - "required" : true, "properties" : { "internetAccessAPI" : diff --git a/libsrc/hyperion/schema/schema-osEvents.json b/libsrc/hyperion/schema/schema-osEvents.json index 9adc91616..46befc61a 100644 --- a/libsrc/hyperion/schema/schema-osEvents.json +++ b/libsrc/hyperion/schema/schema-osEvents.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_os_events_heading_title", "properties": { "suspendEnable": { diff --git a/libsrc/hyperion/schema/schema-protoServer.json b/libsrc/hyperion/schema/schema-protoServer.json index 6a70e3f9b..d7d391e4d 100644 --- a/libsrc/hyperion/schema/schema-protoServer.json +++ b/libsrc/hyperion/schema/schema-protoServer.json @@ -1,6 +1,5 @@ { "type" : "object", - "required" : true, "title" : "edt_conf_pbs_heading_title", "properties" : { diff --git a/libsrc/hyperion/schema/schema-schedEvents.json b/libsrc/hyperion/schema/schema-schedEvents.json index 52fb8b960..85c7f7822 100644 --- a/libsrc/hyperion/schema/schema-schedEvents.json +++ b/libsrc/hyperion/schema/schema-schedEvents.json @@ -1,6 +1,5 @@ { "type": "object", - "required": true, "properties": { "enable": { "type": "boolean", diff --git a/libsrc/hyperion/schema/schema-settings-default.json b/libsrc/hyperion/schema/schema-settings-default.json new file mode 100644 index 000000000..063a9b11e --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-default.json @@ -0,0 +1,17 @@ +{ + "type":"object", + "required":true, + "properties":{ + "global":{ + "type":"object", + "required":true, + "$ref":"schema-settings-global.json" + }, + "instance":{ + "type":"object", + "required":true, + "$ref":"schema-settings-instance.json" + } + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-full-relaxed.json b/libsrc/hyperion/schema/schema-settings-full-relaxed.json new file mode 100644 index 000000000..dc7576a94 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-full-relaxed.json @@ -0,0 +1,59 @@ +{ + "type":"object", + "required":true, + "properties":{ + "global":{ + "type":"object", + "required":false, + "properties":{ + "settings":{ + "type":"object", + "required":true, + "$ref":"schema-settings-global-relaxed.json" + }, + "uuid":{ + "type":"string", + "format":"uuid", + "required":false + } + }, + "additionalProperties":false + }, + "instanceIds":{ + "type":"array", + "required":false, + "items":{ + "type":"integer" + }, + "minItems":1 + }, + "instances":{ + "type":"array", + "required":false, + "items":{ + "type":"object", + "properties":{ + "enabled":{ + "type":"boolean" + }, + "id":{ + "type":"integer", + "minimum":0, + "maximum":255 + }, + "name":{ + "type":"string", + "minLength":5 + }, + "settings":{ + "type":"object", + "required":true, + "$ref":"schema-settings-instance-relaxed.json" + } + } + } + }, + "additionalProperties":true + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-full.json b/libsrc/hyperion/schema/schema-settings-full.json new file mode 100644 index 000000000..f1b35f9c4 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-full.json @@ -0,0 +1,59 @@ +{ + "type":"object", + "required":true, + "properties":{ + "global":{ + "type":"object", + "required":true, + "properties":{ + "settings":{ + "type":"object", + "required":true, + "$ref":"schema-settings-global.json" + }, + "uuid":{ + "type":"string", + "format":"uuid", + "required":false + } + }, + "additionalProperties":true + }, + "instanceIds":{ + "type":"array", + "required":false, + "items":{ + "type":"integer" + }, + "minItems":1 + }, + "instances":{ + "type":"array", + "required":true, + "items":{ + "type":"object", + "properties":{ + "enabled":{ + "type":"boolean" + }, + "id":{ + "type":"integer", + "minimum":0, + "maximum":255 + }, + "name":{ + "type":"string", + "minLength":5 + }, + "settings":{ + "type":"object", + "required":true, + "$ref":"schema-settings-instance.json" + } + } + } + }, + "additionalProperties":true + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-global-relaxed.json b/libsrc/hyperion/schema/schema-settings-global-relaxed.json new file mode 100644 index 000000000..53070b91c --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-global-relaxed.json @@ -0,0 +1,62 @@ +{ + "type":"object", + "properties":{ + "cecEvents":{ + "required":false, + "$ref":"schema-cecEvents.json" + }, + "flatbufServer":{ + "required":false, + "$ref":"schema-flatbufServer.json" + }, + "forwarder":{ + "required":false, + "$ref":"schema-forwarder.json" + }, + "framegrabber":{ + "required":false, + "$ref":"schema-framegrabber.json" + }, + "general":{ + "required":false, + "$ref":"schema-general.json" + }, + "grabberAudio":{ + "required":false, + "$ref":"schema-grabberAudio.json" + }, + "grabberV4L2":{ + "required":false, + "$ref":"schema-grabberV4L2.json" + }, + "jsonServer":{ + "required":false, + "$ref":"schema-jsonServer.json" + }, + "logger":{ + "required":false, + "$ref":"schema-logger.json" + }, + "network":{ + "required":false, + "$ref":"schema-network.json" + }, + "osEvents":{ + "required":false, + "$ref":"schema-osEvents.json" + }, + "protoServer":{ + "required":false, + "$ref":"schema-protoServer.json" + }, + "schedEvents":{ + "required":false, + "$ref":"schema-schedEvents.json" + }, + "webConfig":{ + "required":false, + "$ref":"schema-webConfig.json" + } + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-global.json b/libsrc/hyperion/schema/schema-settings-global.json new file mode 100644 index 000000000..6b1f3b858 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-global.json @@ -0,0 +1,62 @@ +{ + "type":"object", + "properties":{ + "cecEvents":{ + "required":true, + "$ref":"schema-cecEvents.json" + }, + "flatbufServer":{ + "required":true, + "$ref":"schema-flatbufServer.json" + }, + "forwarder":{ + "required":true, + "$ref":"schema-forwarder.json" + }, + "framegrabber":{ + "required":true, + "$ref":"schema-framegrabber.json" + }, + "general":{ + "required":true, + "$ref":"schema-general.json" + }, + "grabberAudio":{ + "required":true, + "$ref":"schema-grabberAudio.json" + }, + "grabberV4L2":{ + "required":true, + "$ref":"schema-grabberV4L2.json" + }, + "jsonServer":{ + "required":true, + "$ref":"schema-jsonServer.json" + }, + "logger":{ + "required":true, + "$ref":"schema-logger.json" + }, + "network":{ + "required":true, + "$ref":"schema-network.json" + }, + "osEvents":{ + "required":true, + "$ref":"schema-osEvents.json" + }, + "protoServer":{ + "required":true, + "$ref":"schema-protoServer.json" + }, + "schedEvents":{ + "required":true, + "$ref":"schema-schedEvents.json" + }, + "webConfig":{ + "required":true, + "$ref":"schema-webConfig.json" + } + }, + "additionalProperties":true +} diff --git a/libsrc/hyperion/schema/schema-settings-instance-relaxed.json b/libsrc/hyperion/schema/schema-settings-instance-relaxed.json new file mode 100644 index 000000000..250e7c4e0 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-instance-relaxed.json @@ -0,0 +1,50 @@ +{ + "type":"object", + "properties":{ + "backgroundEffect":{ + "required":false, + "$ref":"schema-backgroundEffect.json" + }, + "blackborderdetector":{ + "required":false, + "$ref":"schema-blackborderdetector.json" + }, + "boblightServer":{ + "required":false, + "$ref":"schema-boblightServer.json" + }, + "color":{ + "required":false, + "$ref":"schema-color.json" + }, + "device":{ + "required":false, + "$ref":"schema-device.json" + }, + "effects":{ + "required":false, + "$ref":"schema-effects.json" + }, + "foregroundEffect":{ + "required":false, + "$ref":"schema-foregroundEffect.json" + }, + "instCapture":{ + "required":false, + "$ref":"schema-instCapture.json" + }, + "ledConfig":{ + "required":false, + "$ref":"schema-ledConfig.json" + }, + "leds":{ + "required":false, + "$ref":"schema-leds.json" + }, + "smoothing":{ + "required":false, + "$ref":"schema-smoothing.json" + } + }, + "additionalProperties":false +} diff --git a/libsrc/hyperion/schema/schema-settings-instance.json b/libsrc/hyperion/schema/schema-settings-instance.json new file mode 100644 index 000000000..7bf78f499 --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-instance.json @@ -0,0 +1,50 @@ +{ + "type":"object", + "properties":{ + "backgroundEffect":{ + "required":true, + "$ref":"schema-backgroundEffect.json" + }, + "blackborderdetector":{ + "required":true, + "$ref":"schema-blackborderdetector.json" + }, + "boblightServer":{ + "required":true, + "$ref":"schema-boblightServer.json" + }, + "color":{ + "required":true, + "$ref":"schema-color.json" + }, + "device":{ + "required":true, + "$ref":"schema-device.json" + }, + "effects":{ + "required":true, + "$ref":"schema-effects.json" + }, + "foregroundEffect":{ + "required":true, + "$ref":"schema-foregroundEffect.json" + }, + "instCapture":{ + "required":true, + "$ref":"schema-instCapture.json" + }, + "ledConfig":{ + "required":true, + "$ref":"schema-ledConfig.json" + }, + "leds":{ + "required":true, + "$ref":"schema-leds.json" + }, + "smoothing":{ + "required":true, + "$ref":"schema-smoothing.json" + } + }, + "additionalProperties":true +} diff --git a/libsrc/hyperion/schema/schema-settings-ui.json b/libsrc/hyperion/schema/schema-settings-ui.json new file mode 100644 index 000000000..b78fd291b --- /dev/null +++ b/libsrc/hyperion/schema/schema-settings-ui.json @@ -0,0 +1,107 @@ +{ + "type":"object", + "required":true, + "properties":{ + "cecEvents":{ + "required":true, + "$ref":"schema-cecEvents.json" + }, + "flatbufServer":{ + "required":true, + "$ref":"schema-flatbufServer.json" + }, + "forwarder":{ + "required":true, + "$ref":"schema-forwarder.json" + }, + "framegrabber":{ + "required":true, + "$ref":"schema-framegrabber.json" + }, + "general":{ + "required":true, + "$ref":"schema-general.json" + }, + "grabberAudio":{ + "required":true, + "$ref":"schema-grabberAudio.json" + }, + "grabberV4L2":{ + "required":true, + "$ref":"schema-grabberV4L2.json" + }, + "jsonServer":{ + "required":true, + "$ref":"schema-jsonServer.json" + }, + "logger":{ + "required":true, + "$ref":"schema-logger.json" + }, + "network":{ + "required":true, + "$ref":"schema-network.json" + }, + "osEvents":{ + "required":true, + "$ref":"schema-osEvents.json" + }, + "protoServer":{ + "required":true, + "$ref":"schema-protoServer.json" + }, + "schedEvents":{ + "required":true, + "$ref":"schema-schedEvents.json" + }, + "webConfig":{ + "required":true, + "$ref":"schema-webConfig.json" + }, + "backgroundEffect":{ + "required":true, + "$ref":"schema-backgroundEffect.json" + }, + "blackborderdetector":{ + "required":true, + "$ref":"schema-blackborderdetector.json" + }, + "boblightServer":{ + "required":true, + "$ref":"schema-boblightServer.json" + }, + "color":{ + "required":true, + "$ref":"schema-color.json" + }, + "device":{ + "required":true, + "$ref":"schema-device.json" + }, + "effects":{ + "required":true, + "$ref":"schema-effects.json" + }, + "foregroundEffect":{ + "required":true, + "$ref":"schema-foregroundEffect.json" + }, + "instCapture":{ + "required":true, + "$ref":"schema-instCapture.json" + }, + "ledConfig":{ + "required":true, + "$ref":"schema-ledConfig.json" + }, + "leds":{ + "required":true, + "$ref":"schema-leds.json" + }, + "smoothing":{ + "required":true, + "$ref":"schema-smoothing.json" + } + }, + "additionalProperties":false +} diff --git a/libsrc/ssdp/SSDPHandler.cpp b/libsrc/ssdp/SSDPHandler.cpp index 329996426..5f2dd021f 100644 --- a/libsrc/ssdp/SSDPHandler.cpp +++ b/libsrc/ssdp/SSDPHandler.cpp @@ -4,7 +4,7 @@ #include "SSDPDescription.h" #include #include -#include +#include #include #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) @@ -40,7 +40,8 @@ SSDPHandler::~SSDPHandler() void SSDPHandler::initServer() { - _uuid = AuthManager::getInstance()->getID(); + MetaTable metaTable; + _uuid = metaTable.getUUID(); SSDPServer::setUuid(_uuid); // announce targets diff --git a/libsrc/utils/JsonUtils.cpp b/libsrc/utils/JsonUtils.cpp index 1fd44f684..e974e0cab 100644 --- a/libsrc/utils/JsonUtils.cpp +++ b/libsrc/utils/JsonUtils.cpp @@ -7,155 +7,251 @@ //qt includes #include #include +#include +#include #include #include namespace JsonUtils { - QPair readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError) - { - QString data; - if(!FileUtils::readFile(path, data, log, ignError)) - { - return qMakePair(false, QStringList(QString("Error reading file: %1").arg(path))); - } +QPair readFile(const QString& path, QJsonObject& obj, Logger* log, bool ignError) +{ + QJsonValue value(obj); - QPair parsingResult = JsonUtils::parse(path, data, obj, log); - return parsingResult; - } + QPair result = readFile(path, value,log, ignError); + obj = value.toObject(); + return result; +} - bool readSchema(const QString& path, QJsonObject& obj, Logger* log) +QPair readFile(const QString& path, QJsonValue& obj, Logger* log, bool ignError) +{ + QString data; + if(!FileUtils::readFile(path, data, log, ignError)) { - QJsonObject schema; - if(!readFile(path, schema, log).first) - return false; + return qMakePair(false, QStringList(QString("Error reading file: %1").arg(path))); + } + + QPair parsingResult = JsonUtils::parse(path, data, obj, log); + return parsingResult; +} - if(!resolveRefs(schema, obj, log)) - return false; - return true; +bool readSchema(const QString& path, QJsonObject& obj, Logger* log) +{ + QJsonObject schema; + if(!readFile(path, schema, log).first) + { + return false; } - QPair parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log) + if(!resolveRefs(schema, obj, log)) { - QJsonDocument doc; - QPair parsingResult = JsonUtils::parse(path, data, doc, log); - obj = doc.object(); - return parsingResult; + return false; } - QPair parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log) + return true; +} + +QPair parse(const QString& path, const QString& data, QJsonObject& obj, Logger* log) +{ + QJsonValue value(obj); + + QPair result = JsonUtils::parse(path, data, value, log); + obj = value.toObject(); + return result; +} + +QPair parse(const QString& path, const QString& data, QJsonArray& arr, Logger* log) +{ + QJsonValue value(arr); + + QPair result = JsonUtils::parse(path, data, value, log); + arr = value.toArray(); + return result; +} + +QPair parse(const QString& path, const QString& data, QJsonValue& value, Logger* log) +{ + QJsonDocument doc; + QPair parsingResult = JsonUtils::parse(path, data, doc, log); + value = doc.object(); + return parsingResult; +} + +QPair parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log) +{ + QStringList errorList; + + QJsonParseError error; + doc = QJsonDocument::fromJson(data.toUtf8(), &error); + + if (error.error != QJsonParseError::NoError) { - QJsonDocument doc; + int errorLine = 1; + int errorColumn = 1; - QPair parsingResult = JsonUtils::parse(path, data, doc, log); - arr = doc.array(); - return parsingResult; + int lastNewlineIndex = data.lastIndexOf("\n", error.offset - 1); + if (lastNewlineIndex != -1) + { + errorColumn = error.offset - lastNewlineIndex ; + } + errorLine += data.left(error.offset).count('\n'); + + const QString errorMessage = QString("JSON parse error @(%1): %2, line: %3, column: %4, Data: '%5'") + .arg(path) + .arg(error.errorString()) + .arg(errorLine) + .arg(errorColumn) + .arg(data); + errorList.push_back(errorMessage); + Error(log, "%s", QSTRING_CSTR(errorMessage)); + + return qMakePair(false, errorList); } + return qMakePair(true, errorList); +} - QPair parse(const QString& path, const QString& data, QJsonDocument& doc, Logger* log) - { - QStringList errorList; +QPair validate(const QString& file, const QJsonValue& json, const QString& schemaPath, Logger* log) +{ + // get the schema data + QJsonObject schema; - QJsonParseError error; - doc = QJsonDocument::fromJson(data.toUtf8(), &error); + QPair readResult = readFile(schemaPath, schema, log); + if(!readResult.first) + { + return readResult; + } - if (error.error != QJsonParseError::NoError) - { - int errorLine = 1; - int errorColumn = 1; + QPair validationResult = validate(file, json, schema, log); + return validationResult; +} - int lastNewlineIndex = data.lastIndexOf("\n", error.offset - 1); - if (lastNewlineIndex != -1) - { - errorColumn = error.offset - lastNewlineIndex ; - } - errorLine += data.left(error.offset).count('\n'); +QPair validate(const QString& file, const QJsonValue& json, const QJsonObject& schema, Logger* log) +{ + QStringList errorList; - const QString errorMessage = QString("JSON parse error: %1, line: %2, column: %3, Data: '%4'") - .arg(error.errorString()) - .arg(errorLine) - .arg(errorColumn) - .arg(data); + QJsonSchemaChecker schemaChecker; + schemaChecker.setSchema(schema); + if (!schemaChecker.validate(json).first) + { + const QStringList &errors = schemaChecker.getMessages(); + for (const auto& error : errors) + { + QString errorMessage = QString("JSON parse error @(%1) - %2") + .arg(file, error); errorList.push_back(errorMessage); Error(log, "%s", QSTRING_CSTR(errorMessage)); - - return qMakePair(false, errorList); } - return qMakePair(true, errorList); + return qMakePair(false, errorList); } + return qMakePair(true, errorList); +} - QPair validate(const QString& file, const QJsonObject& json, const QString& schemaPath, Logger* log) +QPair correct(const QString& file, QJsonValue& json, const QJsonObject& schema, Logger* log) +{ + bool wasCorrected {false}; + QStringList corrections; + QJsonValue correctJson; + + QJsonSchemaChecker schemaChecker; + schemaChecker.setSchema(schema); + if (!schemaChecker.validate(json).first) { - // get the schema data - QJsonObject schema; + Warning(log, "Fixing JSON data!"); + json = schemaChecker.getAutoCorrectedConfig(json); + wasCorrected = true; - QPair readResult = readFile(schemaPath, schema, log); - if(!readResult.first) + const QStringList &correctionMessages = schemaChecker.getMessages(); + for (const auto& correction : correctionMessages) { - return readResult; + QString message = QString("JSON fix @(%1) - %2") + .arg(file, correction); + corrections.push_back(message); + Warning(log, "%s", QSTRING_CSTR(message)); } - - QPair validationResult = validate(file, json, schema, log); - return validationResult; } + return qMakePair(wasCorrected, corrections); +} + +bool write(const QString& filename, const QJsonObject& json, Logger* log) +{ + QJsonDocument doc; + + doc.setObject(json); + QByteArray data = doc.toJson(QJsonDocument::Indented); + + return FileUtils::writeFile(filename, data, log); +} - QPair validate(const QString& file, const QJsonObject& json, const QJsonObject& schema, Logger* log) +bool resolveRefs(const QJsonObject& schema, QJsonObject& obj, Logger* log) +{ + for (QJsonObject::const_iterator i = schema.begin(); i != schema.end(); ++i) { - QStringList errorList; + QString attribute = i.key(); + const QJsonValue & attributeValue = *i; - QJsonSchemaChecker schemaChecker; - schemaChecker.setSchema(schema); - if (!schemaChecker.validate(json).first) + if (attribute == "$ref" && attributeValue.isString()) { - const QStringList &errors = schemaChecker.getMessages(); - for (const auto& error : errors) + if(!readSchema(":/" + attributeValue.toString(), obj, log)) { - QString errorMessage = QString("JSON parse error: %1") - .arg(error); - errorList.push_back(errorMessage); - Error(log, "%s", QSTRING_CSTR(errorMessage)); + Error(log,"Error while getting schema ref: %s",QSTRING_CSTR(QString(":/" + attributeValue.toString()))); + return false; } - return qMakePair(false, errorList); } - return qMakePair(true, errorList); + else if (attributeValue.isObject()) + { + obj.insert(attribute, resolveRefs(attributeValue.toObject(), obj, log)); + } + else + { + obj.insert(attribute, attributeValue); + } } + return true; +} - bool write(const QString& filename, const QJsonObject& json, Logger* log) +QString jsonValueToQString(const QJsonValue &value, QJsonDocument::JsonFormat format) +{ + switch (value.type()) { + case QJsonValue::Object: { - QJsonDocument doc; - - doc.setObject(json); - QByteArray data = doc.toJson(QJsonDocument::Indented); - - if(!FileUtils::writeFile(filename, data, log)) - return false; - - return true; + return QJsonDocument(value.toObject()).toJson(format); } - - bool resolveRefs(const QJsonObject& schema, QJsonObject& obj, Logger* log) + case QJsonValue::Array: { - for (QJsonObject::const_iterator i = schema.begin(); i != schema.end(); ++i) - { - QString attribute = i.key(); - const QJsonValue & attributeValue = *i; + return QJsonDocument(value.toArray()).toJson(format); + } + case QJsonValue::String: + case QJsonValue::Double: + case QJsonValue::Bool: + { + return value.toString(); + } + case QJsonValue::Null: + { + return "Null"; + } + default: + break; + } + return QString(); +} - if (attribute == "$ref" && attributeValue.isString()) - { - if(!readSchema(":/" + attributeValue.toString(), obj, log)) - { - Error(log,"Error while getting schema ref: %s",QSTRING_CSTR(QString(":/" + attributeValue.toString()))); - return false; - } - } - else if (attributeValue.isObject()) - obj.insert(attribute, resolveRefs(attributeValue.toObject(), obj, log)); - else +QJsonObject mergeJsonObjects(const QJsonObject &obj1, const QJsonObject &obj2, bool overrideObj1) +{ + QJsonObject result = obj1; + + for (auto it = obj2.begin(); it != obj2.end(); ++it) { + if (result.contains(it.key())) { + if (overrideObj1) { - obj.insert(attribute, attributeValue); + result[it.key()] = it.value(); } + } else { + result[it.key()] = it.value(); } - return true; } + + return result; +} }; diff --git a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp index 8cd0be75d..04bb0a813 100644 --- a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp +++ b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp @@ -40,7 +40,7 @@ QStringList QJsonSchemaChecker::getMessages() const return _messages; } -QPair QJsonSchemaChecker::validate(const QJsonObject& value, bool ignoreRequired) +QPair QJsonSchemaChecker::validate(const QJsonValue& value, bool ignoreRequired) { // initialize state _ignoreRequired = ignoreRequired; @@ -56,7 +56,7 @@ QPair QJsonSchemaChecker::validate(const QJsonObject& value, bool ig return QPair(!_error, !_schemaError); } -QJsonObject QJsonSchemaChecker::getAutoCorrectedConfig(const QJsonObject& value, bool ignoreRequired) +QJsonValue QJsonSchemaChecker::getAutoCorrectedConfig(const QJsonValue& value, bool ignoreRequired) { _ignoreRequired = ignoreRequired; const QStringList sequence = QStringList() << "remove" << "modify" << "create"; diff --git a/resources/resources.qrc.in b/resources/resources.qrc.in index d5ed6c803..abc5d1c93 100644 --- a/resources/resources.qrc.in +++ b/resources/resources.qrc.in @@ -1,6 +1,6 @@ - ${CMAKE_BINARY_DIR}/config/hyperion.config.json.default + ${CMAKE_BINARY_DIR}/settings/hyperion.settings.json.default ${HYPERION_RES} diff --git a/config/.gitignore b/settings/.gitignore similarity index 100% rename from config/.gitignore rename to settings/.gitignore diff --git a/settings/hyperion.settings.json.default b/settings/hyperion.settings.json.default new file mode 100644 index 000000000..0f79800d7 --- /dev/null +++ b/settings/hyperion.settings.json.default @@ -0,0 +1,309 @@ +{ + "global":{ + "cecEvents":{ + "actions":[ + { + "action":"Suspend", + "event":"standby" + }, + { + "action":"Resume", + "event":"set stream path" + } + ], + "enable":false + }, + "flatbufServer":{ + "enable":true, + "port":19400, + "timeout":5 + }, + "forwarder":{ + "enable":false, + "jsonapi":[ + + ], + "flatbuffer":[ + + ] + }, + "framegrabber":{ + "enable":false, + "device":"auto", + "input":0, + "width":80, + "height":45, + "fps":10, + "pixelDecimation":8, + "cropLeft":0, + "cropRight":0, + "cropTop":0, + "cropBottom":0 + }, + "general":{ + "name":"My Hyperion Config", + "configVersion":"configVersionValue", + "watchedVersionBranch":"Stable", + "showOptHelp":true + }, + "grabberAudio":{ + "enable":false, + "device":"auto", + "audioEffect":"vuMeter", + "vuMeter":{ + "flip":"NO_CHANGE", + "hotColor":[ + 255, + 0, + 0 + ], + "multiplier":1, + "safeColor":[ + 0, + 255, + 0 + ], + "safeValue":45, + "tolerance":5, + "warnColor":[ + 255, + 255, + 0 + ], + "warnValue":80 + } + }, + "grabberV4L2":{ + "enable":false, + "device":"none", + "input":0, + "encoding":"NO_CHANGE", + "width":0, + "height":0, + "fps":15, + "flip":"NO_CHANGE", + "fpsSoftwareDecimation":0, + "sizeDecimation":8, + "cropLeft":0, + "cropRight":0, + "cropTop":0, + "cropBottom":0, + "redSignalThreshold":0, + "greenSignalThreshold":100, + "blueSignalThreshold":0, + "signalDetection":false, + "noSignalCounterThreshold":200, + "sDVOffsetMin":0.1, + "sDVOffsetMax":0.9, + "sDHOffsetMin":0.4, + "sDHOffsetMax":0.46, + "hardware_brightness":0, + "hardware_contrast":0, + "hardware_saturation":0, + "hardware_hue":0 + }, + "jsonServer":{ + "port":19444 + }, + "logger":{ + "level":"warn" + }, + "network":{ + "internetAccessAPI":false, + "restirctedInternetAccessAPI":false, + "ipWhitelist":[ + + ], + "localApiAuth":false + }, + "osEvents":{ + "suspendEnable":true, + "lockEnable":true + }, + "protoServer":{ + "enable":true, + "port":19445, + "timeout":5 + }, + "schedEvents":{ + "enable":false + }, + "webConfig":{ + "document_root":"", + "port":8090, + "sslPort":8092, + "crtPath":"", + "keyPath":"", + "keyPassPhrase":"" + } + }, + "instance":{ + "backgroundEffect":{ + "enable":false, + "type":"effect", + "color":[ + 255, + 138, + 0 + ], + "effect":"Warm mood blobs" + }, + "blackborderdetector":{ + "enable":true, + "threshold":5, + "unknownFrameCnt":600, + "borderFrameCnt":50, + "maxInconsistentCnt":10, + "blurRemoveCnt":1, + "mode":"default" + }, + "boblightServer":{ + "enable":false, + "port":19333, + "priority":128 + }, + "color":{ + "imageToLedMappingType":"multicolor_mean", + "channelAdjustment":[ + { + "id":"default", + "leds":"*", + "white":[ + 255, + 255, + 255 + ], + "red":[ + 255, + 0, + 0 + ], + "green":[ + 0, + 255, + 0 + ], + "blue":[ + 0, + 0, + 255 + ], + "cyan":[ + 0, + 255, + 255 + ], + "magenta":[ + 255, + 0, + 255 + ], + "yellow":[ + 255, + 255, + 0 + ], + "gammaRed":2.2, + "gammaGreen":2.2, + "gammaBlue":2.2, + "backlightThreshold":0, + "backlightColored":false, + "brightness":100, + "brightnessCompensation":100, + "saturationGain":1.0, + "brightnessGain":1.0 + } + ] + }, + "device":{ + "type":"file", + "hardwareLedCount":1, + "autoStart":true, + "output":"/dev/null", + "colorOrder":"rgb", + "latchTime":0, + "rewriteTime":0, + "enableAttempts":6, + "enableAttemptsInterval":15 + }, + "effects":{ + "paths":[ + "$ROOT/custom-effects" + ], + "disable":[ + "" + ] + }, + "foregroundEffect":{ + "enable":true, + "type":"effect", + "color":[ + 0, + 0, + 255 + ], + "effect":"Rainbow swirl fast", + "duration_ms":3000 + }, + "instCapture":{ + "systemEnable":false, + "systemGrabberDevice":"NONE", + "systemPriority":250, + "v4lEnable":false, + "v4lGrabberDevice":"NONE", + "v4lPriority":240, + "audioEnable":false, + "audioGrabberDevice":"NONE", + "audioPriority":230 + }, + "ledConfig":{ + "classic":{ + "top":1, + "bottom":0, + "left":0, + "right":0, + "glength":0, + "gpos":0, + "position":0, + "reverse":false, + "hdepth":8, + "vdepth":5, + "overlap":0, + "edgegap":0, + "ptlh":0, + "ptlv":0, + "ptrh":100, + "ptrv":0, + "pblh":0, + "pblv":100, + "pbrh":100, + "pbrv":100 + }, + "matrix":{ + "ledshoriz":1, + "ledsvert":1, + "cabling":"snake", + "direction":"horizontal", + "start":"top-left" + } + }, + "leds":[ + { + "hmax":1, + "hmin":0, + "vmax":0.08, + "vmin":0 + } + ], + "smoothing":{ + "enable":true, + "type":"linear", + "time_ms":150, + "updateFrequency":25.0000, + "interpolationRate":25.0000, + "decay":1, + "dithering":false, + "updateDelay":0 + } + } +} diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index 1b1bb7392..731001e12 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -65,14 +65,14 @@ HyperionDaemon* HyperionDaemon::daemon = nullptr; -HyperionDaemon::HyperionDaemon(const QString& rootPath, QObject* parent, bool logLvlOverwrite, bool readonlyMode) +HyperionDaemon::HyperionDaemon(const QString& rootPath, QObject* parent, bool logLvlOverwrite) : QObject(parent), _log(Logger::getInstance("DAEMON")) - , _instanceManager(new HyperionIManager(rootPath, this, readonlyMode)) - , _settingsManager(new SettingsManager(GLOABL_INSTANCE_ID, this, readonlyMode)) // init settings, this settingsManager accesses global settings which are independent from instances + , _instanceManager(new HyperionIManager(this)) + , _settingsManager(new SettingsManager(GLOABL_INSTANCE_ID, this)) // init settings, this settingsManager accesses global settings which are independent from instances #if defined(ENABLE_EFFECTENGINE) , _pyInit(new PythonInit()) #endif - , _authManager(new AuthManager(this, readonlyMode)) + , _authManager(new AuthManager(this)) , _netOrigin(new NetOrigin(this)) , _currVideoMode(VideoMode::VIDEO_2D) { diff --git a/src/hyperiond/hyperiond.h b/src/hyperiond/hyperiond.h index cf64c1349..e2f95ece6 100644 --- a/src/hyperiond/hyperiond.h +++ b/src/hyperiond/hyperiond.h @@ -114,7 +114,7 @@ class HyperionDaemon : public QObject friend SysTray; public: - HyperionDaemon(const QString& rootPath, QObject *parent, bool logLvlOverwrite, bool readonlyMode = false); + HyperionDaemon(const QString& rootPath, QObject *parent, bool logLvlOverwrite); ~HyperionDaemon() override; /// diff --git a/src/hyperiond/main.cpp b/src/hyperiond/main.cpp index 6dfd35c4d..a9dffe9fc 100644 --- a/src/hyperiond/main.cpp +++ b/src/hyperiond/main.cpp @@ -1,3 +1,4 @@ + #include #include #include @@ -35,6 +36,7 @@ #include #include #include +#include #include <../../include/db/AuthTable.h> #include "detectProcess.h" @@ -108,11 +110,11 @@ int main(int argc, char** argv) // check if we are running already an instance // TODO Allow one session per user - #ifdef _WIN32 - const char* processName = "hyperiond.exe"; - #else - const char* processName = "hyperiond"; - #endif +#ifdef _WIN32 + const char* processName = "hyperiond.exe"; +#else + const char* processName = "hyperiond"; +#endif // Initialising QCoreApplication QScopedPointer app(createApplication(argc, argv)); @@ -131,15 +133,18 @@ int main(int argc, char** argv) BooleanOption & versionOption = parser.add (0x0, "version", "Show version information"); Option & userDataOption = parser.add