From 4bfb361fbc264ffdadd657e887cfc4117ca7032d Mon Sep 17 00:00:00 2001 From: Manuel Date: Tue, 14 Jan 2025 13:48:04 +0100 Subject: [PATCH] Add customization checks and links page --- CMakeLists.txt | 3 +- .../PageSettingsCustomizationChecks.qml | 439 ++++++++++++++++++ pages/settings/PageSettingsGeneral.qml | 44 ++ pages/settings/PageSettingsIntegrations.qml | 14 + pages/settings/PageSettingsNodeRed.qml | 14 + pages/settings/PageSettingsUsefulLinks.qml | 45 ++ 6 files changed, 558 insertions(+), 1 deletion(-) create mode 100644 pages/settings/PageSettingsCustomizationChecks.qml create mode 100644 pages/settings/PageSettingsUsefulLinks.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 3cc657130..6a78213ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -480,6 +480,7 @@ set (VENUS_QML_MODULE_SOURCES pages/settings/PageSettingsCGwacs.qml pages/settings/PageSettingsCGwacsOverview.qml pages/settings/PageSettingsCanbus.qml + pages/settings/PageSettingsCustomizationChecks.qml pages/settings/PageSettingsConnectivity.qml pages/settings/PageSettingsDisplayAndAppearance.qml pages/settings/PageSettingsDisplayBrief.qml @@ -526,6 +527,7 @@ set (VENUS_QML_MODULE_SOURCES pages/settings/PageSettingsTailscale.qml pages/settings/PageSettingsTankPump.qml pages/settings/PageSettingsTcpIp.qml + pages/settings/PageSettingsUsefulLinks.qml pages/settings/PageSettingsVrm.qml pages/settings/PageSettingsVecanDevice.qml pages/settings/PageSettingsVecanDevices.qml @@ -1261,4 +1263,3 @@ add_custom_target( DEPENDS "${PROJECT_SOURCE_DIR}/src/themeobjects.h" ) add_dependencies(${PROJECT_NAME} theme_parser) - diff --git a/pages/settings/PageSettingsCustomizationChecks.qml b/pages/settings/PageSettingsCustomizationChecks.qml new file mode 100644 index 000000000..0ca138514 --- /dev/null +++ b/pages/settings/PageSettingsCustomizationChecks.qml @@ -0,0 +1,439 @@ +/* +** Copyright (C) 2025 Victron Energy B.V. +** See LICENSE.txt for license information. +*/ + +import QtQuick +import Victron.VenusOS +import QtQuick.Templates as T + +Page { + id: root + + readonly property int fsModifiedState: fsModifiedStateItem.isValid ? fsModifiedStateItem.value : -1 + readonly property int systemHooksState: systemHooksStateItem.isValid ? systemHooksStateItem.value : -1 + + property bool restoreFirmwareIntegrityPressed: false + + function getSystemState() { + if (fsModifiedState === 0 && systemHooksState === 0 && modelItem.value.indexOf("Raspberry") === -1) { + //% "Supported" + return qsTrId("pagesettingscustomizationchecks_supported") + } else { + //% "No Victron Energy support" + return qsTrId("pagesettingscustomizationchecks_unsupported") + } + } + + function scaleBytes(bytes) { + if (bytes < 1024) { + return bytes + " B" + } else if (bytes < 1024 * 1024) { + return (bytes / 1024).toFixed(1) + " KB" + } else if (bytes < 1024 * 1024 * 1024) { + return (bytes / 1024 / 1024).toFixed(1) + " MB" + } else { + return (bytes / 1024 / 1024 / 1024).toFixed(1) + " GB" + } + } + + function getFsModifiedState() { + if (fsModifiedState === 0) { + //% "Ok" + return qsTrId("common_words_ok") + } else if (fsModifiedState === 1) { + //% "Modified" + return qsTrId("pagesettingscustomizationchecks_modified") + } else { + //% "Unknown: %1" + return qsTrId("pagesettingscustomizationchecks_unknown").arg(fsModifiedState) + } + } + + function getSystemHooksState() { + if (systemHooksState === 0) { + //% "No" + return qsTrId("common_words_no") + } else if (systemHooksState === 1) { + //% "No, but enable at next boot (rc.local)" + return qsTrId("pagesettingscustomizationchecks_no_but_next_boot_rc_local") + } else if (systemHooksState === 2) { + //% "No, but enable at next boot (rcS.local)" + return qsTrId("pagesettingscustomizationchecks_no_but_next_boot_rcS_local") + } else if (systemHooksState === 3) { + //% "No, but enable at next boot (rc.local and rcS.local)" + return qsTrId("pagesettingscustomizationchecks_no_but_next_boot_rc_local_rcS_local") + } else if (systemHooksState === 4) { + //% "Yes, but disable at next boot" + return qsTrId("pagesettingscustomizationchecks_yes_but_disable_next_boot") + } else if (systemHooksState === 5) { + //% "Yes (rc.local)" + return qsTrId("pagesettingscustomizationchecks_yes_rc_local") + } else if (systemHooksState === 6) { + //% "Yes (rcS.local)" + return qsTrId("pagesettingscustomizationchecks_yes_rcS_local") + } else if (systemHooksState === 7) { + //% "Yes (rc.local and rcS.local)" + return qsTrId("pagesettingscustomizationchecks_yes_rc_local_rcS_local") + } else { + //% "Unknown: %1" + return qsTrId("pagesettingscustomizationchecks_unknown").arg(systemHooksState) + } + } + + function getIsFirmwareUpToDate() { + if (firmwareOnlineAvailableBuildItem.isValid && firmwareOnlineAvailableBuildItem.value == "" && firmwareOnlineAvailableVersionItem.isValid && firmwareOnlineAvailableVersionItem.value == "") { + //% "Yes" + return qsTrId("common_words_yes") + } else if (Global.firmwareUpdate.checkingForUpdate) { + //% "Checking..." + return qsTrId("pagesettingscustomizationchecks_firmware_checking") + } else if ((firmwareStateItem.isValid ? firmwareStateItem.value : FirmwareUpdater.Idle) == FirmwareUpdater.ErrorDuringChecking) { + //% "Online check failed" + return qsTrId("pagesettingscustomizationchecks_firmware_online_check_failed") + } else if (Global.firmwareUpdate.onlineAvailableVersion == "") { + //% "Go to \"Firmware: Online update\" and check for updates" + return qsTrId("pagesettingscustomizationchecks_firmware_go_to_online_update") + } else { + //: %1 = firmware version + //% "No, %1 is available" + return qsTrId("pagesettingscustomizationchecks_firmware_no_available").arg(Global.firmwareUpdate.onlineAvailableVersion) + } + } + + function getIsFirmwareUpToDateBool() { + if (firmwareOnlineAvailableBuildItem.isValid && firmwareOnlineAvailableBuildItem.value == "" && firmwareOnlineAvailableVersionItem.isValid && firmwareOnlineAvailableVersionItem.value == "") { + return true + } else { + return false + } + } + + VeQuickItem { + id: allModificationsDisabledItem + uid: Global.systemSettings.serviceUid + "/Settings/System/CustomizationChecks/AllModificationsDisabled" + } + VeQuickItem { + id: hqSerialNumberItem + uid: Global.venusPlatform.serviceUid + "/Device/HQSerialNumber" + } + VeQuickItem { + id: modelItem + uid: Global.venusPlatform.serviceUid + "/Device/Model" + } + VeQuickItem { + id: signalKItem + uid: Global.venusPlatform.serviceUid + "/Services/SignalK/Enabled" + } + VeQuickItem { + id: nodeRedItem + uid: Global.venusPlatform.serviceUid + "/Services/NodeRed/Mode" + } + VeQuickItem { + id: firmwareOnlineAvailableBuildItem + uid: Global.venusPlatform.serviceUid + "/Firmware/Online/AvailableVersion" + } + VeQuickItem { + id: firmwareOnlineAvailableVersionItem + uid: Global.venusPlatform.serviceUid + "/Firmware/Online/AvailableBuild" + } + VeQuickItem { + id: firmwareOnlineCheckItem + uid: Global.venusPlatform.serviceUid + "/Firmware/Online/Check" + } + VeQuickItem { + id: firmwareProgressItem + uid: Global.venusPlatform.serviceUid + "/Firmware/Progress" + } + VeQuickItem { + id: firmwareStateItem + uid: Global.venusPlatform.serviceUid + "/Firmware/State" + } + VeQuickItem { + id: dataPartitionFreeSpaceItem + uid: Global.venusPlatform.serviceUid + "/CustomizationChecks/DataPartitionFreeSpace" + } + VeQuickItem { + id: forceFirmwareReinstallItem + uid: Global.venusPlatform.serviceUid + "/CustomizationChecks/ForceFirmwareReinstall" + } + VeQuickItem { + id: sshKeyForRootPresentItem + uid: Global.venusPlatform.serviceUid + "/CustomizationChecks/SshKeyForRootPresent" + } + VeQuickItem { + id: startCustomizationCheckItem + uid: Global.venusPlatform.serviceUid + "/CustomizationChecks/StartCheck" + } + VeQuickItem { + id: fsModifiedStateItem + uid: Global.venusPlatform.serviceUid + "/CustomizationChecks/FsModifiedState" + } + VeQuickItem { + id: systemHooksStateItem + uid: Global.venusPlatform.serviceUid + "/CustomizationChecks/SystemHooksState" + } + + + GradientListView { + id: supportAndTroubleshootListView + + model: ObjectModel { + + PrimaryListLabel { + //% "Customization Checks" + text: qsTrId("pagesettingscustomizationchecks_customization_checks") + } + + ListText { + //% "System state" + text: qsTrId("pagesettingscustomizationchecks_system_state") + secondaryText: getSystemState() + secondaryLabel.color: fsModifiedState === 0 && systemHooksState === 0 && modelItem.value.indexOf("Raspberry") === -1 ? Theme.color_green : Theme.color_red + } + + ListText { + //% "Device model" + text: qsTrId("pagesettingscustomizationchecks_device_model") + secondaryText: modelItem.value || "" + secondaryLabel.color: modelItem.isValid && modelItem.value.indexOf("Raspberry") === -1 ? Theme.color_green : Theme.color_red + } + + ListText { + // Value is missing on older devices, therefore do not use colors on that + //% "HQ serial number" + text: qsTrId("pagesettingscustomizationchecks_hq_serial_number") + secondaryText: hqSerialNumberItem.value + allowed: defaultAllowed && hqSerialNumberItem.value != "" + } + + ListText { + //% "Data partition free space" + text: qsTrId("pagesettingscustomizationchecks_data_free_space") + secondaryText: scaleBytes(dataPartitionFreeSpaceItem.value) + secondaryLabel.color: { + if (dataPartitionFreeSpaceItem.value < 1024 * 1024 * 10) { + return Theme.color_red; + } else { + return Theme.color_green; + } + } + } + + ListText { + //% "Modifications loaded at boot" + text: qsTrId("pagesettingscustomizationchecks_modifications_at_boot") + secondaryText: getSystemHooksState() + secondaryLabel.color: systemHooksState === 0 ? Theme.color_green : systemHooksState < 4 ? Theme.color_orange : Theme.color_red + } + + ListText { + //% "Firmware integrity" + text: qsTrId("pagesettingscustomizationchecks_firmware_integrity") + secondaryText: getFsModifiedState() + secondaryLabel.color: fsModifiedState === 0 ? Theme.color_green : Theme.color_red + } + + ListText { + //% "Latest firmware version installed?" + text: qsTrId("pagesettingscustomizationchecks_latest_firmware_installed") + secondaryText: getIsFirmwareUpToDate() + secondaryLabel.color: getIsFirmwareUpToDateBool() ? Theme.color_green : Theme.color_red + } + + ListText { + //% "Installed firmware version" + text: qsTrId("pagesettingscustomizationchecks_installed_firmware_version") + secondaryText: FirmwareVersion.versionText(dataItem.value, "venus") + dataItem.uid: Global.venusPlatform.serviceUid + "/Firmware/Installed/Version" + } + + ListText { + //% "Installed build date/time" + text: qsTrId("pagesettingscustomizationchecks_build_date_time") + dataItem.uid: Global.venusPlatform.serviceUid + "/Firmware/Installed/Build" + } + + ListText { + //% "Installed image type" + text: qsTrId("pagesettingscustomizationchecks_installed_image_type") + secondaryText: signalKItem.isValid || nodeRedItem.isValid ? qsTrId("settings_firmware_large") : qsTrId("settings_firmware_normal") + } + + ListText { + //% "User SSH key present" + text: qsTrId("pagesettingscustomizationchecks_user_ssh_key_present") + secondaryText: sshKeyForRootPresentItem.value === 1 ? qsTrId("common_words_yes") : qsTrId("common_words_no") + } + + PrimaryListLabel { + //% "Tools to normalize the system" + text: qsTrId("pagesettingscustomizationchecks_tools_to_normalize_system") + } + + ListSwitch { + id: disableAllModifications + //% "Disable all modifications" + text: qsTrId("pagesettingscustomizationchecks_disable_all_modifications") + /* + Venus Platform + - Save the current state of Signal K and Node-RED + - Disable (service) and lock (GUI buttons) Signal K + - Disable (service) and lock (GUI buttons) Node-RED + - Disable rc.local by renaming it to rc.local.disabled + - Disable rcS.local by renaming it to rcS.local.disabled + */ + dataItem.uid: Global.systemSettings.serviceUid + "/Settings/System/CustomizationChecks/AllModificationsDisabled" + + bottomContentChildren: [ + PrimaryListLabel { + width: Math.min(implicitWidth, disableAllModifications.maximumContentWidth) + topPadding: 0 + bottomPadding: 0 + color: Theme.color_font_secondary + //% "This disables all modifications, including SignalK and Node-RED." + text: qsTrId("pagesettingscustomizationchecks_disable_all_modifications_description") + } + ] + + onCheckedChanged: { + // Show the dialog only if + // - restore integrity button was not pressed + // - it doesn't match the current state + if (!restoreFirmwareIntegrityPressed && + ((!disableAllModifications.checked && systemHooksState < 4) || (disableAllModifications.checked && systemHooksState >= 4))) { + Global.dialogLayer.open(askForRebootDialogComponent) + } else { + startCustomizationCheckItem.setValue(1) + } + } + + Component { + id: askForRebootDialogComponent + + ModalWarningDialog { + //% "To apply changes a reboot is needed.
Press 'OK' to reboot now." + title: qsTrId("pagesettingscustomizationchecks_reboot_needed") + dialogDoneOptions: VenusOS.ModalDialog_DoneOptions_OkAndCancel + onClosed: { + if (result === T.Dialog.Accepted) { + Global.venusPlatform.reboot() + Qt.callLater(Global.dialogLayer.open, rebootingDialogComponent) + } else { + startCustomizationCheckItem.setValue(1) + } + } + } + } + } + + ListButton { + //% "Reboot now to apply changes" + text: qsTrId("pagesettingscustomizationchecks_reboot_now_to_apply_changes") + //% "Reboot now" + button.text: qsTrId("settings_reboot_now") + writeAccessLevel: VenusOS.User_AccessType_User + allowed: defaultAllowed && systemHooksState >= 1 && systemHooksState <= 4 + onClicked: Global.dialogLayer.open(confirmRebootDialogComponent) + + Component { + id: confirmRebootDialogComponent + + ModalWarningDialog { + //% "Press 'OK' to reboot" + title: qsTrId("press_ok_to_reboot") + dialogDoneOptions: VenusOS.ModalDialog_DoneOptions_OkAndCancel + onClosed: { + if (result === T.Dialog.Accepted) { + Global.venusPlatform.reboot() + Qt.callLater(Global.dialogLayer.open, rebootingDialogComponent) + } + } + } + } + + Component { + id: rebootingDialogComponent + + ModalWarningDialog { + title: BackendConnection.type === BackendConnection.DBusSource + //% "Rebooting..." + ? qsTrId("dialoglayer_rebooting") + //% "Device has been rebooted." + : qsTrId("dialoglayer_rebooted") + + // On device, dialog cannot be dismissed; just wait until device is rebooted. + dialogDoneOptions: VenusOS.ModalDialog_DoneOptions_OkOnly + footer.enabled: BackendConnection.type !== BackendConnection.DBusSource + footer.opacity: footer.enabled ? 1 : 0 + } + } + } + + ListButton { + //% "Firmware: Restore clean state" + text: qsTrId("pagesettingscustomizationchecks_firmware_restore_clean_state") + button.text: { + if (Global.firmwareUpdate.state === FirmwareUpdater.DownloadingAndInstalling) { + if (firmwareProgressItem.value) { + //: Firmware update firmwareProgressItem. %1 = firmware version, %2 = current update progress + //% "Installing %1 %2%" + qsTrId("settings_firmware_online_installing_progress").arg(Global.firmwareUpdate.onlineAvailableVersion).arg(firmwareProgressItem.value) + } + //: %1 = firmware version + //% "Installing %1..." + qsTrId("settings_firmware_online_installing").arg(Global.firmwareUpdate.onlineAvailableVersion) + } else { + //% "Press to restore" + qsTrId("pagesettingscustomizationchecks_press_to_restore") + } + } + writeAccessLevel: VenusOS.User_AccessType_User + onClicked: Global.dialogLayer.open(confirmReinstallDialogComponent) + + Component { + id: confirmReinstallDialogComponent + + ModalWarningDialog { + //% "This will disable all modifications, download and reinstall the latest available firmware.
Internet connectivity is required.
Press 'OK' to continue." + title: qsTrId("pagesettingscustomizationchecks_firmware_restore_clean_state_description") + dialogDoneOptions: VenusOS.ModalDialog_DoneOptions_OkAndCancel + onClosed: { + if (result === T.Dialog.Accepted) { + restoreFirmwareIntegrityPressed = true + allModificationsDisabledItem.setValue(1) + forceFirmwareReinstallItem.setValue(1) + } + } + } + } + } + + ListNavigation { + //% "Firmware: Online update" + text: qsTrId("pagesettingscustomizationchecks_firmware_online_update") + onClicked: { + Global.pageManager.pushPage("/pages/settings/PageSettingsFirmwareOnline.qml", { title: text }) + } + } + + ListNavigation { + //% "Firmware: Install from SD/USB" + text: qsTrId("pagesettingscustomizationchecks_firmware_install_from_sd_usb") + onClicked: { + Global.pageManager.pushPage("/pages/settings/PageSettingsFirmwareOffline.qml", { title: text }) + } + } + } + } + + Component.onCompleted: { + // Check for updates + if (firmwareOnlineCheckItem.isValid && firmwareOnlineCheckItem.value === 0) { + Global.firmwareUpdate.checkForUpdate(VenusOS.Firmware_UpdateType_Online) + } + + // Run system integrity check + startCustomizationCheckItem.setValue(1) + } +} diff --git a/pages/settings/PageSettingsGeneral.qml b/pages/settings/PageSettingsGeneral.qml index af18b73cb..73511c590 100644 --- a/pages/settings/PageSettingsGeneral.qml +++ b/pages/settings/PageSettingsGeneral.qml @@ -183,6 +183,50 @@ Page { } } } + + SettingsListHeader { } + + ListNavigation { + //% "Useful Links" + text: qsTrId("pagesettingsgeneral_useful_links") + onClicked: Global.pageManager.pushPage("/pages/settings/PageSettingsUsefulLinks.qml", {"title": text}) + } + + ListNavigation { + //% "Customization Checks" + text: qsTrId("pagesettingsgeneral_customization_checks") + secondaryText: getSystemState() + secondaryLabel.color: fsModifiedState === 0 && systemHooksState === 0 && modelItem.value.indexOf("Raspberry") === -1 ? Theme.color_font_primary : Theme.color_red + onClicked: Global.pageManager.pushPage("/pages/settings/PageSettingsCustomizationChecks.qml", {"title": text}) + + readonly property int fsModifiedState: fsModifiedStateItem.isValid ? fsModifiedStateItem.value : -1 + readonly property int systemHooksState: systemHooksStateItem.isValid ? systemHooksStateItem.value : -1 + + property bool restoreFirmwareIntegrityPressed: false + + function getSystemState() { + if (fsModifiedState === 0 && systemHooksState === 0 && modelItem.value.indexOf("Raspberry") === -1) { + //% "Supported" + return qsTrId("pagesettingscustomizationchecks_supported") + } else { + //% "No Victron Energy support" + return qsTrId("pagesettingscustomizationchecks_unsupported") + } + } + + VeQuickItem { + id: fsModifiedStateItem + uid: Global.venusPlatform.serviceUid + "/CustomizationChecks/FsModifiedState" + } + VeQuickItem { + id: systemHooksStateItem + uid: Global.venusPlatform.serviceUid + "/CustomizationChecks/SystemHooksState" + } + VeQuickItem { + id: modelItem + uid: Global.venusPlatform.serviceUid + "/Device/Model" + } + } } } } diff --git a/pages/settings/PageSettingsIntegrations.qml b/pages/settings/PageSettingsIntegrations.qml index 2f6311d69..d3cd1078b 100644 --- a/pages/settings/PageSettingsIntegrations.qml +++ b/pages/settings/PageSettingsIntegrations.qml @@ -9,6 +9,13 @@ import Victron.VenusOS Page { id: root + readonly property bool allModificationsDisabled: allModificationsDisabledItem.isValid && allModificationsDisabledItem.value === 1 + + VeQuickItem { + id: allModificationsDisabledItem + uid: Global.systemSettings.serviceUid + "/Settings/System/CustomizationChecks/AllModificationsDisabled" + } + GradientListView { id: settingsListView @@ -168,6 +175,12 @@ Page { text: qsTrId("settings_large_features_not_offically_supported") } + PrimaryListLabel { + //% "Venus OS Large features are disabled, since \"Disable all modifications\" under \"Settings -> General -> Customization checks\" is enabled." + text: qsTrId("settings_large_features_currently_disabled") + allowed: root.allModificationsDisabled + } + ListSwitch { id: signalk @@ -175,6 +188,7 @@ Page { text: qsTrId("settings_large_signal_k") dataItem.uid: Global.venusPlatform.serviceUid + "/Services/SignalK/Enabled" allowed: dataItem.isValid + enabled: userHasWriteAccess && !root.allModificationsDisabled } PrimaryListLabel { diff --git a/pages/settings/PageSettingsNodeRed.qml b/pages/settings/PageSettingsNodeRed.qml index 4a4e98b41..95f4b40ab 100644 --- a/pages/settings/PageSettingsNodeRed.qml +++ b/pages/settings/PageSettingsNodeRed.qml @@ -9,14 +9,28 @@ import Victron.VenusOS Page { id: root + readonly property bool allModificationsDisabled: allModificationsDisabledItem.isValid && allModificationsDisabledItem.value === 1 + + VeQuickItem { + id: allModificationsDisabledItem + uid: Global.systemSettings.serviceUid + "/Settings/System/CustomizationChecks/AllModificationsDisabled" + } + GradientListView { model: ObjectModel { + PrimaryListLabel { + //% "Venus OS Large features are disabled, since \"Disable all modifications\" under \"Settings -> Support & Troubleshoot -> Customization checks\" is enabled." + text: qsTrId("settings_large_features_currently_disabled") + allowed: root.allModificationsDisabled + } + ListRadioButtonGroup { id: nodered text: qsTrId("settings_large_node_red") dataItem.uid: Global.venusPlatform.serviceUid + "/Services/NodeRed/Mode" allowed: dataItem.isValid + enabled: userHasWriteAccess && !root.allModificationsDisabled optionModel: [ { display: CommonWords.disabled, value: VenusOS.NodeRed_Mode_Disabled }, { display: CommonWords.enabled, value: VenusOS.NodeRed_Mode_Enabled }, diff --git a/pages/settings/PageSettingsUsefulLinks.qml b/pages/settings/PageSettingsUsefulLinks.qml new file mode 100644 index 000000000..6c2c2c545 --- /dev/null +++ b/pages/settings/PageSettingsUsefulLinks.qml @@ -0,0 +1,45 @@ +/* +** Copyright (C) 2025 Victron Energy B.V. +** See LICENSE.txt for license information. +*/ + +import QtQuick +import Victron.VenusOS + +Page { + id: root + + GradientListView { + model: ObjectModel { + ListLink { + //% "Warranty check" + text: qsTrId("settings_support_links_warranty") + url: "https://www.victronenergy.com/support" + } + + ListLink { + //% "FAQ" + text: qsTrId("settings_support_links_faq") + url: "https://www.victronenergy.com/media/pg/Energy_Storage_System/en/faq.html" + } + + ListLink { + //% "How to troubleshoot" + text: qsTrId("settings_support_links_troubleshoot") + url: "https://www.victronenergy.com/media/pg/Venus_GX/en/troubleshooting.html" + } + + ListLink { + //% "Forum" + text: qsTrId("settings_support_links_forum") + url: "https://community.victronenergy.com/" + } + + ListLink { + //% "Find a local distributor" + text: qsTrId("settings_support_links_distributor") + url: "https://www.victronenergy.com/where-to-buy" + } + } + } +}