-
Notifications
You must be signed in to change notification settings - Fork 23
Settings: Add 'Opportunity Loads' page #2782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
|
|
||
| /* | ||
| ** Copyright (C) 2026 Victron Energy B.V. | ||
| ** See LICENSE.txt for license information. | ||
| */ | ||
|
|
||
| import QtQuick | ||
| import Victron.VenusOS | ||
|
|
||
| Item { | ||
| id: root | ||
|
|
||
| property var jsonArray: [ | ||
| { | ||
| "controllable": true, | ||
| "deviceInstance": 0, | ||
| "label": "Battery", | ||
| "serviceType": "battery", | ||
| "uniqueIdentifier": "battery" | ||
| }, | ||
| { | ||
| "controllable": true, | ||
| "deviceInstance": 56, | ||
| "serviceType": "acload", | ||
| "uniqueIdentifier": "shellyPro2PMPVHeat2_56" | ||
| }, | ||
| { | ||
| "controllable": true, | ||
| "deviceInstance": 54, | ||
| "serviceType": "acload", | ||
| "uniqueIdentifier": "shellyPro2PMPVHeat1_54" | ||
| }, | ||
| { | ||
| "controllable": true, | ||
| "deviceInstance": 55, | ||
| "serviceType": "acload", | ||
| "uniqueIdentifier": "shellyPro2PMPVHeat2_55" | ||
| }, | ||
| { | ||
| "serviceType":"evcharger", | ||
| "deviceInstance":920, | ||
| "uniqueIdentifier":"EVCS_Mock_3_Phase_920", | ||
| "controllable":true | ||
| } | ||
| ] | ||
|
|
||
| VeQuickItem { | ||
| uid: BackendConnection.serviceUidForType("opportunityloads") + "/AvailableServices" | ||
| Component.onCompleted: setValue(JSON.stringify(jsonArray)) | ||
| } | ||
|
|
||
| VeQuickItem { | ||
| uid: Global.systemSettings.serviceUid + "/Settings/OpportunityLoads/Mode" | ||
| Component.onCompleted: setValue(1) | ||
| } | ||
|
|
||
| VeQuickItem { | ||
| uid: BackendConnection.serviceUidForType("opportunityloads") + "/ReservationBasePower" | ||
| Component.onCompleted: setValue(4000) | ||
| } | ||
|
|
||
| VeQuickItem { | ||
| uid: BackendConnection.serviceUidForType("opportunityloads") + "/ReservationDecrement" | ||
| Component.onCompleted: setValue(40) | ||
| } | ||
|
|
||
| VeQuickItem { | ||
| uid: BackendConnection.serviceUidForType("opportunityloads") + "/BatteryLifeSupport" | ||
| Component.onCompleted: setValue(true) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| /* | ||
| ** Copyright (C) 2026 Victron Energy B.V. | ||
| ** See LICENSE.txt for license information. | ||
| */ | ||
|
|
||
| import QtQuick | ||
| import Victron.VenusOS | ||
|
|
||
| Page { | ||
| id: root | ||
|
|
||
| property alias model: opportunityLoadsModel | ||
| property VeQuickItem loads: VeQuickItem { | ||
| uid: BackendConnection.serviceUidForType("opportunityloads") + "/AvailableServices" | ||
| invalidate: true | ||
| onValueChanged: { | ||
| const jsv = JSON.parse(value) | ||
| for (var i = 0; jsv && i < jsv.length; ++i) { | ||
| const newValue = jsv[i] | ||
| if ((opportunityLoadsModel.count < (i + 1)) || (newValue !== opportunityLoadsModel.get(i))) | ||
| opportunityLoadsModel.set(i, jsv[i]) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Style: indent and add braces around this block |
||
| } | ||
| } | ||
| } | ||
|
|
||
| component Arrow: ListItemButton { | ||
| icon.source: "qrc:/images/icon_arrow.svg" | ||
| flat: false | ||
| height: parent.height - 2 * Theme.geometry_opportunityLoad_margin | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| component DevicePriorityListNavigation: ListNavigation { | ||
| id: devicePriorityDelegate | ||
|
|
||
| property string serviceType: "" | ||
| property int deviceInstance: -1 | ||
| property string label: "" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "" initialisers are not needed. Also, |
||
| property string uniqueIdentifier: "" | ||
| readonly property var devices: serviceType === "acload" ? acLoadDevices : serviceType === "evcharger" ? evcsDevices : null | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could use property type |
||
| readonly property Device device: devices && deviceInstance > 0 ? devices.deviceForDeviceInstance(deviceInstance) : null | ||
| readonly property string pageSource: { | ||
| switch (serviceType) { | ||
| case "battery": | ||
| return "/pages/settings/PageControllableLoadsBattery.qml" | ||
| case "acload": | ||
| return "/pages/settings/PageControllableLoadsAcLoad.qml" | ||
| case "evcharger": | ||
| return "/pages/settings/PageControllableLoadsEVCS.qml" | ||
| default: | ||
| console.warn("Controllable Loads: Invalid service type.") | ||
| return "" | ||
| } | ||
| } | ||
|
|
||
| property alias text: primary.text | ||
| property var pageProperties: ({ | ||
| "title": devicePriorityDelegate.text, | ||
| "device": devicePriorityDelegate.device | ||
| }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| onClicked: Global.pageManager.pushPage(devicePriorityDelegate.pageSource, devicePriorityDelegate.pageProperties) | ||
| interactive: serviceType !== "evcharger" // TODO: remove this once backend supports "evcs maximum charging power limit". | ||
|
|
||
| Arrow { | ||
| id: upArrow | ||
|
|
||
| anchors { | ||
| left: parent.left | ||
| leftMargin: Theme.geometry_opportunityLoad_margin | ||
| verticalCenter: parent.verticalCenter | ||
| } | ||
| enabled: index !== 0 | ||
| onClicked: { | ||
| root.model.move(index, index - 1, 1) | ||
| opportunityLoadsModel.writeToBackEnd() | ||
| } | ||
| } | ||
|
|
||
| Arrow { | ||
| id: downArrow | ||
|
|
||
| anchors { | ||
| left: upArrow.right | ||
| leftMargin: Theme.geometry_opportunityLoad_margin | ||
| verticalCenter: parent.verticalCenter | ||
| } | ||
| enabled: index !== (listView.count - 1) | ||
| rotation: 180 | ||
| onClicked: { | ||
| root.model.move(index + 1, index, 1) | ||
| opportunityLoadsModel.writeToBackEnd() | ||
| } | ||
| } | ||
|
|
||
| Column { | ||
| anchors { | ||
| left: downArrow.right | ||
| leftMargin: Theme.geometry_listItem_content_horizontalMargin | ||
| verticalCenter: parent.verticalCenter | ||
| } | ||
|
|
||
| Label { | ||
| id: primary | ||
|
|
||
| property var device: devicePriorityDelegate.device | ||
|
|
||
| // eg. "Shelly switch (shellyPro2)" | ||
| readonly property string longName: ((device?.productName ?? "") && (device?.name ?? "")) | ||
| ? (device.productName + " (" + device.name + ")") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this really supposed to show both the product name and device name? Normally the device name is (customName || productName) so that the custom name is shown instead of the product name. If it is supposed to show both, then this should check that the product name is not the same as the device name (i.e. when there is no custom name), otherwise the text would be duplicated. Also, use translations here for the parentheses, e.g: |
||
| : "" | ||
|
|
||
| font.pixelSize: Theme.font_size_body2 | ||
| wrapMode: Text.Wrap | ||
| text: serviceType === "battery" | ||
| ? "Battery" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Battery" needs translation. Also, it looks like this expression can be simplified to: |
||
| : longName | ||
| ? longName | ||
| : (device?.name ?? "") | ||
| ? device.name | ||
| : uniqueIdentifier || "" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| GradientListView { | ||
| model: VisibleItemModel { | ||
| ListSwitch { | ||
| dataItem.uid: BackendConnection.serviceUidForType("platform") + "/OpportunityLoads/Mode" | ||
| text: CommonWords.enabled | ||
| } | ||
|
|
||
| SettingsListHeader { | ||
| //% "Devices and Priorities" | ||
| text: qsTrId("pagecontrollableloads_devices_and_priorities") | ||
| } | ||
|
|
||
| SettingsColumn { | ||
| width: parent ? parent.width : 0 | ||
| preferredVisible: opportunityLoadsModel.count > 0 | ||
|
|
||
| ListView { | ||
| id: listView | ||
|
|
||
| model: opportunityLoadsModel | ||
| spacing: Theme.geometry_gradientList_spacing | ||
| implicitHeight: contentHeight | ||
| width: parent ? parent.width : 0 | ||
| delegate: DevicePriorityListNavigation { | ||
| serviceType: model.serviceType | ||
| deviceInstance: model.deviceInstance | ||
| uniqueIdentifier: model.uniqueIdentifier | ||
| label: model.label | ||
| } | ||
| move: Transition { | ||
| NumberAnimation { | ||
| properties: "x,y" | ||
| easing.type: Easing.InOutQuad | ||
| } | ||
| } | ||
| displaced: Transition { | ||
| NumberAnimation { | ||
| properties: "x,y" | ||
| easing.type: Easing.InOutQuad | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| SettingsListHeader { | ||
| //% "Arrange the controllable devices according to their priority; the control algorithm will control them based on the currently available PV excess." | ||
| text: qsTrId("pagecontrollableloads_arrange") | ||
| font.pixelSize: Theme.font_size_tiny | ||
| width: 528 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded width? Also should the font size is too small, maybe |
||
| wrapMode: Text.Wrap | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ListModel { | ||
| id: opportunityLoadsModel | ||
|
|
||
| function writeToBackEnd() { | ||
| var newValue = [] | ||
| for (var i = 0; i < opportunityLoadsModel.count; ++i) { | ||
| newValue.push(opportunityLoadsModel.get(i)) | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary newline; also, use |
||
| loads.setValue(JSON.stringify(newValue)) | ||
| } | ||
| } | ||
|
|
||
| FilteredDeviceModel { | ||
| id: acLoadDevices | ||
| objectName: "PageControllableLoads.acLoadDevices" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are the objectNames from leftover debug? |
||
| serviceTypes: ["acload"] | ||
| } | ||
|
|
||
| FilteredDeviceModel { | ||
| id: evcsDevices | ||
| objectName: "PageControllableLoads.evcsDevices" | ||
| serviceTypes: ["evcharger"] | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
|
|
||
| /* | ||
| ** Copyright (C) 2026 Victron Energy B.V. | ||
| ** See LICENSE.txt for license information. | ||
| */ | ||
|
|
||
| import QtQuick | ||
| import Victron.VenusOS | ||
|
|
||
| Page { | ||
| id: root | ||
|
|
||
| required property Device device | ||
|
|
||
| GradientListView { | ||
| model: VisibleItemModel { | ||
| ListQuantityField { | ||
| unit: VenusOS.Units_Watt | ||
| //% "Expected power consumption" | ||
| text: qsTrId("pagecontrollableloads_expected_power_consumption") | ||
| dataItem.uid: device.serviceUid + "/S2/0/RmSettings/PowerSetting" | ||
| } | ||
| ListQuantityField { | ||
| unit: VenusOS.Units_Time_Second | ||
| //% "Minimum run duration when turned on" | ||
| text: qsTrId("pagecontrollableloads_minimum_run_duration") | ||
| dataItem.uid: device.serviceUid + "/S2/0/RmSettings/OffHysteresis" | ||
| } | ||
| ListQuantityField { | ||
| unit: VenusOS.Units_Time_Second | ||
| //% "Minimum rest duration when turned off" | ||
| text: qsTrId("pagecontrollableloads_minimum_rest_duration") | ||
| dataItem.uid: device.serviceUid + "/S2/0/RmSettings/OnHysteresis" | ||
| } | ||
| } | ||
| } | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a new JSON file for an
opportunityloadsservice todata/mock/conf/services/and add the file todata/mock/conf/maximal.json? Then we can easily choose whether to load the service for different configurations.