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