This repository has been archived on 2026-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
powersync-app/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml

427 lines
19 KiB
QML

import QtQuick 2.3
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.3
import QtCharts 2.3
import Nymea 1.0
import "qrc:/ui/components/"
StatsBase {
id: root
property EnergyManager energyManager: null
QtObject {
id: d
property BarSet consumptionSet: null
property BarSet productionSet: null
property BarSet acquisitionSet: null
property BarSet returnSet: null
}
ColumnLayout {
anchors.fill: parent
Label {
Layout.fillWidth: true
Layout.margins: Style.smallMargins
horizontalAlignment: Text.AlignHCenter
text: qsTr("Totals")
}
SelectionTabs {
id: selectionTabs
Layout.fillWidth: true
Layout.leftMargin: Style.smallMargins
Layout.rightMargin: Style.smallMargins
model: ListModel {
Component.onCompleted: {
append({modelData: qsTr("Hours"), config: "hours" })
append({modelData: qsTr("Days"), config: "days" })
append({modelData: qsTr("Weeks"), config: "weeks" })
append({modelData: qsTr("Months"), config: "months" })
append({modelData: qsTr("Years"), config: "years" })
// append({modelData: qsTr("Minutes"), config: "minutes" })
selectionTabs.currentIndex = 1
}
}
onCurrentValueChanged: {
if (currentValue === undefined) {
return
}
var config = root.configs[currentValue.config]
// print("config:", config.startTime(), config.sampleRate)
powerBalanceLogs.loadingInhibited = true
powerBalanceLogs.sampleRate = config.sampleRate
powerBalanceLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000)
powerBalanceLogs.loadingInhibited = false
barSeries.clear();
d.consumptionSet = barSeries.append(qsTr("Consumed"), [])
d.consumptionSet.color = Style.blue
d.consumptionSet.borderColor = d.consumptionSet.color
d.consumptionSet.borderWidth = 0
d.productionSet = barSeries.append(qsTr("Produced"), [])
d.productionSet.color = Style.green
d.productionSet.borderColor = d.productionSet.color
d.productionSet.borderWidth = 0
d.acquisitionSet = barSeries.append(qsTr("From grid"), [])
d.acquisitionSet.color = Style.red
d.acquisitionSet.borderColor = d.acquisitionSet.color
d.acquisitionSet.borderWidth = 0
d.returnSet = barSeries.append(qsTr("To grid"), [])
d.returnSet.color = Style.orange
d.returnSet.borderColor = d.returnSet.color
d.returnSet.borderWidth = 0
valueAxis.max = 0
}
}
Connections {
target: energyManager
onPowerBalanceChanged: {
var start = powerBalanceLogs.get(powerBalanceLogs.count - 1 )
// print("balance changed:", d.consumptionSet, powerBalanceLogs, powerBalanceLogs.count)
// print("updating", start ? start.timestamp : "", start ? start.totalConsumption : 0, root.energyManager.totalConsumption, root.energyManager.totalConsumption - (start ? start.totalConsumption : 0))
d.consumptionSet.replace(d.consumptionSet.count - 1, root.energyManager.totalConsumption - (start ? start.totalConsumption : 0))
d.productionSet.replace(d.productionSet.count - 1, root.energyManager.totalProduction - (start ? start.totalProduction : 0))
d.acquisitionSet.replace(d.acquisitionSet.count - 1, root.energyManager.totalAcquisition - (start ? start.totalAcquisition : 0))
d.returnSet.replace(d.returnSet.count - 1, root.energyManager.totalReturn - (start ? start.totalReturn : 0))
}
}
PowerBalanceLogs {
id: powerBalanceLogs
engine: _engine
loadingInhibited: true
onFetchingDataChanged: {
if (!fetchingData) {
chartView.animationOptions = ChartView.NoAnimation
// print("Logs fetched")
var config = root.configs[selectionTabs.currentValue.config]
var labels = []
var entries = []
var newestLogTimestamp = powerBalanceLogs.count > 0 ? powerBalanceLogs.get(powerBalanceLogs.count - 1).timestamp : new Date();
for (var i = 0; i < config.count; i++) {
var entry = powerBalanceLogs.get(powerBalanceLogs.count - i - 1)
// if it's the first, let's add a generated entry which shows the total from the newest log to the current live value
if (i == 0) {
var liveEntry = {
consumption: energyManager.totalConsumption,
production: energyManager.totalProduction,
acquisition: energyManager.totalAcquisition,
returned: energyManager.totalReturn
}
if (entry) {
liveEntry.consumption -= entry.totalConsumption
liveEntry.production -= entry.totalProduction
liveEntry.acquisition -= entry.totalAcquisition
liveEntry.returned -= entry.totalReturn
}
// print("Adding live entry:", liveEntry.consumption, root.energyManager.totalConsumption, entry ? entry.totalConsumption : 0)
entries.unshift(liveEntry)
valueAxis.adjustMax(liveEntry.consumption)
valueAxis.adjustMax(liveEntry.production)
valueAxis.adjustMax(liveEntry.acquisition)
valueAxis.adjustMax(liveEntry.returned)
}
// Add the actual entry
var graphEntry = {
consumption: 0,
production: 0,
acquisition: 0,
returned: 0
}
var labelTime = new Date();
if (entry) {
// print("Have entry:", entry.timestamp, config.toLabel(entry.timestamp))
var previous = powerBalanceLogs.get(powerBalanceLogs.count - i - 2)
if (previous) {
graphEntry.consumption = entry.totalConsumption - previous.totalConsumption
graphEntry.production = entry.totalProduction - previous.totalProduction
graphEntry.acquisition = entry.totalAcquisition - previous.totalAcquisition
graphEntry.returned = entry.totalReturn - previous.totalReturn
} else {
graphEntry.consumption = entry.totalConsumption
graphEntry.production = entry.totalProduction
graphEntry.acquisition = entry.totalAcquisition
graphEntry.returned = entry.totalReturn
}
labelTime = entry.timestamp
} else {
labelTime = new Date(newestLogTimestamp.getTime() - config.sampleRate * i * 60000)
}
// print("Adding entry:", labelTime, graphEntry.consumption, config.toLabel(labelTime))
entries.unshift(graphEntry)
labels.unshift(labelTime)
// Given we've added 2 entries for the first run but only one label, we'll add the missing label
// at the end. This will shift the labels by one entries but that's ok because the logs timestamp
// is when the sample was created, but for the user it's better to show the the consumption values
// *during* that sample, not *before* the sample
if (i == config.count - 1) {
labelTime = new Date(labelTime.getTime() - config.sampleRate * 60000)
// print("Adding oldest entry label", labelTime, config.sampleRate, config.toLabel(labelTime))
labels.unshift(labelTime)
}
valueAxis.adjustMax(graphEntry.consumption)
valueAxis.adjustMax(graphEntry.production)
valueAxis.adjustMax(graphEntry.acquisition)
valueAxis.adjustMax(graphEntry.returned)
}
// print("assigning categories:", labels)
categoryAxis.timestamps = labels
chartView.animationOptions = ChartView.SeriesAnimations
for (var i = 0; i < entries.length; i++) {
// print("Appending to set", JSON.stringify(entries[i]))
d.consumptionSet.append(entries[i].consumption)
d.productionSet.append(entries[i].production)
d.acquisitionSet.append(entries[i].acquisition)
d.returnSet.append(entries[i].returned)
}
}
}
onEntryAdded: {
if (fetchingData) {
return
}
// print("Entry added")
var config = root.configs[selectionTabs.currentValue.config]
var start = entry
var consumptionValue = root.energyManager.totalConsumption - (start ? start.totalConsumption : 0)
var productionValue = root.energyManager.totalProduction - (start ? start.totalProduction : 0)
var acquisitionValue = root.energyManager.totalAcquisition - (start ? start.totalAcquisition : 0)
var returnValue = root.energyManager.totalReturn - (start ? start.totalReturn : 0)
// print("Entry added:", entry.timestamp, entry.totalConsumption, consumptionValue)
chartView.animationOptions = ChartView.NoAnimation
var timestamps = categoryAxis.timestamps;
timestamps.push(entry.timestamp)
timestamps.splice(0, 1)
categoryAxis.timestamps = timestamps
d.consumptionSet.remove(0, 1);
d.productionSet.remove(0, 1);
d.acquisitionSet.remove(0, 1);
d.returnSet.remove(0, 1);
d.consumptionSet.append(consumptionValue)
d.productionSet.append(productionValue)
d.acquisitionSet.append(acquisitionValue)
d.returnSet.append(returnValue)
chartView.animationOptions = ChartView.SeriesAnimations
}
}
ChartView {
id: chartView
Layout.fillWidth: true
Layout.fillHeight: true
animationOptions: ChartView.NoAnimation
backgroundColor: "transparent"
legend.alignment: Qt.AlignBottom
legend.font: Style.extraSmallFont
legend.labelColor: Style.foregroundColor
// margins.left: 0
margins.right: 0
margins.bottom: 0
margins.top: 0
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: chartView.plotArea.x - x
Repeater {
model: valueAxis.tickCount
delegate: Label {
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
width: parent.width - Style.smallMargins
horizontalAlignment: Text.AlignRight
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
}
}
BarSeries {
id: barSeries
axisX: BarCategoryAxis {
id: categoryAxis
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
gridVisible: false
gridLineColor: Style.tileOverlayColor
lineVisible: false
titleVisible: false
shadesVisible: false
categories: {
var ret = []
for (var i = 0; i < timestamps.length; i++) {
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i]))
}
return ret
}
property var timestamps: []
}
axisY: ValueAxis {
id: valueAxis
min: 0
gridLineColor: Style.tileOverlayColor
labelsVisible: false
labelsColor: Style.foregroundColor
labelsFont: Style.extraSmallFont
lineVisible: false
titleVisible: false
shadesVisible: false
function adjustMax(newValue) {
if (max < newValue) {
max = newValue // Math.ceil(newValue / 100) * 100
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: chartView
anchors.leftMargin: chartView.plotArea.x
anchors.topMargin: chartView.plotArea.y
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
hoverEnabled: true
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: mouseArea.preventStealing = true
}
onReleased: mouseArea.preventStealing = false
Item {
id: toolTip
property int idx: Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)
visible: mouseArea.containsMouse
x: Math.min(idx * mouseArea.width / categoryAxis.count, mouseArea.width - width)
property double setMaxValue: d.consumptionSet && d.productionSet && d.acquisitionSet && d.returnSet ?
Math.max(d.consumptionSet.at(idx), Math.max(d.productionSet.at(idx), Math.max(d.acquisitionSet.at(idx), d.returnSet.at(idx))))
: 0
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
Behavior on x { NumberAnimation { duration: Style.animationDuration } }
Behavior on y { NumberAnimation { duration: Style.animationDuration } }
Behavior on width { NumberAnimation { duration: Style.animationDuration } }
Behavior on height { NumberAnimation { duration: Style.animationDuration } }
Rectangle {
anchors.fill: parent
color: Style.tileOverlayColor
opacity: .8
radius: Style.smallCornerRadius
}
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
}
Label {
text: toolTip.idx >= 0 && categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : ""
font: Style.smallFont
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.blue
}
Label {
text: d.consumptionSet ? qsTr("Consumed: %1 kWh").arg(d.consumptionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
}
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.green
}
Label {
text: d.productionSet ? qsTr("Produced: %1 kWh").arg(d.productionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
}
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.red
}
Label {
text: d.acquisitionSet ? qsTr("From grid: %1 kWh").arg(d.acquisitionSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
}
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: Style.orange
}
Label {
text: d.returnSet ? qsTr("To grid: %1 kWh").arg(d.returnSet.at(toolTip.idx).toFixed(2)) : ""
font: Style.extraSmallFont
}
}
}
}
}
}
}
}