Mercurial > ~darius > hgwebdir.cgi > OverviewJD
changeset 0:57ffb39f29d4
First commit of new carousel page to allow battery charging current to
be adjusted.
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Mon, 13 Dec 2021 23:05:38 +1030 |
parents | |
children | 594ba407689b |
files | OverviewJD.qml OverviewMobile-to-JD.diff OverviewMobile.qml README install.sh main.qml main.qml.diff main.qml.orig |
diffstat | 8 files changed, 2258 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OverviewJD.qml Mon Dec 13 23:05:38 2021 +1030 @@ -0,0 +1,698 @@ +// Modified version of OverviewMobile.qml +import QtQuick 1.1 +import com.victron.velib 1.0 +import "utils.js" as Utils + +OverviewPage { + id: root + + property variant sys: theSystem + property string settingsBindPreffix: "com.victronenergy.settings" + property string pumpBindPreffix: "com.victronenergy.pump.startstop0" + property variant activeNotifications: NotificationCenter.notifications.filter( + function isActive(obj) { return obj.active} ) + property string noAdjustableByDmc: qsTr("This setting is disabled when a Digital Multi Control " + + "is connected. If it was recently disconnected execute " + + "\"Redetect system\" that is avalible on the inverter menu page.") + property string noAdjustableByBms: qsTr("This setting is disabled when a VE.Bus BMS " + + "is connected. If it was recently disconnected execute " + + "\"Redetect system\" that is avalible on the inverter menu page.") + property string noAdjustableTextByConfig: qsTr("This setting is disabled. " + + "Possible reasons are \"Overruled by remote\" is not enabled or " + + "an assistant is preventing the adjustment. Please, check " + + "the inverter configuration with VEConfigure.") + property int numberOfMultis: 0 + property string vebusPrefix: "" + + // Keeps track of which button on the bottom row is active + property int buttonIndex: 0 + + title: qsTr("Java Drive") + + Component.onCompleted: discoverMulti() + + ListView { + id: pwColumn + + property int tilesCount: solarTile.visible || dcSystem.visible ? 3 : 2 + property int tileHeight: Math.ceil(height / tilesCount) + interactive: false // static tiles + + width: 136 + anchors { + left: parent.left + top: parent.top; + bottom: acModeButton.top; + } + + model: VisualItemModel { + Tile { + width: pwColumn.width + height: visible ? pwColumn.tileHeight : 0 + title: qsTr("AC INPUT") + color: "#82acde" + visible: !dcSystem.visible || !solarTile.visible + values: [ + TileText { + text: sys.acInput.power.uiText + font.pixelSize: 25 + }, + TileText { + property VBusItem inV1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/ActiveIn/L1/V"); unit: "V" } + text: inV1.format(1) + font.pixelSize: 15 + }, + TileText { + property VBusItem inI1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/ActiveIn/L1/I"); unit: "A" } + text: inI1.format(1) + font.pixelSize: 15 + }, + TileText { + property VBusItem inF1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/ActiveIn/L1/F"); unit: "Hz" } + text: inF1.format(0) + font.pixelSize: 15 + } + ] + } + + TileAcPower { + width: pwColumn.width + height: visible ? pwColumn.tileHeight : 0 + title: qsTr("AC LOADS") + color: "#e68e8a" + values: [ + TileText { + text: sys.acLoad.power.uiText + font.pixelSize: 25 + }, + TileText { + property VBusItem outV1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/Out/L1/V"); unit: "V" } + text: outV1.format(1) + font.pixelSize: 15 + }, + TileText { + property VBusItem outI1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/Out/L1/I"); unit: "A" } + text: outI1.format(1) + font.pixelSize: 15 + }, + TileText { + property VBusItem outF1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/Out/L1/F"); unit: "Hz" } + text: outF1.format(0) + font.pixelSize: 15 + } + ] + } + + Tile { + id: solarTile + width: pwColumn.width + height: visible ? pwColumn.tileHeight : 0 + title: qsTr("PV CHARGER") + color: "#2cc36b" + visible : sys.pvCharger.power.valid + + values: [ + TileText { + font.pixelSize: 30 + text: sys.pvCharger.power.uiText + } + ] + } + Tile { + id: dcSystem + width: pwColumn.width + height: visible ? pwColumn.tileHeight : 0 + title: qsTr("DC SYSTEM") + color: "#16a085" + visible : hasDcSys.value === 1 + + VBusItem { + id: hasDcSys + bind: Utils.path(settingsBindPreffix, "/Settings/SystemSetup/HasDcSystem") + } + + values: [ + TileText { + font.pixelSize: 30 + text: sys.dcSystem.power.format(0) + }, + TileText { + text: !sys.dcSystem.power.valid ? "---" : + sys.dcSystem.power.value < 0 ? qsTr("to battery") : qsTr("from battery") + } + ] + } + } + } + + Tile { + id: logoTile + + color: "#575748" + height: 120 + anchors { + left: pwColumn.right + right: tanksColum.left + top: parent.top + } + + MbIcon { + x: 1 + y: 1 + // see below, so the svg instead of a png if there is a 1x1 image + visible: customImage.sourceSize.width === 1 && customImage.sourceSize.height === 1 + iconId: "mobile-builder-logo-svg" + } + + // The uploaded png, the default is a 1x1 transparent pixel now. + Image { + id: customImage + source: "image://theme/mobile-builder-logo" + anchors.centerIn: parent + } + } + + Tile { + id: batteryTile + height: 112 + title: qsTr("BATTERY") + anchors { + left: pwColumn.right + right: stateTile.left + top: logoTile.bottom + bottom: acModeButton.top + } + + values: [ + TileText { + text: sys.battery.soc.absFormat(0) + font.pixelSize: 30 + height: 32 + }, + TileText { + text: { + if (!sys.battery.state.valid) + return "---" + switch(sys.battery.state.value) { + case sys.batteryStateIdle: return qsTr("idle") + case sys.batteryStateCharging : return qsTr("charging") + case sys.batteryStateDischarging : return qsTr("discharging") + } + } + }, + TileText { + text: sys.battery.power.absFormat(0) + }, + TileText { + text: sys.battery.voltage.format(1) + " " + sys.battery.current.format(1) + } + ] + } + + Tile { + id: stateTile + + width: 104 + title: qsTr("STATUS") + color: "#4789d0" + + anchors { + right: tanksColum.left + top: logoTile.bottom + bottom: acModeButton.top + } + + Timer { + id: wallClock + + running: true + repeat: true + interval: 1000 + triggeredOnStart: true + onTriggered: time = Qt.formatDateTime(new Date(), "hh:mm") + + property string time + } + + values: [ + TileText { + id: systemTile + text: wallClock.time + font.pixelSize: 30 + }, + TileText { + property VeQuickItem gpsService: VeQuickItem { uid: "dbus/com.victronenergy.system/GpsService" } + property VeQuickItem speed: VeQuickItem { uid: Utils.path("dbus/", gpsService.value, "/Speed") } + property VeQuickItem speedUnit: VeQuickItem { uid: "dbus/com.victronenergy.settings/Settings/Gps/SpeedUnit" } + + text: speed.value === undefined ? "" : getValue() + visible: speed.value !== undefined && speedUnit.value !== undefined + + function getValue() + { + if (speedUnit.value === "km/h") + return (speed.value * 3.6).toFixed(1) + speedUnit.value + if (speedUnit.value === "mph") + return (speed.value * 2.236936).toFixed(1) + speedUnit.value + if (speedUnit.value === "kt") + return (speed.value * (3600/1852)).toFixed(1) + speedUnit.value + return speed.value.toFixed(2) + "m/s" + } + }, + Marquee { + text: notificationText() + width: stateTile.width + interval: 100 + fontSize: 13 + } + ] + } + + ListView { + id: tanksColum + + property int tileHeight: Math.ceil(height / Math.max(count, 2)) + width: 134 + interactive: false // static tiles + model: TankModel { id: tankModel } + delegate: TileTank { + // Without an intermediate assignment this will trigger a binding loop warning. + property variant theService: DBusServices.get(buddy.id) + service: theService + width: tanksColum.width + height: tanksColum.tileHeight + pumpBindPrefix: root.pumpBindPreffix + compact: tankModel.rowCount > (pumpButton.pumpEnabled ? 4 : 5) + Connections { + target: scrollTimer + onTriggered: doScroll() + } + } + + anchors { + top: root.top + bottom: pumpButton.pumpEnabled ? acModeButton.top : acModeButton.bottom + right: root.right + } + + // Synchronise tank name text scroll start + Timer { + id: scrollTimer + interval: 15000 + repeat: true + running: root.active && tankModel.rowCount > 4 + } + + Tile { + title: qsTr("TANKS") + anchors.fill: parent + values: TileText { + text: qsTr("No tanks found") + width: parent.width + wrapMode: Text.WordWrap + } + z: -1 + } + } + + Keys.forwardTo: [keyHandler] + + Item { + id: keyHandler + Keys.onLeftPressed: { + if (buttonIndex > 0) + buttonIndex-- + + event.accepted = true + } + + Keys.onRightPressed: { + if (buttonIndex < (pumpButton.pumpEnabled ? 3 : 2)) + buttonIndex++ + + event.accepted = true + } + } + + MouseArea { + anchors.fill: parent + enabled: parent.active + onPressed: mouse.accepted = acCurrentButton.expanded + onClicked: acCurrentButton.cancel() + } + + TileSpinBox { + id: acCurrentButton + + anchors.bottom: parent.bottom + anchors.left: parent.left + isCurrentItem: (buttonIndex == 0) + focus: root.active && isCurrentItem + + bind: Utils.path(vebusPrefix, "/Ac/ActiveIn/CurrentLimit") + title: qsTr("AC CURRENT LIMIT") + color: containsMouse && !editMode ? "#d3d3d3" : "#A8A8A8" + width: pumpButton.pumpEnabled ? 160 : 173 + fontPixelSize: 14 + unit: "A" + readOnly: currentLimitIsAdjustable.value !== 1 || numberOfMultis > 1 + buttonColor: "#979797" + + VBusItem { id: currentLimitIsAdjustable; bind: Utils.path(vebusPrefix, "/Ac/ActiveIn/CurrentLimitIsAdjustable") } + + Keys.onSpacePressed: showErrorToast(event) + + function editIsAllowed() { + if (numberOfMultis > 1) { + toast.createToast(qsTr("It is not possible to change this setting when there are more than one inverter connected."), 5000) + return false + } + + if (currentLimitIsAdjustable.value === 0) { + if (dmc.valid) { + toast.createToast(noAdjustableByDmc, 5000) + return false + } + if (bms.valid) { + toast.createToast(noAdjustableByBms, 5000) + return false + } + if (!dmc.valid && !bms.valid) { + toast.createToast(noAdjustableTextByConfig, 5000) + return false + } + } + + return true + } + + function showErrorToast(event) { + editIsAllowed() + event.accepted = true + } + } + + Tile { + id: acModeButton + anchors.left: acCurrentButton.right + anchors.bottom: parent.bottom + property variant texts: { 4: qsTr("OFF"), 3: qsTr("ON"), 1: qsTr("CHARGER ONLY") } + property int value: mode.valid ? mode.value : 3 + property int shownValue: applyAnimation2.running ? applyAnimation2.pendingValue : value + + isCurrentItem: (buttonIndex == 1) + focus: root.active && isCurrentItem + + editable: true + readOnly: !modeIsAdjustable.valid || modeIsAdjustable.value !== 1 || numberOfMultis > 1 + width: pumpButton.pumpEnabled ? 160 : 173 + height: 45 + color: acModeButtonMouseArea.containsPressed ? "#d3d3d3" : "#A8A8A8" + title: qsTr("AC MODE") + + values: [ + TileText { + text: modeIsAdjustable.valid && numberOfMultis === 1 ? qsTr("%1").arg(acModeButton.texts[acModeButton.shownValue]) : qsTr("NOT AVAILABLE") + } + ] + + VBusItem { id: mode; bind: Utils.path(vebusPrefix, "/Mode") } + VBusItem { id: modeIsAdjustable; bind: Utils.path(vebusPrefix,"/ModeIsAdjustable") } + + Keys.onSpacePressed: edit() + + function edit() { + if (!mode.valid) + return + + if (numberOfMultis > 1) { + toast.createToast(qsTr("It is not possible to change this setting when there are more than one inverter connected."), 5000) + return + } + + if (modeIsAdjustable.value === 0) { + if (dmc.valid) + toast.createToast(noAdjustableByDmc, 5000) + if (bms.valid) + toast.createToast(noAdjustableByBms, 5000) + if (!dmc.valid && !bms.valid) + toast.createToast(noAdjustableTextByConfig, 5000) + return + } + + switch (shownValue) { + case 4: + applyAnimation2.pendingValue = 3 + break; + case 3: + applyAnimation2.pendingValue = 1 + break; + case 1: + applyAnimation2.pendingValue = 4 + break; + } + + applyAnimation2.restart() + } + + MouseArea { + id: acModeButtonMouseArea + anchors.fill: parent + property bool containsPressed: containsMouse && pressed + onClicked: { + buttonIndex = 1 + parent.edit() + } + } + + Rectangle { + id: timerRect2 + height: 2 + width: acModeButton.width * 0.8 + visible: applyAnimation2.running + anchors { + bottom: parent.bottom; bottomMargin: 5 + horizontalCenter: parent.horizontalCenter + } + } + + SequentialAnimation { + id: applyAnimation2 + + property int pendingValue + + NumberAnimation { + target: timerRect2 + property: "width" + from: 0 + to: acModeButton.width * 0.8 + duration: 3000 + } + + ColorAnimation { + target: acModeButton + property: "color" + from: "#A8A8A8" + to: "#4789d0" + duration: 200 + } + + ColorAnimation { + target: acModeButton + property: "color" + from: "#4789d0" + to: "#A8A8A8" + duration: 200 + } + PropertyAction { + target: timerRect2 + property: "width" + value: 0 + } + + ScriptAction { script: mode.setValue(applyAnimation2.pendingValue) } + + PauseAnimation { duration: 1000 } + } + } + + TileSpinBox { + id: battCurrentButton + + anchors.bottom: parent.bottom + anchors.left: acModeButton.right + isCurrentItem: (buttonIndex == 2) + focus: root.active && isCurrentItem + + bind: Utils.path(vebusPrefix, "/Dc/0/MaxChargeCurrent") + title: qsTr("BAT CURR LIMIT") + color: containsMouse && !editMode ? "#d3d3d3" : "#A8A8A8" + width: 134 + fontPixelSize: 14 + unit: "A" + readOnly: false + editable: true + buttonColor: "#979797" + + Keys.onSpacePressed: showErrorToast(event) + + function showErrorToast(event) { + editIsAllowed() + event.accepted = true + } + } + + Tile { + id: pumpButton + + anchors.left: battCurrentButton.right + anchors.bottom: parent.bottom + + property variant texts: [ qsTr("AUTO"), qsTr("ON"), qsTr("OFF")] + property int value: 0 + property bool reset: false + property bool pumpEnabled: pumpRelay.value === 3 + + show: pumpEnabled + isCurrentItem: (buttonIndex == 3) + focus: root.active && isCurrentItem + + title: qsTr("PUMP") + width: show ? 160 : 0 + height: 45 + editable: true + readOnly: false + color: pumpButtonMouseArea.containsPressed ? "#d3d3d3" : "#A8A8A8" + + VBusItem { id: pump; bind: Utils.path(settingsBindPreffix, "/Settings/Pump0/Mode") } + VBusItem { id: pumpRelay; bind: Utils.path(settingsBindPreffix, "/Settings/Relay/Function") } + + values: [ + TileText { + text: pumpButton.pumpEnabled ? qsTr("%1").arg(pumpButton.texts[pumpButton.value]) : qsTr("DISABLED") + } + ] + + Keys.onSpacePressed: edit() + + function edit() { + if (!pumpEnabled) { + toast.createToast(qsTr("Pump functionality is not enabled. To enable it go to the relay settings page and set function to \"Tank pump\""), 5000) + return + } + + reset = true + applyAnimation.restart() + reset = false + + if (value < 2) + value++ + else + value = 0 + } + + MouseArea { + id: pumpButtonMouseArea + property bool containsPressed: containsMouse && pressed + anchors.fill: parent + onClicked: { + buttonIndex = 2 + parent.edit() + } + } + + Rectangle { + id: timerRect + height: 2 + width: pumpButton.width * 0.8 + visible: applyAnimation.running + anchors { + bottom: parent.bottom; bottomMargin: 5 + horizontalCenter: parent.horizontalCenter + } + } + + SequentialAnimation { + id: applyAnimation + alwaysRunToEnd: false + NumberAnimation { + target: timerRect + property: "width" + from: 0 + to: pumpButton.width * 0.8 + duration: 3000 + } + + ColorAnimation { + target: pumpButton + property: "color" + from: "#A8A8A8" + to: "#4789d0" + duration: 200 + } + + ColorAnimation { + target: pumpButton + property: "color" + from: "#4789d0" + to: "#A8A8A8" + duration: 200 + } + PropertyAction { + target: timerRect + property: "width" + value: 0 + } + // Do not set value if the animation is restarted by user pressing the button + // to move between options + onCompleted: if (!pumpButton.reset) pump.setValue(pumpButton.value) + } + } + + // When new service is found check if is a tank sensor + Connections { + target: DBusServices + onDbusServiceFound: addService(service) + } + + function addService(service) + { + if (service.type === DBusService.DBUS_SERVICE_MULTI) { + numberOfMultis++ + if (vebusPrefix === "") + vebusPrefix = service.name; + } + } + + // Check available services to find tank sesnsors + function discoverMulti() + { + for (var i = 0; i < DBusServices.count; i++) { + if (DBusServices.at(i).type === DBusService.DBUS_SERVICE_MULTI) { + addService(DBusServices.at(i)) + } + } + } + + function notificationText() + { + if (activeNotifications.length === 0) + return qsTr("no alarms") + + var descr = [] + for (var n = 0; n < activeNotifications.length; n++) { + var notification = activeNotifications[n]; + + var text = notification.serviceName + " - " + notification.description; + if (notification.value !== "" ) + text += ": " + notification.value + + descr.push(text) + } + + return descr.join(" | ") + } + + VBusItem { id: dmc; bind: Utils.path(vebusPrefix, "/Devices/Dmc/Version") } + VBusItem { id: bms; bind: Utils.path(vebusPrefix, "/Devices/Bms/Version") } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OverviewMobile-to-JD.diff Mon Dec 13 23:05:38 2021 +1030 @@ -0,0 +1,122 @@ +--- OverviewMobile.qml 2021-12-13 22:43:39.000000000 +1030 ++++ OverviewJD.qml 2021-12-13 22:43:19.000000000 +1030 +@@ -1,3 +1,4 @@ ++// Modified version of OverviewMobile.qml + import QtQuick 1.1 + import com.victron.velib 1.0 + import "utils.js" as Utils +@@ -26,7 +27,7 @@ + // Keeps track of which button on the bottom row is active + property int buttonIndex: 0 + +- title: qsTr("Mobile") ++ title: qsTr("Java Drive") + + Component.onCompleted: discoverMulti() + +@@ -54,9 +55,23 @@ + values: [ + TileText { + text: sys.acInput.power.uiText +- font.pixelSize: 30 ++ font.pixelSize: 25 ++ }, ++ TileText { ++ property VBusItem inV1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/ActiveIn/L1/V"); unit: "V" } ++ text: inV1.format(1) ++ font.pixelSize: 15 ++ }, ++ TileText { ++ property VBusItem inI1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/ActiveIn/L1/I"); unit: "A" } ++ text: inI1.format(1) ++ font.pixelSize: 15 ++ }, ++ TileText { ++ property VBusItem inF1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/ActiveIn/L1/F"); unit: "Hz" } ++ text: inF1.format(0) ++ font.pixelSize: 15 + } +- + ] + } + +@@ -68,7 +83,22 @@ + values: [ + TileText { + text: sys.acLoad.power.uiText +- font.pixelSize: 30 ++ font.pixelSize: 25 ++ }, ++ TileText { ++ property VBusItem outV1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/Out/L1/V"); unit: "V" } ++ text: outV1.format(1) ++ font.pixelSize: 15 ++ }, ++ TileText { ++ property VBusItem outI1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/Out/L1/I"); unit: "A" } ++ text: outI1.format(1) ++ font.pixelSize: 15 ++ }, ++ TileText { ++ property VBusItem outF1: VBusItem { bind: Utils.path(sys.vebusPrefix, "/Ac/Out/L1/F"); unit: "Hz" } ++ text: outF1.format(0) ++ font.pixelSize: 15 + } + ] + } +@@ -297,7 +327,7 @@ + } + + Keys.onRightPressed: { +- if (buttonIndex < (pumpButton.pumpEnabled ? 2 : 1)) ++ if (buttonIndex < (pumpButton.pumpEnabled ? 3 : 2)) + buttonIndex++ + + event.accepted = true +@@ -486,10 +516,36 @@ + } + } + ++ TileSpinBox { ++ id: battCurrentButton ++ ++ anchors.bottom: parent.bottom ++ anchors.left: acModeButton.right ++ isCurrentItem: (buttonIndex == 2) ++ focus: root.active && isCurrentItem ++ ++ bind: Utils.path(vebusPrefix, "/Dc/0/MaxChargeCurrent") ++ title: qsTr("BAT CURR LIMIT") ++ color: containsMouse && !editMode ? "#d3d3d3" : "#A8A8A8" ++ width: 134 ++ fontPixelSize: 14 ++ unit: "A" ++ readOnly: false ++ editable: true ++ buttonColor: "#979797" ++ ++ Keys.onSpacePressed: showErrorToast(event) ++ ++ function showErrorToast(event) { ++ editIsAllowed() ++ event.accepted = true ++ } ++ } ++ + Tile { + id: pumpButton + +- anchors.left: acModeButton.right ++ anchors.left: battCurrentButton.right + anchors.bottom: parent.bottom + + property variant texts: [ qsTr("AUTO"), qsTr("ON"), qsTr("OFF")] +@@ -498,7 +554,7 @@ + property bool pumpEnabled: pumpRelay.value === 3 + + show: pumpEnabled +- isCurrentItem: (buttonIndex == 2) ++ isCurrentItem: (buttonIndex == 3) + focus: root.active && isCurrentItem + + title: qsTr("PUMP")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OverviewMobile.qml Mon Dec 13 23:05:38 2021 +1030 @@ -0,0 +1,642 @@ +import QtQuick 1.1 +import com.victron.velib 1.0 +import "utils.js" as Utils + +OverviewPage { + id: root + + property variant sys: theSystem + property string settingsBindPreffix: "com.victronenergy.settings" + property string pumpBindPreffix: "com.victronenergy.pump.startstop0" + property variant activeNotifications: NotificationCenter.notifications.filter( + function isActive(obj) { return obj.active} ) + property string noAdjustableByDmc: qsTr("This setting is disabled when a Digital Multi Control " + + "is connected. If it was recently disconnected execute " + + "\"Redetect system\" that is avalible on the inverter menu page.") + property string noAdjustableByBms: qsTr("This setting is disabled when a VE.Bus BMS " + + "is connected. If it was recently disconnected execute " + + "\"Redetect system\" that is avalible on the inverter menu page.") + property string noAdjustableTextByConfig: qsTr("This setting is disabled. " + + "Possible reasons are \"Overruled by remote\" is not enabled or " + + "an assistant is preventing the adjustment. Please, check " + + "the inverter configuration with VEConfigure.") + property int numberOfMultis: 0 + property string vebusPrefix: "" + + // Keeps track of which button on the bottom row is active + property int buttonIndex: 0 + + title: qsTr("Mobile") + + Component.onCompleted: discoverMulti() + + ListView { + id: pwColumn + + property int tilesCount: solarTile.visible || dcSystem.visible ? 3 : 2 + property int tileHeight: Math.ceil(height / tilesCount) + interactive: false // static tiles + + width: 136 + anchors { + left: parent.left + top: parent.top; + bottom: acModeButton.top; + } + + model: VisualItemModel { + Tile { + width: pwColumn.width + height: visible ? pwColumn.tileHeight : 0 + title: qsTr("AC INPUT") + color: "#82acde" + visible: !dcSystem.visible || !solarTile.visible + values: [ + TileText { + text: sys.acInput.power.uiText + font.pixelSize: 30 + } + + ] + } + + TileAcPower { + width: pwColumn.width + height: visible ? pwColumn.tileHeight : 0 + title: qsTr("AC LOADS") + color: "#e68e8a" + values: [ + TileText { + text: sys.acLoad.power.uiText + font.pixelSize: 30 + } + ] + } + + Tile { + id: solarTile + width: pwColumn.width + height: visible ? pwColumn.tileHeight : 0 + title: qsTr("PV CHARGER") + color: "#2cc36b" + visible : sys.pvCharger.power.valid + + values: [ + TileText { + font.pixelSize: 30 + text: sys.pvCharger.power.uiText + } + ] + } + Tile { + id: dcSystem + width: pwColumn.width + height: visible ? pwColumn.tileHeight : 0 + title: qsTr("DC SYSTEM") + color: "#16a085" + visible : hasDcSys.value === 1 + + VBusItem { + id: hasDcSys + bind: Utils.path(settingsBindPreffix, "/Settings/SystemSetup/HasDcSystem") + } + + values: [ + TileText { + font.pixelSize: 30 + text: sys.dcSystem.power.format(0) + }, + TileText { + text: !sys.dcSystem.power.valid ? "---" : + sys.dcSystem.power.value < 0 ? qsTr("to battery") : qsTr("from battery") + } + ] + } + } + } + + Tile { + id: logoTile + + color: "#575748" + height: 120 + anchors { + left: pwColumn.right + right: tanksColum.left + top: parent.top + } + + MbIcon { + x: 1 + y: 1 + // see below, so the svg instead of a png if there is a 1x1 image + visible: customImage.sourceSize.width === 1 && customImage.sourceSize.height === 1 + iconId: "mobile-builder-logo-svg" + } + + // The uploaded png, the default is a 1x1 transparent pixel now. + Image { + id: customImage + source: "image://theme/mobile-builder-logo" + anchors.centerIn: parent + } + } + + Tile { + id: batteryTile + height: 112 + title: qsTr("BATTERY") + anchors { + left: pwColumn.right + right: stateTile.left + top: logoTile.bottom + bottom: acModeButton.top + } + + values: [ + TileText { + text: sys.battery.soc.absFormat(0) + font.pixelSize: 30 + height: 32 + }, + TileText { + text: { + if (!sys.battery.state.valid) + return "---" + switch(sys.battery.state.value) { + case sys.batteryStateIdle: return qsTr("idle") + case sys.batteryStateCharging : return qsTr("charging") + case sys.batteryStateDischarging : return qsTr("discharging") + } + } + }, + TileText { + text: sys.battery.power.absFormat(0) + }, + TileText { + text: sys.battery.voltage.format(1) + " " + sys.battery.current.format(1) + } + ] + } + + Tile { + id: stateTile + + width: 104 + title: qsTr("STATUS") + color: "#4789d0" + + anchors { + right: tanksColum.left + top: logoTile.bottom + bottom: acModeButton.top + } + + Timer { + id: wallClock + + running: true + repeat: true + interval: 1000 + triggeredOnStart: true + onTriggered: time = Qt.formatDateTime(new Date(), "hh:mm") + + property string time + } + + values: [ + TileText { + id: systemTile + text: wallClock.time + font.pixelSize: 30 + }, + TileText { + property VeQuickItem gpsService: VeQuickItem { uid: "dbus/com.victronenergy.system/GpsService" } + property VeQuickItem speed: VeQuickItem { uid: Utils.path("dbus/", gpsService.value, "/Speed") } + property VeQuickItem speedUnit: VeQuickItem { uid: "dbus/com.victronenergy.settings/Settings/Gps/SpeedUnit" } + + text: speed.value === undefined ? "" : getValue() + visible: speed.value !== undefined && speedUnit.value !== undefined + + function getValue() + { + if (speedUnit.value === "km/h") + return (speed.value * 3.6).toFixed(1) + speedUnit.value + if (speedUnit.value === "mph") + return (speed.value * 2.236936).toFixed(1) + speedUnit.value + if (speedUnit.value === "kt") + return (speed.value * (3600/1852)).toFixed(1) + speedUnit.value + return speed.value.toFixed(2) + "m/s" + } + }, + Marquee { + text: notificationText() + width: stateTile.width + interval: 100 + fontSize: 13 + } + ] + } + + ListView { + id: tanksColum + + property int tileHeight: Math.ceil(height / Math.max(count, 2)) + width: 134 + interactive: false // static tiles + model: TankModel { id: tankModel } + delegate: TileTank { + // Without an intermediate assignment this will trigger a binding loop warning. + property variant theService: DBusServices.get(buddy.id) + service: theService + width: tanksColum.width + height: tanksColum.tileHeight + pumpBindPrefix: root.pumpBindPreffix + compact: tankModel.rowCount > (pumpButton.pumpEnabled ? 4 : 5) + Connections { + target: scrollTimer + onTriggered: doScroll() + } + } + + anchors { + top: root.top + bottom: pumpButton.pumpEnabled ? acModeButton.top : acModeButton.bottom + right: root.right + } + + // Synchronise tank name text scroll start + Timer { + id: scrollTimer + interval: 15000 + repeat: true + running: root.active && tankModel.rowCount > 4 + } + + Tile { + title: qsTr("TANKS") + anchors.fill: parent + values: TileText { + text: qsTr("No tanks found") + width: parent.width + wrapMode: Text.WordWrap + } + z: -1 + } + } + + Keys.forwardTo: [keyHandler] + + Item { + id: keyHandler + Keys.onLeftPressed: { + if (buttonIndex > 0) + buttonIndex-- + + event.accepted = true + } + + Keys.onRightPressed: { + if (buttonIndex < (pumpButton.pumpEnabled ? 2 : 1)) + buttonIndex++ + + event.accepted = true + } + } + + MouseArea { + anchors.fill: parent + enabled: parent.active + onPressed: mouse.accepted = acCurrentButton.expanded + onClicked: acCurrentButton.cancel() + } + + TileSpinBox { + id: acCurrentButton + + anchors.bottom: parent.bottom + anchors.left: parent.left + isCurrentItem: (buttonIndex == 0) + focus: root.active && isCurrentItem + + bind: Utils.path(vebusPrefix, "/Ac/ActiveIn/CurrentLimit") + title: qsTr("AC CURRENT LIMIT") + color: containsMouse && !editMode ? "#d3d3d3" : "#A8A8A8" + width: pumpButton.pumpEnabled ? 160 : 173 + fontPixelSize: 14 + unit: "A" + readOnly: currentLimitIsAdjustable.value !== 1 || numberOfMultis > 1 + buttonColor: "#979797" + + VBusItem { id: currentLimitIsAdjustable; bind: Utils.path(vebusPrefix, "/Ac/ActiveIn/CurrentLimitIsAdjustable") } + + Keys.onSpacePressed: showErrorToast(event) + + function editIsAllowed() { + if (numberOfMultis > 1) { + toast.createToast(qsTr("It is not possible to change this setting when there are more than one inverter connected."), 5000) + return false + } + + if (currentLimitIsAdjustable.value === 0) { + if (dmc.valid) { + toast.createToast(noAdjustableByDmc, 5000) + return false + } + if (bms.valid) { + toast.createToast(noAdjustableByBms, 5000) + return false + } + if (!dmc.valid && !bms.valid) { + toast.createToast(noAdjustableTextByConfig, 5000) + return false + } + } + + return true + } + + function showErrorToast(event) { + editIsAllowed() + event.accepted = true + } + } + + Tile { + id: acModeButton + anchors.left: acCurrentButton.right + anchors.bottom: parent.bottom + property variant texts: { 4: qsTr("OFF"), 3: qsTr("ON"), 1: qsTr("CHARGER ONLY") } + property int value: mode.valid ? mode.value : 3 + property int shownValue: applyAnimation2.running ? applyAnimation2.pendingValue : value + + isCurrentItem: (buttonIndex == 1) + focus: root.active && isCurrentItem + + editable: true + readOnly: !modeIsAdjustable.valid || modeIsAdjustable.value !== 1 || numberOfMultis > 1 + width: pumpButton.pumpEnabled ? 160 : 173 + height: 45 + color: acModeButtonMouseArea.containsPressed ? "#d3d3d3" : "#A8A8A8" + title: qsTr("AC MODE") + + values: [ + TileText { + text: modeIsAdjustable.valid && numberOfMultis === 1 ? qsTr("%1").arg(acModeButton.texts[acModeButton.shownValue]) : qsTr("NOT AVAILABLE") + } + ] + + VBusItem { id: mode; bind: Utils.path(vebusPrefix, "/Mode") } + VBusItem { id: modeIsAdjustable; bind: Utils.path(vebusPrefix,"/ModeIsAdjustable") } + + Keys.onSpacePressed: edit() + + function edit() { + if (!mode.valid) + return + + if (numberOfMultis > 1) { + toast.createToast(qsTr("It is not possible to change this setting when there are more than one inverter connected."), 5000) + return + } + + if (modeIsAdjustable.value === 0) { + if (dmc.valid) + toast.createToast(noAdjustableByDmc, 5000) + if (bms.valid) + toast.createToast(noAdjustableByBms, 5000) + if (!dmc.valid && !bms.valid) + toast.createToast(noAdjustableTextByConfig, 5000) + return + } + + switch (shownValue) { + case 4: + applyAnimation2.pendingValue = 3 + break; + case 3: + applyAnimation2.pendingValue = 1 + break; + case 1: + applyAnimation2.pendingValue = 4 + break; + } + + applyAnimation2.restart() + } + + MouseArea { + id: acModeButtonMouseArea + anchors.fill: parent + property bool containsPressed: containsMouse && pressed + onClicked: { + buttonIndex = 1 + parent.edit() + } + } + + Rectangle { + id: timerRect2 + height: 2 + width: acModeButton.width * 0.8 + visible: applyAnimation2.running + anchors { + bottom: parent.bottom; bottomMargin: 5 + horizontalCenter: parent.horizontalCenter + } + } + + SequentialAnimation { + id: applyAnimation2 + + property int pendingValue + + NumberAnimation { + target: timerRect2 + property: "width" + from: 0 + to: acModeButton.width * 0.8 + duration: 3000 + } + + ColorAnimation { + target: acModeButton + property: "color" + from: "#A8A8A8" + to: "#4789d0" + duration: 200 + } + + ColorAnimation { + target: acModeButton + property: "color" + from: "#4789d0" + to: "#A8A8A8" + duration: 200 + } + PropertyAction { + target: timerRect2 + property: "width" + value: 0 + } + + ScriptAction { script: mode.setValue(applyAnimation2.pendingValue) } + + PauseAnimation { duration: 1000 } + } + } + + Tile { + id: pumpButton + + anchors.left: acModeButton.right + anchors.bottom: parent.bottom + + property variant texts: [ qsTr("AUTO"), qsTr("ON"), qsTr("OFF")] + property int value: 0 + property bool reset: false + property bool pumpEnabled: pumpRelay.value === 3 + + show: pumpEnabled + isCurrentItem: (buttonIndex == 2) + focus: root.active && isCurrentItem + + title: qsTr("PUMP") + width: show ? 160 : 0 + height: 45 + editable: true + readOnly: false + color: pumpButtonMouseArea.containsPressed ? "#d3d3d3" : "#A8A8A8" + + VBusItem { id: pump; bind: Utils.path(settingsBindPreffix, "/Settings/Pump0/Mode") } + VBusItem { id: pumpRelay; bind: Utils.path(settingsBindPreffix, "/Settings/Relay/Function") } + + values: [ + TileText { + text: pumpButton.pumpEnabled ? qsTr("%1").arg(pumpButton.texts[pumpButton.value]) : qsTr("DISABLED") + } + ] + + Keys.onSpacePressed: edit() + + function edit() { + if (!pumpEnabled) { + toast.createToast(qsTr("Pump functionality is not enabled. To enable it go to the relay settings page and set function to \"Tank pump\""), 5000) + return + } + + reset = true + applyAnimation.restart() + reset = false + + if (value < 2) + value++ + else + value = 0 + } + + MouseArea { + id: pumpButtonMouseArea + property bool containsPressed: containsMouse && pressed + anchors.fill: parent + onClicked: { + buttonIndex = 2 + parent.edit() + } + } + + Rectangle { + id: timerRect + height: 2 + width: pumpButton.width * 0.8 + visible: applyAnimation.running + anchors { + bottom: parent.bottom; bottomMargin: 5 + horizontalCenter: parent.horizontalCenter + } + } + + SequentialAnimation { + id: applyAnimation + alwaysRunToEnd: false + NumberAnimation { + target: timerRect + property: "width" + from: 0 + to: pumpButton.width * 0.8 + duration: 3000 + } + + ColorAnimation { + target: pumpButton + property: "color" + from: "#A8A8A8" + to: "#4789d0" + duration: 200 + } + + ColorAnimation { + target: pumpButton + property: "color" + from: "#4789d0" + to: "#A8A8A8" + duration: 200 + } + PropertyAction { + target: timerRect + property: "width" + value: 0 + } + // Do not set value if the animation is restarted by user pressing the button + // to move between options + onCompleted: if (!pumpButton.reset) pump.setValue(pumpButton.value) + } + } + + // When new service is found check if is a tank sensor + Connections { + target: DBusServices + onDbusServiceFound: addService(service) + } + + function addService(service) + { + if (service.type === DBusService.DBUS_SERVICE_MULTI) { + numberOfMultis++ + if (vebusPrefix === "") + vebusPrefix = service.name; + } + } + + // Check available services to find tank sesnsors + function discoverMulti() + { + for (var i = 0; i < DBusServices.count; i++) { + if (DBusServices.at(i).type === DBusService.DBUS_SERVICE_MULTI) { + addService(DBusServices.at(i)) + } + } + } + + function notificationText() + { + if (activeNotifications.length === 0) + return qsTr("no alarms") + + var descr = [] + for (var n = 0; n < activeNotifications.length; n++) { + var notification = activeNotifications[n]; + + var text = notification.serviceName + " - " + notification.description; + if (notification.value !== "" ) + text += ": " + notification.value + + descr.push(text) + } + + return descr.join(" | ") + } + + VBusItem { id: dmc; bind: Utils.path(vebusPrefix, "/Devices/Dmc/Version") } + VBusItem { id: bms; bind: Utils.path(vebusPrefix, "/Devices/Bms/Version") } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Mon Dec 13 23:05:38 2021 +1030 @@ -0,0 +1,8 @@ +OverviewJD.qml is a lightly customised version of OverviewMobile.qml to +add more information and charge current control. + +main.qml.orig is the original main.qml from the device, main.qml is the +modified version to add OverviewJD to the carousel. + +install.sh will install the new page and attempt to patch main.qml unless +it has already been done. \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/install.sh Mon Dec 13 23:05:38 2021 +1030 @@ -0,0 +1,41 @@ +#!/bin/sh + +root=$(cd $(dirname $0); pwd) +qmldir=/opt/victronenergy/gui/qml + +cp -f $root/OverviewJD.qml $qmldir +if ! grep -lq "OverviewJD sentinel" $qmldir/main.qml; then + cp -f $qmldir/main.qml /tmp + (cd /tmp ; patch >/dev/null 2>&1 ) <$root/main.qml.diff + if [ $? -ne 0 ]; then + echo "Patching main.qml failed" + else + echo "Updating main.qml" + mv -f $qmldir/main.qml $qmldir/main.qml.orig + mv /tmp/main.qml $qmldir/ + fi +else + echo "main.qml already patched, skipping" +fi +# Create rc.local hooks for reinstallation +mkdir /data/rc.local.d /data/rcS.local.d >/dev/null 2>&1 +cat >/data/rc.local.d/overviewjd <<EOF +#!/bin/sh +sh $root/install.sh +EOF + +cat >/data/rc.local <<EOF +#!/bin/sh +for s in /data/rc.local.d/*; do + \$s +done +EOF + +cat >/data/rcS.local <<EOF +#!/bin/sh +for s in /data/rcS.local.d/*; do + \$s +done +EOF + +chmod 755 /data/rc.local.d/overviewjd /data/rc.local /data/rcS.local
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.qml Mon Dec 13 23:05:38 2021 +1030 @@ -0,0 +1,369 @@ +import QtQuick 1.1 + +import Qt.labs.components.native 1.0 +import com.victron.velib 1.0 +import "utils.js" as Utils + +PageStackWindow { + id: rootWindow + + gpsConnected: gpsFix.value === 1 + onCompletedChanged: checkAlarm() + initialPage: PageMain {} + + property VeQuickItem gpsService: VeQuickItem { uid: "dbus/com.victronenergy.system/GpsService" } + property VeQuickItem gpsFix: VeQuickItem { uid: Utils.path("dbus/", gpsService.value, "/Fix") } + property bool completed: false + property bool showAlert: NotificationCenter.alert + property bool alarm: NotificationCenter.alarm + property bool overviewsLoaded: defaultOverview.valid && generatorOverview.valid && mobileOverview.valid && tanksOverview.valid && startWithMenu.valid + property string bindPrefix: "com.victronenergy.settings" + + property bool isNotificationPage: pageStack.currentPage && pageStack.currentPage.title === qsTr("Notifications") + property bool isOverviewPage: pageStack.currentPage && pageStack.currentPage.model === overviewModel; + property bool isOfflineFwUpdatePage: pageStack.currentPage && pageStack.currentPage.objectName === "offlineFwUpdatePage"; + + + property string hubOverviewType: theSystem.systemType.valid ? + withoutGridMeter.value === 1 ? "Hub" : theSystem.systemType.value : "" + + // Keep track of the current view (menu/overview) to show as default next time the + // CCGX is restarted + onIsOverviewPageChanged: startWithMenu.setValue(isOverviewPage ? 0 : 1) + + // Add the correct OverviewHub page + onHubOverviewTypeChanged: { + switch(hubOverviewType){ + case "Hub": + case "Hub-1": + case "Hub-2": + case "Hub-3": + replaceOverview("OverviewGridParallel.qml", "OverviewHub.qml"); + break; + case "Hub-4": + case "ESS": + replaceOverview("OverviewHub.qml", "OverviewGridParallel.qml"); + break; + default: + break; + } + // Workaround the QTBUG-17012 (only the first sentence in each case of Switch Statement can be executed) + // by adding a return statement + return + } + + VBusItem { + id: generatorOverview + bind: "com.victronenergy.settings/Settings/Relay/Function" + onValueChanged: extraOverview("OverviewGeneratorRelay.qml", value === 1) + } + + VBusItem { + id: fischerPandaGenOverview + bind: "com.victronenergy.settings/Settings/Services/FischerPandaAutoStartStop" + onValueChanged: extraOverview("OverviewGeneratorFp.qml", value === 1) + } + + VBusItem { + id: mobileOverview + bind: "com.victronenergy.settings/Settings/Gui/MobileOverview" + onValueChanged:{ + extraOverview("OverviewMobile.qml", value === 1) + } + } + VBusItem { + id: tanksOverview + bind: "com.victronenergy.settings/Settings/Gui/TanksOverview" + onValueChanged:{ + extraOverview("OverviewTanks.qml", value === 1) + } + } + + VBusItem { + id: startWithMenu + bind: "com.victronenergy.settings/Settings/Gui/StartWithMenuView" + } + + VBusItem { + id: withoutGridMeter + bind: "com.victronenergy.settings/Settings/CGwacs/RunWithoutGridMeter" + } + + + VBusItem { + id: defaultOverview + bind: "com.victronenergy.settings/Settings/Gui/DefaultOverview" + } + + // Note: finding a firmware image on the storage device is error 4 for vrm storage + // since it should not be used for logging. That fact is used here to determine if + // there is a firmware image. + Connections { + target: storageEvents + onVrmStorageError: { + if (error === 4) { + setTopPage(offlineFwUpdates) + } + } + } + + onAlarmChanged: { + if (completed) + checkAlarm() + } + + // always keep track of system information + HubData { + id: theSystem + } + + // note: used for leaving the overviews as well + function backToMainMenu() + { + pageStack.pop(initialPage); + } + + Toast { + id: toast + transform: Scale { + xScale: screen.scaleX + yScale: screen.scaleY + origin.x: toast.width / 2 + origin.y: toast.height / 2 + } + } + + SignalToaster {} + + ToolbarHandlerPages { + id: mainToolbarHandler + isDefault: true + } + + ToolBarLayout { + id: mbTools + height: parent.height + + Item { + anchors.verticalCenter: parent.verticalCenter + anchors.left: mbTools.left + height: mbTools.height + width: 200 + + MouseArea { + anchors.fill: parent + onClicked: { + if (pageStack.currentPage) + pageStack.currentPage.toolbarHandler.leftAction(true) + } + } + + Row { + anchors.centerIn: parent + + MbIcon { + anchors.verticalCenter: parent.verticalCenter + iconId: pageStack.currentPage ? pageStack.currentPage.leftIcon : "" + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: pageStack.currentPage ? pageStack.currentPage.leftText : "" + color: "white" + font.bold: true + font.pixelSize: 16 + } + } + } + + MbIcon { + id: centerScrollIndicator + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: mbTools.verticalCenter + } + iconId: pageStack.currentPage ? pageStack.currentPage.scrollIndicator : "" + } + + Item { + anchors.verticalCenter: parent.verticalCenter + height: mbTools.height + anchors.right: mbTools.right + width: 200 + + MouseArea { + anchors.fill: parent + onClicked: { + if (pageStack.currentPage) + pageStack.currentPage.toolbarHandler.rightAction(true) + } + } + + Row { + anchors.centerIn: parent + + MbIcon { + iconId: pageStack.currentPage ? pageStack.currentPage.rightIcon : "" + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: pageStack.currentPage ? pageStack.currentPage.rightText : "" + anchors.verticalCenter: parent.verticalCenter + color: "white" + font.bold: true + font.pixelSize: 16 + } + } + } + } + + Component.onCompleted: { + completed = true + } + + ListModel { + id: overviewModel + ListElement { + pageSource: "OverviewHub.qml" + } + ListElement { + pageSource: "OverviewTiles.qml" + } + // ---> OverviewJD sentinel + ListElement { + pageSource: "OverviewJD.qml" + } + // <--- OverviewJD trailer + } + + Component { + id: overviewComponent + PageFlow { + // Display default overview when loaded + defaultIndex: getDefaultOverviewIndex() + // Store the current overview page as default + onCurrentIndexChanged: if (active) defaultOverview.setValue(overviewModel.get(currentIndex).pageSource.replace(".qml", "")) + model: overviewModel + } + } + + // When all the related settings items are valid, show the overview page if was the last oppened page + // before restarting + Timer { + interval: 2000 + running: completed && overviewsLoaded && startWithMenu.valid + onTriggered: if (startWithMenu.value === 0) showOverview() + } + + function getDefaultOverviewIndex() + { + if(!defaultOverview.valid) + return 0 + for (var i = 0; i < overviewModel.count; i++){ + if (overviewModel.get(i).pageSource.replace(".qml", "") === defaultOverview.value) { + return i + } + } + return 0 + } + + Component { + id: noticationsComponent + PageNotifications {} + } + + Component { + id: offlineFwUpdates + PageSettingsFirmwareOffline { checkOnCompleted: true} + + } + + // Add or remove extra overviews. for example, generator overview + // shouldn't be shown if the start/stop functionality is not enabled. + // Index parameter is optional, usefull to keep an order. + function extraOverview(name, show, index) + { + var i = 0 + if (show) { + if (index !== undefined) { + if (overviewModel.get(index).pageSource === name) + return + // First append the page + overviewModel.append({"pageSource": name}) + // Then move all the pages behind index + overviewModel.move(index, overviewModel.count - 2, overviewModel.count - 2) + } else { + for (i = 0; i < overviewModel.count; i++) + if (overviewModel.get(i).pageSource === name) + // Don't append if already exists + return + overviewModel.append({"pageSource": name}) + } + } else { + for (i = 0; i < overviewModel.count; i++) + if (overviewModel.get(i).pageSource === name) + overviewModel.remove(i) + } + } + + function replaceOverview(oldPage, newPage) + { + for (var i = 0; i < overviewModel.count; i++) + if (overviewModel.get(i).pageSource === oldPage) + overviewModel.get(i).pageSource = newPage + } + + // Central mover for the ball animation on the overviews + // Instead of using a timer per line, using a central one + // reduces the CPU usage a little bit and makes the animations + // smoother. + Timer { + id: mover + property double pos: _counter / _loops + property int _counter + property int _loops: 13 + + interval: 100 + running: true + repeat: true + onTriggered: if (_counter >= (_loops - 1)) _counter = 0; else _counter++ + } + + // If an overview or notifications is active, the new page will replace it + // instead to be pushed. This way we prevent an unwanted stackpage depth + // increment everytime another page wants to be on top. + function setTopPage(page) + { + if (isNotificationPage || isOverviewPage || isOfflineFwUpdatePage) + rootWindow.pageStack.replace(page); + else + rootWindow.pageStack.push(page); + } + + function spuriousKeyPress() + { + return !pageStack.currentPage || !pageStack.currentPage.active + } + + function showOverview() + { + if (spuriousKeyPress() || isOverviewPage) + return + setTopPage(overviewComponent) + } + + function showPageNotifications() + { + if (spuriousKeyPress() || isNotificationPage) + return + setTopPage(noticationsComponent) + } + + function checkAlarm() + { + if (alarm) + showPageNotifications() + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.qml.diff Mon Dec 13 23:05:38 2021 +1030 @@ -0,0 +1,14 @@ +--- main.qml.orig 2021-12-13 23:00:48.000000000 +1030 ++++ main.qml 2021-12-13 23:01:18.000000000 +1030 +@@ -231,6 +231,11 @@ + ListElement { + pageSource: "OverviewTiles.qml" + } ++ // ---> OverviewJD sentinel ++ ListElement { ++ pageSource: "OverviewJD.qml" ++ } ++ // <--- OverviewJD trailer + } + + Component {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.qml.orig Mon Dec 13 23:05:38 2021 +1030 @@ -0,0 +1,364 @@ +import QtQuick 1.1 + +import Qt.labs.components.native 1.0 +import com.victron.velib 1.0 +import "utils.js" as Utils + +PageStackWindow { + id: rootWindow + + gpsConnected: gpsFix.value === 1 + onCompletedChanged: checkAlarm() + initialPage: PageMain {} + + property VeQuickItem gpsService: VeQuickItem { uid: "dbus/com.victronenergy.system/GpsService" } + property VeQuickItem gpsFix: VeQuickItem { uid: Utils.path("dbus/", gpsService.value, "/Fix") } + property bool completed: false + property bool showAlert: NotificationCenter.alert + property bool alarm: NotificationCenter.alarm + property bool overviewsLoaded: defaultOverview.valid && generatorOverview.valid && mobileOverview.valid && tanksOverview.valid && startWithMenu.valid + property string bindPrefix: "com.victronenergy.settings" + + property bool isNotificationPage: pageStack.currentPage && pageStack.currentPage.title === qsTr("Notifications") + property bool isOverviewPage: pageStack.currentPage && pageStack.currentPage.model === overviewModel; + property bool isOfflineFwUpdatePage: pageStack.currentPage && pageStack.currentPage.objectName === "offlineFwUpdatePage"; + + + property string hubOverviewType: theSystem.systemType.valid ? + withoutGridMeter.value === 1 ? "Hub" : theSystem.systemType.value : "" + + // Keep track of the current view (menu/overview) to show as default next time the + // CCGX is restarted + onIsOverviewPageChanged: startWithMenu.setValue(isOverviewPage ? 0 : 1) + + // Add the correct OverviewHub page + onHubOverviewTypeChanged: { + switch(hubOverviewType){ + case "Hub": + case "Hub-1": + case "Hub-2": + case "Hub-3": + replaceOverview("OverviewGridParallel.qml", "OverviewHub.qml"); + break; + case "Hub-4": + case "ESS": + replaceOverview("OverviewHub.qml", "OverviewGridParallel.qml"); + break; + default: + break; + } + // Workaround the QTBUG-17012 (only the first sentence in each case of Switch Statement can be executed) + // by adding a return statement + return + } + + VBusItem { + id: generatorOverview + bind: "com.victronenergy.settings/Settings/Relay/Function" + onValueChanged: extraOverview("OverviewGeneratorRelay.qml", value === 1) + } + + VBusItem { + id: fischerPandaGenOverview + bind: "com.victronenergy.settings/Settings/Services/FischerPandaAutoStartStop" + onValueChanged: extraOverview("OverviewGeneratorFp.qml", value === 1) + } + + VBusItem { + id: mobileOverview + bind: "com.victronenergy.settings/Settings/Gui/MobileOverview" + onValueChanged:{ + extraOverview("OverviewMobile.qml", value === 1) + } + } + VBusItem { + id: tanksOverview + bind: "com.victronenergy.settings/Settings/Gui/TanksOverview" + onValueChanged:{ + extraOverview("OverviewTanks.qml", value === 1) + } + } + + VBusItem { + id: startWithMenu + bind: "com.victronenergy.settings/Settings/Gui/StartWithMenuView" + } + + VBusItem { + id: withoutGridMeter + bind: "com.victronenergy.settings/Settings/CGwacs/RunWithoutGridMeter" + } + + + VBusItem { + id: defaultOverview + bind: "com.victronenergy.settings/Settings/Gui/DefaultOverview" + } + + // Note: finding a firmware image on the storage device is error 4 for vrm storage + // since it should not be used for logging. That fact is used here to determine if + // there is a firmware image. + Connections { + target: storageEvents + onVrmStorageError: { + if (error === 4) { + setTopPage(offlineFwUpdates) + } + } + } + + onAlarmChanged: { + if (completed) + checkAlarm() + } + + // always keep track of system information + HubData { + id: theSystem + } + + // note: used for leaving the overviews as well + function backToMainMenu() + { + pageStack.pop(initialPage); + } + + Toast { + id: toast + transform: Scale { + xScale: screen.scaleX + yScale: screen.scaleY + origin.x: toast.width / 2 + origin.y: toast.height / 2 + } + } + + SignalToaster {} + + ToolbarHandlerPages { + id: mainToolbarHandler + isDefault: true + } + + ToolBarLayout { + id: mbTools + height: parent.height + + Item { + anchors.verticalCenter: parent.verticalCenter + anchors.left: mbTools.left + height: mbTools.height + width: 200 + + MouseArea { + anchors.fill: parent + onClicked: { + if (pageStack.currentPage) + pageStack.currentPage.toolbarHandler.leftAction(true) + } + } + + Row { + anchors.centerIn: parent + + MbIcon { + anchors.verticalCenter: parent.verticalCenter + iconId: pageStack.currentPage ? pageStack.currentPage.leftIcon : "" + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: pageStack.currentPage ? pageStack.currentPage.leftText : "" + color: "white" + font.bold: true + font.pixelSize: 16 + } + } + } + + MbIcon { + id: centerScrollIndicator + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: mbTools.verticalCenter + } + iconId: pageStack.currentPage ? pageStack.currentPage.scrollIndicator : "" + } + + Item { + anchors.verticalCenter: parent.verticalCenter + height: mbTools.height + anchors.right: mbTools.right + width: 200 + + MouseArea { + anchors.fill: parent + onClicked: { + if (pageStack.currentPage) + pageStack.currentPage.toolbarHandler.rightAction(true) + } + } + + Row { + anchors.centerIn: parent + + MbIcon { + iconId: pageStack.currentPage ? pageStack.currentPage.rightIcon : "" + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: pageStack.currentPage ? pageStack.currentPage.rightText : "" + anchors.verticalCenter: parent.verticalCenter + color: "white" + font.bold: true + font.pixelSize: 16 + } + } + } + } + + Component.onCompleted: { + completed = true + } + + ListModel { + id: overviewModel + ListElement { + pageSource: "OverviewHub.qml" + } + ListElement { + pageSource: "OverviewTiles.qml" + } + } + + Component { + id: overviewComponent + PageFlow { + // Display default overview when loaded + defaultIndex: getDefaultOverviewIndex() + // Store the current overview page as default + onCurrentIndexChanged: if (active) defaultOverview.setValue(overviewModel.get(currentIndex).pageSource.replace(".qml", "")) + model: overviewModel + } + } + + // When all the related settings items are valid, show the overview page if was the last oppened page + // before restarting + Timer { + interval: 2000 + running: completed && overviewsLoaded && startWithMenu.valid + onTriggered: if (startWithMenu.value === 0) showOverview() + } + + function getDefaultOverviewIndex() + { + if(!defaultOverview.valid) + return 0 + for (var i = 0; i < overviewModel.count; i++){ + if (overviewModel.get(i).pageSource.replace(".qml", "") === defaultOverview.value) { + return i + } + } + return 0 + } + + Component { + id: noticationsComponent + PageNotifications {} + } + + Component { + id: offlineFwUpdates + PageSettingsFirmwareOffline { checkOnCompleted: true} + + } + + // Add or remove extra overviews. for example, generator overview + // shouldn't be shown if the start/stop functionality is not enabled. + // Index parameter is optional, usefull to keep an order. + function extraOverview(name, show, index) + { + var i = 0 + if (show) { + if (index !== undefined) { + if (overviewModel.get(index).pageSource === name) + return + // First append the page + overviewModel.append({"pageSource": name}) + // Then move all the pages behind index + overviewModel.move(index, overviewModel.count - 2, overviewModel.count - 2) + } else { + for (i = 0; i < overviewModel.count; i++) + if (overviewModel.get(i).pageSource === name) + // Don't append if already exists + return + overviewModel.append({"pageSource": name}) + } + } else { + for (i = 0; i < overviewModel.count; i++) + if (overviewModel.get(i).pageSource === name) + overviewModel.remove(i) + } + } + + function replaceOverview(oldPage, newPage) + { + for (var i = 0; i < overviewModel.count; i++) + if (overviewModel.get(i).pageSource === oldPage) + overviewModel.get(i).pageSource = newPage + } + + // Central mover for the ball animation on the overviews + // Instead of using a timer per line, using a central one + // reduces the CPU usage a little bit and makes the animations + // smoother. + Timer { + id: mover + property double pos: _counter / _loops + property int _counter + property int _loops: 13 + + interval: 100 + running: true + repeat: true + onTriggered: if (_counter >= (_loops - 1)) _counter = 0; else _counter++ + } + + // If an overview or notifications is active, the new page will replace it + // instead to be pushed. This way we prevent an unwanted stackpage depth + // increment everytime another page wants to be on top. + function setTopPage(page) + { + if (isNotificationPage || isOverviewPage || isOfflineFwUpdatePage) + rootWindow.pageStack.replace(page); + else + rootWindow.pageStack.push(page); + } + + function spuriousKeyPress() + { + return !pageStack.currentPage || !pageStack.currentPage.active + } + + function showOverview() + { + if (spuriousKeyPress() || isOverviewPage) + return + setTopPage(overviewComponent) + } + + function showPageNotifications() + { + if (spuriousKeyPress() || isNotificationPage) + return + setTopPage(noticationsComponent) + } + + function checkAlarm() + { + if (alarm) + showPageNotifications() + } +}