From 949843281a611429d95e29430b8bafc9cccabdbb Mon Sep 17 00:00:00 2001 From: Bea Lam Date: Thu, 23 Jan 2025 18:32:36 +1000 Subject: [PATCH] Rework trackers and improve SolarDeviceListPage performance Add SolarInputModel to store the data for each tracker and PV inverter entry in the list. This allows the ListView to just create one list item for each entry in the model, instead of creating complex delegates with Column+Repeater objects when showing multiple trackers for a single solarcharger/multi/inverter service. Also, the model sorts its items to allow the use of ListView sections for creating the column headers. This makes the list load and scroll more quickly, especially when it contains a large number of trackers. Also, remove the tracker list from SolarDevice.qml as it is only required in specific pages, and those can load their own SolarTracker objects when needed. Fixes #1848 --- CMakeLists.txt | 3 + data/common/SolarDevice.qml | 74 +------ data/common/SolarTracker.qml | 33 +++ data/mock/SolarDevicesImpl.qml | 9 +- pages/settings/devicelist/rs/PageMultiRs.qml | 25 +-- pages/solar/PageSolarCharger.qml | 13 +- pages/solar/SolarDeviceListPage.qml | 219 +++++++++++-------- pages/solar/SolarDevicePage.qml | 36 ++- src/solarinputmodel.cpp | 168 ++++++++++++++ src/solarinputmodel.h | 86 ++++++++ 10 files changed, 473 insertions(+), 193 deletions(-) create mode 100644 data/common/SolarTracker.qml create mode 100644 src/solarinputmodel.cpp create mode 100644 src/solarinputmodel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c4853156..7f4e49106 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -424,6 +424,7 @@ set (VENUS_QML_MODULE_SOURCES data/common/SolarDevice.qml data/common/SolarHistory.qml data/common/SolarHistoryErrorModel.qml + data/common/SolarTracker.qml data/common/SolarTrackerDailyHistory.qml data/common/SystemBattery.qml data/common/Tank.qml @@ -704,6 +705,8 @@ list(APPEND VenusQMLModule_CPP_SOURCES src/units.cpp src/screenblanker.h src/screenblanker.cpp + src/solarinputmodel.h + src/solarinputmodel.cpp src/widgetconnectorpathupdater.h src/widgetconnectorpathupdater.cpp ) diff --git a/data/common/SolarDevice.qml b/data/common/SolarDevice.qml index 01cba3be8..a0e7bad9f 100644 --- a/data/common/SolarDevice.qml +++ b/data/common/SolarDevice.qml @@ -13,14 +13,18 @@ import Victron.VenusOS Device { id: root - readonly property ListModel trackers: ListModel {} readonly property real power: _totalPower.isValid ? _totalPower.value : NaN readonly property alias history: _history + // For solarcharger services, we can assume trackerCount=1 if /NrOfTrackers is not set. + readonly property int trackerCount: _nrOfTrackers.isValid ? _nrOfTrackers.value : (_isSolarCharger ? 1 : 0) + // This is the overall error history. // For the per-day error history, use dailyHistory(day).errorModel readonly property alias errorModel: _history.errorModel + readonly property bool _isSolarCharger: BackendConnection.serviceTypeFromUid(serviceUid) === "solarcharger" + signal yieldUpdatedForDay(day: int, yieldKwh: real) function dailyHistory(day) { @@ -31,80 +35,22 @@ Device { return _history.dailyTrackerHistory(day, trackerIndex) } - function trackerName(trackerIndex, format) { - const tracker = _trackerObjects.objectAt(trackerIndex) - const trackerName = tracker ? tracker.name || "" : "" - return Global.solarDevices.formatTrackerName(trackerName, trackerIndex, trackers.count, root.name, format) - } - //--- internal members below --- readonly property VeQuickItem _totalPower: VeQuickItem { uid: root.serviceUid + "/Yield/Power" } + readonly property VeQuickItem _nrOfTrackers: VeQuickItem { + uid: root.serviceUid + "/NrOfTrackers" + } + //--- history --- readonly property SolarHistory _history: SolarHistory { id: _history bindPrefix: root.serviceUid deviceName: root.name - trackerCount: root.trackers.count - } - - //--- Solar trackers --- - - readonly property VeQuickItem _trackerCount: VeQuickItem { - uid: root.serviceUid + "/NrOfTrackers" - } - - readonly property Instantiator _trackerObjects: Instantiator { - model: _trackerCount.value || 1 // there is always at least one tracker, even if NrOfTrackers is not set - delegate: QtObject { - id: tracker - - readonly property int modelIndex: model.index - readonly property real power: root.trackers.count <= 1 ? root.power : _power.value || 0 - readonly property real voltage: _voltage.value || 0 - readonly property real current: isNaN(power) || isNaN(voltage) || voltage === 0 ? NaN : power / voltage - readonly property string name: _name.value || "" - - readonly property VeQuickItem _voltage: VeQuickItem { - uid: root.trackers.count <= 1 - ? root.serviceUid + "/Pv/V" - : root.serviceUid + "/Pv/" + model.index + "/V" - } - - readonly property VeQuickItem _power: VeQuickItem { - uid: root.trackers.count === 1 - ? "" // only 1 tracker, use root.power instead (i.e. same as /Yield/Power) - : root.serviceUid + "/Pv/" + model.index + "/P" - } - - readonly property VeQuickItem _name: VeQuickItem { - uid: root.serviceUid + "/Pv/" + model.index + "/Name" - } - } - - onObjectAdded: function(index, object) { - let insertionIndex = root.trackers.count - for (let i = 0; i < root.trackers.count; ++i) { - const sortIndex = root.trackers.get(i).solarTracker.modelIndex - if (index < sortIndex) { - insertionIndex = i - break - } - } - root.trackers.insert(insertionIndex, {"solarTracker": object}) - } - - onObjectRemoved: function(index, object) { - for (let i = 0; i < root.trackers.count; ++i) { - if (root.trackers.get(i).solarTracker.serviceUid === object.serviceUid) { - root.trackers.remove(i) - break - } - } - } + trackerCount: root.trackerCount } } diff --git a/data/common/SolarTracker.qml b/data/common/SolarTracker.qml new file mode 100644 index 000000000..160205b71 --- /dev/null +++ b/data/common/SolarTracker.qml @@ -0,0 +1,33 @@ +/* +** Copyright (C) 2025 Victron Energy B.V. +** See LICENSE.txt for license information. +*/ + +import QtQuick +import Victron.VenusOS + +QtObject { + id: root + + required property SolarDevice device + required property int trackerIndex + + readonly property string name: _name.value ?? "" + readonly property real power: _power.isValid ? _power.value : NaN + readonly property real voltage: _voltage.isValid ? _voltage.value : NaN + readonly property real current: !power || !voltage ? NaN : power / voltage + + // If there is only 1 tracker (e.g. all common MPPTs), the voltage and power are provided via + // /Pv/V and /Yield/Power instead of /Pv/0/V and /Pv/0/P. + readonly property VeQuickItem _voltage: VeQuickItem { + uid: root.device.trackerCount <= 1 ? `${root.device.serviceUid}/Pv/V` : `${root.device.serviceUid}/Pv/${root.trackerIndex}/V` + } + + readonly property VeQuickItem _power: VeQuickItem { + uid: root.device.trackerCount <= 1 ? `${root.device.serviceUid}/Yield/Power` : `${root.device.serviceUid}/Pv/${root.trackerIndex}/P` + } + + readonly property VeQuickItem _name: VeQuickItem { + uid: root.device.trackerCount <= 1 ? "" : `${root.device.serviceUid}/Pv/${root.trackerIndex}/Name` + } +} diff --git a/data/mock/SolarDevicesImpl.qml b/data/mock/SolarDevicesImpl.qml index 8a3785c25..dd98f83a5 100644 --- a/data/mock/SolarDevicesImpl.qml +++ b/data/mock/SolarDevicesImpl.qml @@ -38,6 +38,10 @@ QtObject { Global.mockDataSimulator.setMockValue(serviceUid + path, value) } + function mockValue(path) { + return Global.mockDataSimulator.mockValue(serviceUid + path) + } + function randomizeMeasurments() { /* 1) a solar charger with one tracker has 3 paths: @@ -57,8 +61,9 @@ QtObject { */ let totalPower = 0 - if (_trackerCount.value > 1) { - for (let i = 0; i < _trackerCount.value; ++i) { + const trackerCount = mockValue("/NrOfTrackers") + if (trackerCount > 1) { + for (let i = 0; i < trackerCount; ++i) { const p = Math.random() * 100 const trackerUid = serviceUid + "/Pv/" + i Global.mockDataSimulator.setMockValue(trackerUid + "/V", 90 + (Math.random() * 10)) diff --git a/pages/settings/devicelist/rs/PageMultiRs.qml b/pages/settings/devicelist/rs/PageMultiRs.qml index 54eac6c83..350e8cf08 100644 --- a/pages/settings/devicelist/rs/PageMultiRs.qml +++ b/pages/settings/devicelist/rs/PageMultiRs.qml @@ -12,10 +12,11 @@ Page { property string bindPrefix readonly property bool multiPhase: numberOfPhases.isValid && numberOfPhases.value >= 2 && !_phase.isValid readonly property int trackerCount: numberOfTrackers.value || 0 + readonly property alias solarDevice: device title: device.name - Device { + SolarDevice { id: device serviceUid: root.bindPrefix } @@ -117,14 +118,7 @@ Page { preferredVisible: root.trackerCount > 0 onClicked: { Global.pageManager.pushPage("/pages/solar/SolarHistoryPage.qml", - { "solarHistory": solarHistory }) - } - - SolarHistory { - id: solarHistory - bindPrefix: root.bindPrefix - deviceName: root.title - trackerCount: root.trackerCount + { "solarHistory": root.device.history }) } } @@ -253,16 +247,11 @@ Page { Instantiator { id: trackerObjects model: root.trackerCount - delegate: QtObject { + delegate: SolarTracker { required property int index - readonly property real power: _power.isValid ? _power.value : NaN - readonly property real voltage: _voltage.isValid ? _voltage.value : NaN - readonly property real current: !_power.isValid || !_voltage.isValid || voltage === 0 ? NaN : power / voltage - readonly property string name: _name.value || "" - - readonly property VeQuickItem _voltage: VeQuickItem { uid: root.bindPrefix + "/Pv/" + index + "/V" } - readonly property VeQuickItem _power: VeQuickItem { uid: root.bindPrefix + "/Pv/" + index + "/P" } - readonly property VeQuickItem _name: VeQuickItem { uid: root.bindPrefix + "/Pv/" + index + "/Name" } + + device: root.solarDevice + trackerIndex: index } } } diff --git a/pages/solar/PageSolarCharger.qml b/pages/solar/PageSolarCharger.qml index d20490d01..ba4fbf8c1 100644 --- a/pages/solar/PageSolarCharger.qml +++ b/pages/solar/PageSolarCharger.qml @@ -161,7 +161,7 @@ Page { { title: CommonWords.power_watts, unit: VenusOS.Units_Watt } ] valueForModelIndex: function(trackerIndex, column) { - const tracker = root.solarDevice.trackers.get(trackerIndex).solarTracker + const tracker = trackerObjects.objectAt(trackerIndex) if (column === 0) { return Global.solarDevices.formatTrackerName(tracker.name, trackerIndex, root.trackerCount, root.solarDevice.name, VenusOS.TrackerName_NoDevicePrefix) } else if (column === 1) { @@ -172,6 +172,17 @@ Page { return tracker.power } } + + Instantiator { + id: trackerObjects + model: root.solarDevice.trackerCount + delegate: SolarTracker { + required property int index + + device: root.solarDevice + trackerIndex: index + } + } } ListQuantityGroup { diff --git a/pages/solar/SolarDeviceListPage.qml b/pages/solar/SolarDeviceListPage.qml index bb62f42f6..33f90139d 100644 --- a/pages/solar/SolarDeviceListPage.qml +++ b/pages/solar/SolarDeviceListPage.qml @@ -9,120 +9,147 @@ import Victron.VenusOS Page { id: root + // Adds an input to the list. This should be called whenever an input's name changes; if the + // input is already in the list, it is removed and re-inserted. This allows the list sorting to + // be preserved (since the name affects the sort order) without re-sorting the entire list. + function _addSolarInput(serviceUid, inputValues, trackerIndex) { + const inputIndex = solarInputModel.indexOf(serviceUid, trackerIndex) + if (inputIndex >= 0) { + solarInputModel.removeAt(inputIndex) + } + solarInputModel.addInput(serviceUid, inputValues, trackerIndex) + } + // A list of all PV arrays. For solar chargers, each tracker for the charger is an individual // entry in the list. For PV inverters, each inverter is an entry in the list, since inverters // do not have multiple trackers. GradientListView { - id: chargerListView - - // If there are both PV chargers and PV inverters, the ListView headerItem will be the - // 'PV chargers' header, and one of the list delegates will be the 'PV inverters' header - // row instead of a row containing the quantity measurements. - // If there are only PV chargers or only PV inverters, only the ListView headerItem is - // required, and no additional header is needed. - readonly property int extraHeaderCount: Global.solarDevices.model.count > 0 && Global.pvInverters.model.count > 0 ? 1 : 0 - - header: listHeaderComponent - model: Global.solarDevices.model.count + Global.pvInverters.model.count + extraHeaderCount - - delegate: Loader { - width: parent ? parent.width : 0 - height: Math.max(item ? item.implicitHeight : 0, Theme.geometry_listItem_height) - sourceComponent: { - if (Global.solarDevices.model.count > 0 - && Global.pvInverters.model.count > 0 - && model.index === Global.solarDevices.model.count) { - return listHeaderComponent - } - if (model.index < Global.solarDevices.model.count) { - return solarChargerRowComponent - } - return pvInverterRowComponent - } + model: SolarInputModel { + id: solarInputModel + } + delegate: ListQuantityGroupNavigation { + required property int index + required property string serviceUid + required property string name + required property real todaysYield + required property real energy + required property real power + required property real voltage + required property real current + readonly property string serviceType: BackendConnection.serviceTypeFromUid(serviceUid) + + text: name + tableMode: true + quantityModel: [ + { value: serviceType === "pvinverter" ? energy : todaysYield, unit: VenusOS.Units_Energy_KiloWattHour }, + { value: voltage, unit: VenusOS.Units_Volt_DC }, + { value: current, unit: VenusOS.Units_Amp }, + { value: power, unit: VenusOS.Units_Watt }, + ] - onLoaded: { - if (sourceComponent === listHeaderComponent) { - item.chargerMode = false + onClicked: { + if (serviceType === "pvinverter") { + const pvInverter = Global.pvInverters.model.deviceAt(Global.pvInverters.model.indexOf(serviceUid)) + Global.pageManager.pushPage("/pages/solar/PvInverterPage.qml", { pvInverter: pvInverter }) + } else { + const solarDevice = Global.solarDevices.model.deviceAt(Global.solarDevices.model.indexOf(serviceUid)) + Global.pageManager.pushPage("/pages/solar/SolarDevicePage.qml", { solarDevice: solarDevice }) } } + } - Component { - id: solarChargerRowComponent - - Column { - readonly property QtObject solarDevice: Global.solarDevices.model.deviceAt(model.index) - - width: parent.width - - Repeater { - model: solarDevice.trackers - delegate: ListQuantityGroupNavigation { - readonly property real yieldToday: { - const historyToday = solarDevice.trackers.count > 1 - ? solarDevice.dailyTrackerHistory(0, model.index) - : solarDevice.dailyHistory(0) - return historyToday ? historyToday.yieldKwh : NaN - } - - text: solarDevice.trackerName(model.index, VenusOS.TrackerName_WithDevicePrefix) - quantityModel: [ - { value: yieldToday, unit: VenusOS.Units_Energy_KiloWattHour }, - { value: modelData.voltage, unit: VenusOS.Units_Volt_DC }, - { value: modelData.current, unit: VenusOS.Units_Amp }, - { value: modelData.power, unit: VenusOS.Units_Watt }, - ] - tableMode: true - - onClicked: { - Global.pageManager.pushPage("/pages/solar/SolarDevicePage.qml", { "solarDevice": solarDevice }) - } - } - } - } - } + section.property: "group" + section.delegate: QuantityGroupListHeader { + required property string section - Component { - id: pvInverterRowComponent + firstColumnText: section === "pvinverter" ? CommonWords.pv_inverter : "" + quantityTitleModel: [ + { text: section === "pvinverter" ? CommonWords.energy : CommonWords.yield_today, unit: VenusOS.Units_Energy_KiloWattHour }, + { text: CommonWords.voltage, unit: section === "pvinverter" ? VenusOS.Units_Volt_AC : VenusOS.Units_Volt_DC }, + { text: CommonWords.current_amps, unit: VenusOS.Units_Amp }, + { text: CommonWords.power_watts, unit: VenusOS.Units_Watt }, + ] + } + } - ListQuantityGroupNavigation { - readonly property QtObject pvInverter: { - let pvInverterIndex = model.index - Global.solarDevices.model.count - chargerListView.extraHeaderCount - return Global.pvInverters.model.deviceAt(pvInverterIndex) + Instantiator { + model: Global.solarDevices.model + delegate: Instantiator { + id: solarDeviceDelegate + + required property var device + + readonly property Instantiator trackerObjects: Instantiator { + model: solarDeviceDelegate.device.trackerCount + delegate: SolarTracker { + required property int index + readonly property real todaysYield: { + const historyToday = device.trackerCount > 1 + ? device.dailyTrackerHistory(0, index) + : device.dailyHistory(0) + return historyToday?.yieldKwh ?? NaN } - - text: pvInverter.name - quantityModel: [ - { value: pvInverter.energy, unit: VenusOS.Units_Energy_KiloWattHour }, - { value: pvInverter.voltage, unit: VenusOS.Units_Volt_AC }, - { value: pvInverter.current, unit: VenusOS.Units_Amp }, - { value: pvInverter.power, unit: VenusOS.Units_Watt }, - ] - tableMode: true - - onClicked: { - Global.pageManager.pushPage("/pages/solar/PvInverterPage.qml", { "pvInverter": pvInverter }) + readonly property string formattedName: Global.solarDevices.formatTrackerName( + name, index, device.trackerCount, device.name, VenusOS.TrackerName_WithDevicePrefix) + + device: solarDeviceDelegate.device + trackerIndex: index + + onTodaysYieldChanged: solarInputModel.setInputValue(device.serviceUid, SolarInputModel.TodaysYieldRole, todaysYield, trackerIndex) + onPowerChanged: solarInputModel.setInputValue(device.serviceUid, SolarInputModel.PowerRole, power, trackerIndex) + onCurrentChanged: solarInputModel.setInputValue(device.serviceUid, SolarInputModel.CurrentRole, current, trackerIndex) + onVoltageChanged: solarInputModel.setInputValue(device.serviceUid, SolarInputModel.VoltageRole, voltage, trackerIndex) + + onFormattedNameChanged: { + const values = { + group: "generic", + name: formattedName, + todaysYield: todaysYield, + power: power, + current: current, + voltage: voltage + } + root._addSolarInput(device.serviceUid, values, trackerIndex) } } + + onObjectRemoved: (index, object) => { + solarInputModel.removeAt(solarInputModel.indexOf(device.serviceUid, object.index)) + } } } } - Component { - id: listHeaderComponent - - QuantityGroupListHeader { - property bool chargerMode: Global.solarDevices.model.count > 0 + Instantiator { + model: Global.pvInverters.model + delegate: QtObject { + required property var device + readonly property string name: device.name + readonly property real energy: device.energy + readonly property real power: device.power + readonly property real current: device.current + readonly property real voltage: device.voltage + + onEnergyChanged: solarInputModel.setInputValue(device.serviceUid, SolarInputModel.EnergyRole, energy) + onPowerChanged: solarInputModel.setInputValue(device.serviceUid, SolarInputModel.PowerRole, power) + onCurrentChanged: solarInputModel.setInputValue(device.serviceUid, SolarInputModel.CurrentRole, current) + onVoltageChanged: solarInputModel.setInputValue(device.serviceUid, SolarInputModel.VoltageRole, voltage) + + onNameChanged: { + const values = { + group: "pvinverter", + name: name, + energy: energy, + power: power, + current: current, + voltage: voltage + } + root._addSolarInput(device.serviceUid, values, 0) + } + } - firstColumnText: chargerMode - //% "PV Charger" - ? qsTrId("solardevices_pv_charger") - : CommonWords.pv_inverter - quantityTitleModel: [ - { text: chargerMode ? CommonWords.yield_today : CommonWords.energy, unit: VenusOS.Units_Energy_KiloWattHour }, - { text: CommonWords.voltage, unit: chargerMode ? VenusOS.Units_Volt_DC : VenusOS.Units_Volt_AC }, - { text: CommonWords.current_amps, unit: VenusOS.Units_Amp }, - { text: CommonWords.power_watts, unit: VenusOS.Units_Watt }, - ] + onObjectRemoved: (index, object) => { + solarInputModel.removeAt(solarInputModel.indexOf(device.serviceUid)) } } } diff --git a/pages/solar/SolarDevicePage.qml b/pages/solar/SolarDevicePage.qml index 4ba0dac55..5d248d6e7 100644 --- a/pages/solar/SolarDevicePage.qml +++ b/pages/solar/SolarDevicePage.qml @@ -10,7 +10,7 @@ Page { id: root required property SolarDevice solarDevice - readonly property QtObject singleTracker: solarDevice.trackers.count === 1 ? solarDevice.trackers.get(0).solarTracker : null + readonly property SolarTracker overallMeasurements: trackerObjects.count === 1 ? trackerObjects.objectAt(0) : null title: solarDevice.name @@ -39,17 +39,17 @@ Page { unit: VenusOS.Units_Energy_KiloWattHour }, { - title: root.singleTracker ? CommonWords.voltage : "", - value: root.singleTracker ? root.singleTracker.voltage : NaN, - unit: root.singleTracker ? VenusOS.Units_Volt_DC : VenusOS.Units_None, + title: !!root.overallMeasurements ? CommonWords.voltage : "", + value: root.overallMeasurements?.voltage ?? NaN, + unit: !!root.overallMeasurements ? VenusOS.Units_Volt_DC : VenusOS.Units_None, }, { - title: root.singleTracker ? CommonWords.current_amps : "", - value: root.singleTracker ? root.singleTracker.current : NaN, - unit: root.singleTracker ? VenusOS.Units_Amp : VenusOS.Units_None + title: !!root.overallMeasurements ? CommonWords.current_amps : "", + value: root.overallMeasurements?.current ?? NaN, + unit: !!root.overallMeasurements ? VenusOS.Units_Amp : VenusOS.Units_None }, { - title: root.singleTracker + title: !!root.overallMeasurements ? CommonWords.pv_power //% "Total PV Power" : qsTrId("charger_total_pv_power"), @@ -63,9 +63,9 @@ Page { id: trackerTable anchors.top: trackerSummary.bottom - visible: root.solarDevice.trackers.count > 1 + visible: root.solarDevice.trackerCount > 1 - rowCount: root.solarDevice.trackers.count + rowCount: root.solarDevice.trackerCount units: [ { title: CommonWords.tracker, unit: VenusOS.Units_None }, { title: trackerSummary.model[1].title, unit: VenusOS.Units_Energy_KiloWattHour }, @@ -74,9 +74,9 @@ Page { { title: CommonWords.power_watts, unit: VenusOS.Units_Watt } ] valueForModelIndex: function(trackerIndex, column) { - const tracker = root.solarDevice.trackers.get(trackerIndex).solarTracker + const tracker = trackerObjects.objectAt(trackerIndex) if (column === 0) { - return Global.solarDevices.formatTrackerName(tracker.name, trackerIndex, root.solarDevice.trackers.count, root.solarDevice.name, VenusOS.TrackerName_NoDevicePrefix) + return Global.solarDevices.formatTrackerName(tracker.name, trackerIndex, root.solarDevice.trackerCount, root.solarDevice.name, VenusOS.TrackerName_NoDevicePrefix) } else if (column === 1) { // Today's yield for this tracker const history = root.solarDevice.dailyTrackerHistory(0, trackerIndex) @@ -89,6 +89,18 @@ Page { return tracker.power } } + + Instantiator { + id: trackerObjects + + model: root.solarDevice.trackerCount + delegate: SolarTracker { + required property int index + + device: root.solarDevice + trackerIndex: index + } + } } } diff --git a/src/solarinputmodel.cpp b/src/solarinputmodel.cpp new file mode 100644 index 000000000..37a87b34e --- /dev/null +++ b/src/solarinputmodel.cpp @@ -0,0 +1,168 @@ +/* +** Copyright (C) 2025 Victron Energy B.V. +** See LICENSE.txt for license information. +*/ + +#include "solarinputmodel.h" + +using namespace Victron::VenusOS; + +SolarInputModel::SolarInputModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +int SolarInputModel::count() const +{ + return m_inputs.count(); +} + +int SolarInputModel::insertionIndex(const Input &input) const +{ + for (int i = 0; i < m_inputs.count(); ++i) { + if (m_inputs.at(i).group > input.group) { + return i; + } else if (m_inputs.at(i).group == input.group) { + if (m_inputs.at(i).name.localeAwareCompare(input.name) > 0) { + return i; + } + } + } + return m_inputs.count(); +} + +void SolarInputModel::addInput(const QString &serviceUid, const QVariantMap &values, int trackerIndex) +{ + Input input(serviceUid, trackerIndex); + input.group = values["group"].toString(); + input.name = values["name"].toString(); + input.todaysYield = values["todaysYield"].value(); + input.energy = values["energy"].value(); + input.power = values["power"].value(); + input.current = values["current"].value(); + input.voltage = values["voltage"].value(); + + const int index = insertionIndex(input); + beginInsertRows(QModelIndex(), index, index); + m_inputs.insert(index, input); + endInsertRows(); + emit countChanged(); +} + +void SolarInputModel::setInputValue(const QString &serviceUid, Role role, const QVariant &value, int trackerIndex) +{ + const int index = indexOf(serviceUid, trackerIndex); + if (index < 0) { + return; + } + + Input &input = m_inputs[index]; + switch (role) + { + case ServiceUidRole: + qWarning() << "serviceUid is read-only"; + break; + case GroupRole: + input.group = value.toString(); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), { GroupRole }); + break; + case NameRole: + input.name = value.toString(); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), { NameRole }); + break; + case TodaysYieldRole: + input.todaysYield = value.value(); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), { TodaysYieldRole }); + break; + case EnergyRole: + input.energy = value.value(); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), { EnergyRole }); + break; + case PowerRole: + input.power = value.value(); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), { PowerRole }); + break; + case CurrentRole: + input.current = value.value(); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), { CurrentRole }); + break; + case VoltageRole: + input.voltage = value.value(); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), { VoltageRole }); + break; + default: + break; + } +} + +int SolarInputModel::indexOf(const QString &serviceUid, int trackerIndex) const +{ + for (int i = 0; i < m_inputs.count(); ++i) { + const Input &input = m_inputs.at(i); + if (input.serviceUid == serviceUid && input.trackerIndex == trackerIndex) { + return i; + } + } + return -1; +} + +void SolarInputModel::removeAt(int index) +{ + if (index >= 0 && index < count()) { + beginRemoveRows(QModelIndex(), index, index); + m_inputs.removeAt(index); + endRemoveRows(); + emit countChanged(); + } +} + +QVariant SolarInputModel::data(const QModelIndex &index, int role) const +{ + const int row = index.row(); + if (row < 0 || row >= m_inputs.count()) { + return QVariant(); + } + + const Input &input = m_inputs.at(row); + switch (role) + { + case ServiceUidRole: + return input.serviceUid; + case GroupRole: + return input.group; + case NameRole: + return input.name; + case TodaysYieldRole: + return input.todaysYield; + case EnergyRole: + return input.energy; + case PowerRole: + return input.power; + case CurrentRole: + return input.current; + case VoltageRole: + return input.voltage; + default: + return QVariant(); + } +} + +int SolarInputModel::rowCount(const QModelIndex &) const +{ + return count(); +} + +QHash SolarInputModel::roleNames() const +{ + static QHash roles = { + { ServiceUidRole, "serviceUid" }, + { GroupRole, "group" }, + { NameRole, "name" }, + { TodaysYieldRole, "todaysYield" }, + { EnergyRole, "energy" }, + { PowerRole, "power" }, + { CurrentRole, "current" }, + { VoltageRole, "voltage" }, + }; + return roles; +} diff --git a/src/solarinputmodel.h b/src/solarinputmodel.h new file mode 100644 index 000000000..7c756781c --- /dev/null +++ b/src/solarinputmodel.h @@ -0,0 +1,86 @@ +/* +** Copyright (C) 2024 Victron Energy B.V. +** See LICENSE.txt for license information. +*/ + +#ifndef SOLARINPUTMODEL_H +#define SOLARINPUTMODEL_H + +#include +#include + +namespace Victron { +namespace VenusOS { + +/* + Provides a model for solar input data. + + Solar inputs include: + - trackers from solarcharger, multi and inverter services + - pvinverter services +*/ +class SolarInputModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + enum Role { + ServiceUidRole = Qt::UserRole, + GroupRole, + NameRole, + TodaysYieldRole, + PowerRole, + CurrentRole, + VoltageRole, + EnergyRole + }; + Q_ENUM(Role) + + explicit SolarInputModel(QObject *parent = nullptr); + + int count() const; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + + Q_INVOKABLE void addInput(const QString &serviceUid, const QVariantMap &values, int trackerIndex = 0); + Q_INVOKABLE void setInputValue(const QString &serviceUid, Role role, const QVariant &value, int trackerIndex = 0); + Q_INVOKABLE int indexOf(const QString &serviceUid, int trackerIndex = 0) const; + Q_INVOKABLE void removeAt(int index); + +Q_SIGNALS: + void countChanged(); + +protected: + QHash roleNames() const override; + +private: + class Input + { + public: + Input(const QString &serviceUid, int trackerIndex) + : serviceUid(serviceUid), trackerIndex(trackerIndex) {} + + QString serviceUid; + QString group; + QString name; + qreal todaysYield = qQNaN(); + qreal energy = qQNaN(); + qreal power = qQNaN(); + qreal current = qQNaN(); + qreal voltage = qQNaN(); + int trackerIndex = 0; + }; + + int insertionIndex(const Input &input) const; + + QHash m_roleNames; + QVector m_inputs; +}; + +} /* VenusOS */ +} /* Victron */ + +#endif // SOLARINPUTMODEL_H