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()
+	}
+}