Skip to content

Commit

Permalink
Add PV inverters support #346
Browse files Browse the repository at this point in the history
Add solar data from devices at com.victronenergy.pvinverter.* on D-Bus
and mqtt/pvinverter on MQTT. The total solar output on the Brief page
now shows the combined output from PV chargers and PV inverters, and
PV inverters are also listed in the solar device list that is shown
when the solar overview widget is clicked.

Adjust the UI to accommodate for differences between PV inverters and
the PV chargers that were already supported. The main difference is
that PV inverters do not have solar history data. So:

- The solar gauge on the Brief page now shows runtime history from
approximately the last 15 mintues, instead of from the last few days
- The solar widget on the Overview page now shows PV inverter phase
data instead of history data, if no PV chargers are present
  • Loading branch information
blammit committed Jul 13, 2023
1 parent bcf8693 commit 61060dc
Show file tree
Hide file tree
Showing 24 changed files with 777 additions and 77 deletions.
1 change: 1 addition & 0 deletions Global.qml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ QtObject {
property var generators
property var inverters
property var notifications
property var pvInverters
property var relays
property var solarChargers
property var system
Expand Down
19 changes: 19 additions & 0 deletions components/SolarMeasurements.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
** Copyright (C) 2023 Victron Energy B.V.
*/

import QtQuick
import Victron.VenusOS

//
// Provides combined solar measurement data from PV chargers and PV inverters.
//
QtObject {
id: root

readonly property real power: isNaN(Global.solarChargers.power) && isNaN(Global.pvInverters.power) ? NaN
: (Global.solarChargers.power || 0) + (Global.pvInverters.power || 0)

readonly property real current: isNaN(Global.solarChargers.current) && isNaN(Global.pvInverters.current) ? NaN
: (Global.solarChargers.current || 0) + (Global.pvInverters.current || 0)
}
46 changes: 40 additions & 6 deletions components/SolarYieldGauge.qml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ Item {
Repeater {
id: gaugeRepeater

model: SolarYieldModel {
id: yieldModel
dayRange: [0, Theme.geometry.briefPage.solarHistoryGauge.maximumGaugeCount + 1]
}
model: powerSampler.sampledAverages.length + 1

delegate: ScaledArcGauge {
width: Theme.geometry.briefPage.edgeGauge.width
Expand All @@ -36,7 +33,38 @@ Item {
direction: PathArc.Counterclockwise
strokeWidth: Theme.geometry.arc.strokeWidth
arcY: root.alignment & Qt.AlignVCenter ? undefined : -radius + strokeWidth/2
value: yieldModel.maximumYield ? Utils.scaleToRange(model.yieldKwh, 0, yieldModel.maximumYield, 0, 100) : 100
value: {
// First gauge shows the current runtime power, other gauges show historical values.
const power = model.index === 0 ? solarMeasurements.power : powerSampler.sampledAverages[model.index - 1]
return Utils.scaleToRange(power, 0, solarMeasurements.maxPower, 0, 100)
}
}
}

// Take 30-second samples of the solar power. Every 5 minutes, take the average of these samples
// and add a new gauge bar with that value.
Timer {
id: powerSampler

property var sampledAverages: []
property var _activeSamples: []

running: true
repeat: true
interval: 30 * 1000
onTriggered: {
_activeSamples.push(solarMeasurements.power)
if (_activeSamples.length < 10) {
return
}
const averagePower = _activeSamples.reduce((a, b) => a + b) / _activeSamples.length
let newAverages = sampledAverages
newAverages.unshift(averagePower)
if (newAverages.length >= Theme.geometry.briefPage.solarHistoryGauge.maximumGaugeCount) {
newAverages.pop()
}
_activeSamples = []
sampledAverages = newAverages
}
}

Expand All @@ -45,7 +73,13 @@ Item {

alignment: root.alignment
icon.source: "qrc:/images/solaryield.svg"
quantityLabel.dataObject: Global.solarChargers
quantityLabel.dataObject: SolarMeasurements {
id: solarMeasurements

property real maxPower: NaN

onPowerChanged: maxPower = isNaN(maxPower) ? power : Math.max(maxPower, power)
}
}
}

31 changes: 25 additions & 6 deletions components/widgets/SolarYieldWidget.qml
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,47 @@ OverviewWidget {
border.width: enabled ? Theme.geometry.overviewPage.widget.border.width : 0
border.color: Theme.color.overviewPage.widget.border

quantityLabel.dataObject: Global.solarChargers
quantityLabel.dataObject: SolarMeasurements {}
enabled: Global.solarChargers.model.count || Global.pvInverters.model.count

// Solar yield history is only available for PV chargers, and phase data is only available for
// PV inverters. So, if there is a single PV inverter and no PV chargers, then show the PV
// inverter's phase data; otherwise, show the solar history.
extraContent.children: [
SolarYieldGraph {
ThreePhaseDisplay {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: Theme.geometry.overviewPage.widget.solar.graph.margins
}
visible: root.size >= VenusOS.OverviewWidget_Size_L
visible: root.size >= VenusOS.OverviewWidget_Size_L && model != null
model: Global.solarChargers.model.count === 0 && Global.pvInverters.model.count === 1
? Global.pvInverters.model.objectAt(0).phases
: null
width: parent.width - (2 * Theme.geometry.overviewPage.widget.content.horizontalMargin)
},
SolarYieldGraph {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: Theme.geometry.overviewPage.widget.extraContent.bottomMargin
}
visible: root.size >= VenusOS.OverviewWidget_Size_L && Global.pvInverters.model.count === 0
width: parent.width - Theme.geometry.overviewPage.widget.solar.graph.margins*2
height: parent.height - (2 * Theme.geometry.overviewPage.widget.solar.graph.margins)
}
]

MouseArea {
anchors.fill: parent
enabled: Global.solarChargers.model.count > 0
onClicked: {
if (Global.solarChargers.model.count === 1) {
const singleDeviceOnly = (Global.solarChargers.model.count + Global.pvInverters.model.count) === 1
if (singleDeviceOnly && Global.solarChargers.model.count === 1) {
Global.pageManager.pushPage("/pages/solar/SolarChargerPage.qml",
{ "solarCharger": Global.solarChargers.model.get(0).solarCharger })
{ "solarCharger": Global.solarChargers.model.objectAt(0) })
} else if (singleDeviceOnly && Global.pvInverters.model === 1) {
Global.pageManager.pushPage("/pages/solar/PvInverterPage.qml",
{ "pvInverter": Global.pvInverters.model.objectAt(0) })
} else {
Global.pageManager.pushPage("/pages/solar/SolarDeviceListPage.qml", { "title": root.title })
}
Expand Down
2 changes: 2 additions & 0 deletions data/DataManager.qml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Item {
&& !!Global.generators
&& !!Global.inverters
&& !!Global.notifications
&& !!Global.pvInverters
&& !!Global.relays
&& !!Global.solarChargers
&& !!Global.system
Expand Down Expand Up @@ -80,6 +81,7 @@ Item {
Generators {}
Inverters {}
Notifications {}
PvInverters {}
Relays {}
SolarChargers {}
System {}
Expand Down
93 changes: 93 additions & 0 deletions data/PvInverters.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
** Copyright (C) 2022 Victron Energy B.V.
*/

import QtQuick
import Victron.VenusOS
import "common"

QtObject {
id: root

property DeviceModel model: DeviceModel {
objectProperty: "pvInverter"
}

property real energy: NaN
property real power: NaN
property real current: NaN
property real voltage: NaN

function addInverter(inverter) {
if (model.addObject(inverter)) {
updateTotalMeasurements()
}
}

function removeInverter(inverter) {
if (model.removeObject(inverter.serviceUid)) {
updateTotalMeasurements()
}
}

function updateTotalMeasurements() {
const totals = model.totalValues(["energy", "power", "current", "voltage"])
energy = totals.energy
power = totals.power
current = totals.current
voltage = totals.voltage
}

function reset() {
model.clear()
updateTotalMeasurements()
}

function statusCodeToText(statusCode) {
switch (statusCode) {
case VenusOS.PvInverter_StatusCode_Startup0:
//% "Startup 0"
return qsTrId("pvinverters_statusCode_startup_0")
case VenusOS.PvInverter_StatusCode_Startup1:
//% "Startup 1"
return qsTrId("pvinverters_statusCode_startup_1")
case VenusOS.PvInverter_StatusCode_Startup2:
//% "Startup 2"
return qsTrId("pvinverters_statusCode_startup_2")
case VenusOS.PvInverter_StatusCode_Startup3:
//% "Startup 3"
return qsTrId("pvinverters_statusCode_startup_3")
case VenusOS.PvInverter_StatusCode_Startup4:
//% "Startup 4"
return qsTrId("pvinverters_statusCode_startup_4")
case VenusOS.PvInverter_StatusCode_Startup5:
//% "Startup 5"
return qsTrId("pvinverters_statusCode_startup_5")
case VenusOS.PvInverter_StatusCode_Startup6:
//% "Startup 6"
return qsTrId("pvinverters_statusCode_startup_6")
case VenusOS.PvInverter_StatusCode_Running:
//% "Running"
return qsTrId("pvinverters_statusCode_running")
case VenusOS.PvInverter_StatusCode_Standby:
//% "Standby"
return qsTrId("pvinverters_statusCode_standby")
case VenusOS.PvInverter_StatusCode_BootLoading:
//% "Boot loading"
return qsTrId("pvinverters_statusCode_boot_loading")
case VenusOS.PvInverter_StatusCode_Error:
//% "Error"
return qsTrId("pvinverters_statusCode_error")
case VenusOS.PvInverter_StatusCode_RunningMPPT:
//% "Running (MPPT)"
return qsTrId("pvinverters_statusCode_running_mppt")
case VenusOS.PvInverter_StatusCode_RunningThrottled:
//% "Running (Throttled)"
return qsTrId("pvinverters_statusCode_running_throttled")
default:
return ""
}
}

Component.onCompleted: Global.pvInverters = root
}
22 changes: 22 additions & 0 deletions data/common/DeviceModel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ ListModel {
return get(index)[objectProperty]
}

function totalValues(propertyNames) {
let totals = {}
let propertyIndex
for (propertyIndex = 0; propertyIndex < propertyNames.length; ++propertyIndex) {
totals[propertyNames[propertyIndex]] = NaN
}
for (let i = 0; i < count; ++i) {
const object = objectAt(i)
for (propertyIndex = 0; propertyIndex < propertyNames.length; ++propertyIndex) {
const propertyName = propertyNames[propertyIndex]
const value = object[propertyName]
if (!isNaN(value)) {
if (isNaN(totals[propertyName])) {
totals[propertyName] = 0
}
totals[propertyName] += value
}
}
}
return totals
}

function _refreshFirstObject() {
firstObject = count === 0 ? null : objectAt(0)
}
Expand Down
95 changes: 95 additions & 0 deletions data/common/PvInverter.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
** Copyright (C) 2023 Victron Energy B.V.
*/

import QtQuick
import Victron.VenusOS
import Victron.Veutil

Device {
id: pvInverter

readonly property int statusCode: _statusCode.value === undefined ? -1 : _statusCode.value
readonly property int errorCode: _errorCode.value === undefined ? -1 : _errorCode.value

readonly property real energy: _energy.value === undefined ? NaN : _energy.value
readonly property real power: _power.value === undefined ? NaN : _power.value
readonly property real current: _current.value === undefined ? NaN : _current.value
readonly property real voltage: _voltage.value === undefined ? NaN : _voltage.value

readonly property ListModel phases: ListModel {
function setPhaseProperty(index, propertyName, value) {
if (index >= 0 && index < count) {
setProperty(index, propertyName, value === undefined ? NaN : value)
} else {
console.warn("setPhaseProperty(): bad index", index, "count is", count)
}
}

Component.onCompleted: {
const phaseCount = 3
for (let i = 0; i < phaseCount; ++i) {
append({ name: "L" + (i + 1), energy: NaN, power: NaN, current: NaN, voltage: NaN })
}
_phaseObjects.model = phaseCount
}
}

readonly property Instantiator _phaseObjects: Instantiator {
delegate: QtObject {
readonly property VeQuickItem _phaseEnergy: VeQuickItem {
uid: pvInverter.serviceUid + "/Ac/L" + (model.index + 1) + "/Energy/Forward"
onValueChanged: phases.setPhaseProperty(model.index, "energy", value)
}
readonly property VeQuickItem _phasePower: VeQuickItem {
uid: pvInverter.serviceUid + "/Ac/L" + (model.index + 1) + "/Power"
onValueChanged: phases.setPhaseProperty(model.index, "power", value)
}
readonly property VeQuickItem _phaseCurrent: VeQuickItem {
uid: pvInverter.serviceUid + "/Ac/L" + (model.index + 1) + "/Current"
onValueChanged: phases.setPhaseProperty(model.index, "current", value)
}
readonly property VeQuickItem _phaseVoltage: VeQuickItem {
uid: pvInverter.serviceUid + "/Ac/L" + (model.index + 1) + "/Voltage"
onValueChanged: phases.setPhaseProperty(model.index, "voltage", value)
}
}
}

readonly property VeQuickItem _statusCode: VeQuickItem {
uid: pvInverter.serviceUid + "/StatusCode"
}

readonly property VeQuickItem _errorCode: VeQuickItem {
uid: pvInverter.serviceUid + "/ErrorCode"
}

readonly property VeQuickItem _energy: VeQuickItem {
uid: pvInverter.serviceUid + "/Ac/Energy/Forward"
onValueChanged: Qt.callLater(Global.pvInverters.updateTotalMeasurements)
}

readonly property VeQuickItem _power: VeQuickItem {
uid: pvInverter.serviceUid + "/Ac/Power"
onValueChanged: Qt.callLater(Global.pvInverters.updateTotalMeasurements)
}

readonly property VeQuickItem _current: VeQuickItem {
uid: pvInverter.serviceUid + "/Ac/Current"
onValueChanged: Qt.callLater(Global.pvInverters.updateTotalMeasurements)
}

readonly property VeQuickItem _voltage: VeQuickItem {
uid: pvInverter.serviceUid + "/Ac/Voltage"
onValueChanged: Qt.callLater(Global.pvInverters.updateTotalMeasurements)
}

property bool _valid: deviceInstance.value !== undefined
on_ValidChanged: {
if (_valid) {
Global.pvInverters.addInverter(pvInverter)
} else {
Global.pvInverters.removeInverter(pvInverter)
}
}
}
1 change: 1 addition & 0 deletions data/dbus/DBusDataManager.qml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ QtObject {
property var generators: GeneratorsImpl { }
property var inverters: InvertersImpl { }
property var notifications: NotificationsImpl {}
property var pvInverters: PvInvertersImpl { }
property var relays: RelaysImpl {}
property var solarChargers: SolarChargersImpl { }
property var system: SystemImpl { }
Expand Down
Loading

0 comments on commit 61060dc

Please sign in to comment.