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/ConsumersHistory.qml
2025-12-05 13:58:19 +01:00

810 lines
35 KiB
QML

// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-app.
*
* nymea-app is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-app is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-app. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick
import QtCharts
import QtQuick.Layouts
import QtQuick.Controls
import Nymea
import NymeaApp.Utils
import "qrc:/ui/components"
Item {
id: root
property EnergyManager energyManager: null
property ThingsProxy consumers: null
property bool titleVisible: true
PowerBalanceLogs {
id: powerBalanceLogs
engine: _engine
startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate
Component.onCompleted: fetchLogs()
onEntriesAddedIdx: (index, count) => {
print("entries added", index, count)
for (var i = 0; i < count; i++) {
var entry = powerBalanceLogs.get(index + i)
// print("got entry", entry.timestamp)
zeroSeries.ensureValue(entry.timestamp)
valueAxis.adjustMax(entry.consumption)
consumptionSeries.insertEntry(index + i, entry)
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
d.now = entry.timestamp
}
}
}
onEntriesRemoved: (index, count) => {
consumptionUpperSeries.removePoints(index, Math.min(count, consumptionUpperSeries.count))
zeroSeries.shrink()
}
}
ThingPowerLogsLoader {
id: logsLoader
engine: _engine
startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate
}
QtObject {
id: d
property date now: new Date()
property var selectedSeries: null
readonly property int range: selectionTabs.currentValue.range
readonly property int sampleRate: selectionTabs.currentValue.sampleRate
readonly property int visibleValues: range / sampleRate
readonly property var startTime: {
var date = new Date(fixTime(now));
date.setTime(date.getTime() - range * 60000 + 2000);
return date;
}
readonly property var endTime: {
var date = new Date(fixTime(now));
date.setTime(date.getTime() + 2000)
return date;
}
function fixTime(timestamp) {
switch (sampleRate) {
case EnergyLogs.SampleRate1Min:
timestamp.setSeconds(0, 0)
break;
case EnergyLogs.SampleRate15Mins:
timestamp.setMinutes(timestamp.getMinutes() - timestamp.getMinutes() % 15, 0, 0)
break;
case EnergyLogs.SampleRate1Hour:
timestamp.setMinutes(0, 0, 0);
break;
case EnergyLogs.SampleRate3Hours:
timestamp.setHours(timestamp.getHours() % 3, 0, 0, 0);
break;
case EnergyLogs.SampleRate1Day:
timestamp.setHours(0, 0, 0, 0)
break;
}
return timestamp
}
function update() {
if (!engine.thingManager.fetchingData && !engine.tagsManager.busy && consumersRepeater.count == consumers.count) {
logsLoader.fetchLogs();
}
}
function selectSeries(series) {
print("selecting series", series)
if (d.selectedSeries === series) {
d.selectedSeries = null
} else {
d.selectedSeries = series
}
}
}
Connections {
target: engine.tagsManager
onBusyChanged: d.update()
}
Component {
id: lineSeriesComponent
LineSeries { }
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Label {
Layout.fillWidth: true
Layout.margins: Style.smallMargins
horizontalAlignment: Text.AlignHCenter
text: qsTr("Consumers history")
visible: root.titleVisible
MouseArea {
anchors.fill: parent
onClicked: pageStack.push(Qt.resolvedUrl("ConsumersHistoryPage.qml"), {energyManager: root.energyManager, consumers: root.consumers})
}
}
SelectionTabs {
id: selectionTabs
Layout.fillWidth: true
Layout.leftMargin: Style.smallMargins
Layout.rightMargin: Style.smallMargins
currentIndex: 1
model: ListModel {
ListElement {
modelData: qsTr("Hours")
sampleRate: EnergyLogs.SampleRate1Min
range: 180 // 3 Hours: 3 * 60
}
ListElement {
modelData: qsTr("Days")
sampleRate: EnergyLogs.SampleRate15Mins
range: 1440 // 1 Day: 24 * 60
}
ListElement {
modelData: qsTr("Weeks")
sampleRate: EnergyLogs.SampleRate1Hour
range: 10080 // 7 Days: 7 * 24 * 60
}
ListElement {
modelData: qsTr("Months")
sampleRate: EnergyLogs.SampleRate3Hours
range: 43200 // 30 Days: 30 * 24 * 60
}
}
onTabSelected: {
d.now = new Date()
powerBalanceLogs.fetchLogs()
d.update()
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Label {
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.y + chartView.plotArea.y + Style.smallMargins
text: {
switch (d.sampleRate) {
case EnergyLogs.SampleRate1Min:
return d.startTime.toLocaleDateString(Qt.locale(), Locale.LongFormat)
case EnergyLogs.SampleRate15Mins:
case EnergyLogs.SampleRate1Hour:
case EnergyLogs.SampleRate3Hours:
case EnergyLogs.SampleRate1Day:
case EnergyLogs.SampleRate1Week:
case EnergyLogs.SampleRate1Month:
case EnergyLogs.SampleRate1Year:
return d.startTime.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + " - " + d.endTime.toLocaleDateString(Qt.locale(), Locale.ShortFormat)
}
}
font: Style.smallFont
opacity: ((new Date().getTime() - d.now.getTime()) / d.sampleRate / 60000) > d.visibleValues ? .5 : 0
Behavior on opacity { NumberAnimation {} }
}
ChartView {
id: chartView
anchors.fill: parent
backgroundColor: "transparent"
margins.left: Math.max(Style.smallMargins * 2, valueLabelMetrics.width + Style.smallMargins * 2)
margins.right: 0
margins.top: 0
margins.bottom: Style.smallIconSize + Style.margins
legend.visible: false
legend.alignment: Qt.AlignBottom
legend.font: Style.extraSmallFont
legend.labelColor: Style.foregroundColor
ActivityIndicator {
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
visible: powerBalanceLogs.fetchingData || logsLoader.fetchingData
opacity: .5
}
Label {
x: chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.plotArea.y + (chartView.plotArea.height - height) / 2 + (chartView.plotArea.height / 8)
text: qsTr("No data available")
visible: !powerBalanceLogs.fetchingData && !logsLoader.fetchingData && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.now)
font: Style.smallFont
opacity: .5
}
TextMetrics {
id: valueLabelMetrics
font: Style.extraSmallFont
text: ((valueAxis.max) / 1000).toFixed(2) + "kW"
}
ValueAxis {
id: valueAxis
min: 0
max: 1
labelFormat: ""
gridLineColor: Style.tileOverlayColor
labelsVisible: false
lineVisible: false
titleVisible: false
shadesVisible: false
function adjustMax(value) {
max = Math.max(max, Math.ceil(value / 100) * 100)
}
}
Item {
id: labelsLayout
x: Style.smallMargins
y: chartView.plotArea.y
height: chartView.plotArea.height
width: Math.max(0, chartView.margins.left - Style.smallMargins)
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))) / 1000).toFixed(2) + "kW"
verticalAlignment: Text.AlignTop
font: Style.extraSmallFont
}
}
}
DateTimeAxis {
id: dateTimeAxis
min: d.startTime
max: d.endTime
format: {
switch (selectionTabs.currentValue.sampleRate) {
case EnergyLogs.SampleRate1Min:
case EnergyLogs.SampleRate15Mins:
return "hh:mm"
case EnergyLogs.SampleRate1Hour:
case EnergyLogs.SampleRate3Hours:
case EnergyLogs.SampleRate1Day:
return "dd.MM."
}
}
tickCount: {
switch (selectionTabs.currentValue.sampleRate) {
case EnergyLogs.SampleRate1Min:
case EnergyLogs.SampleRate15Mins:
return root.width > 500 ? 13 : 7
case EnergyLogs.SampleRate1Hour:
return 7
case EnergyLogs.SampleRate3Hours:
case EnergyLogs.SampleRate1Day:
return root.width > 500 ? 12 : 6
}
}
labelsFont: Style.extraSmallFont
gridVisible: false
minorGridVisible: false
lineVisible: false
shadesVisible: false
labelsColor: Style.foregroundColor
}
AreaSeries {
id: consumptionSeries
axisX: dateTimeAxis
axisY: valueAxis
color: Style.gray
borderWidth: 0
borderColor: color
name: qsTr("Unknown")
// visible: false
opacity: .2
lowerSeries: LineSeries {
id: zeroSeries
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
function ensureValue(timestamp) {
if (count == 0) {
append(timestamp, 0)
} else if (count == 1) {
if (timestamp.getTime() < at(0).x) {
insert(0, timestamp, 0)
} else {
append(timestamp, 0)
}
} else {
if (timestamp.getTime() < at(0).x) {
remove(0)
insert(0, timestamp, 0)
} else if (timestamp.getTime() > at(1).x) {
remove(1)
append(timestamp, 0)
}
}
}
function shrink() {
clear();
if (powerBalanceLogs.count > 0) {
ensureValue(powerBalanceLogs.get(0).timestamp)
ensureValue(powerBalanceLogs.get(powerBalanceLogs.count - 1).timestamp)
}
}
}
upperSeries: LineSeries {
id: consumptionUpperSeries
}
function addEntry(entry) {
consumptionUpperSeries.append(entry.timestamp.getTime(), entry.consumption)
}
function insertEntry(index, entry) {
consumptionUpperSeries.insert(index, entry.timestamp.getTime(), entry.consumption)
}
}
Repeater {
id: consumersRepeater
model: consumers.count
Component.onCompleted: {
if (count != 0) {
d.update()
}
}
onCountChanged: {
if (count == consumers.count) {
d.update();
}
}
delegate: Item {
id: consumerDelegate
readonly property Thing thing: consumers.get(index)
property AreaSeries series: null
property QHash valueCache: QHash {}
property LineSeries lowerSeries: null
property LineSeries upperSeries: null
function calculateBaseValue(timestamp) {
if (index > 0) {
return consumersRepeater.itemAt(index - 1).calculateValue(timestamp)
}
return 0
}
function calculateValue(timestamp) {
var cached = valueCache.value(timestamp)
// print("ached:", cached)
if (cached !== undefined) {
return cached//valueCache.value(timestamp)
}
var ret = calculateBaseValue(timestamp)
var entry = logs.find(timestamp)
if (entry) {
ret += entry.currentPower;
}
valueCache.insert(timestamp, ret);
// print("calculating value for", thing.name, timestamp, ret)
return ret
}
function insertEntry(idx, entry) {
// print("inserting entry for", thing.name, entry.timestamp)
var baseValue = calculateBaseValue(entry.timestamp);
lowerSeries.insert(idx, entry.timestamp.getTime(), baseValue)
upperSeries.insert(idx, entry.timestamp.getTime(), baseValue + entry.currentPower)
}
function addEntries(index, count) {
// print("adding entries for", thing.name)
series.lowerSeries = null;
series.upperSeries = null;
var oldestTimestamp = null
var newestTimestamp = null
// Remove the leading 0-value entry
if (lowerSeries.count > 0) {
lowerSeries.removePoints(0, 1);
}
if (upperSeries.count > 0) {
upperSeries.removePoints(0, 1);
}
for (var i = 0; i < count; i++) {
var entry = logs.get(index + i)
// print("got thing entry", thing.name, entry.timestamp, entry.currentPower, index + i)
// zeroSeries.ensureValue(entry.timestamp)
if (oldestTimestamp == null || entry.timestamp < oldestTimestamp) {
oldestTimestamp = entry.timestamp;
}
if (newestTimestamp == null || entry.timestamp > newestTimestamp) {
newestTimestamp = entry.timestamp;
}
valueAxis.adjustMax(entry.currentPower)
insertEntry(index + i, entry)
if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) {
d.now = entry.timestamp
}
}
zeroSeries.ensureValue(oldestTimestamp)
zeroSeries.ensureValue(newestTimestamp)
// Add the leading 0-value entry back
lowerSeries.insert(0, upperSeries.at(0).x, 0)
upperSeries.insert(0, upperSeries.at(0).x, 0)
series.upperSeries = upperSeries;
series.lowerSeries = lowerSeries;
}
readonly property ThingPowerLogs logs: ThingPowerLogs {
engine: _engine
startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate
thingId: consumerDelegate.thing.id
loader: logsLoader
onEntriesAddedIdx: (index, count) => {
addTimer.addEntries(index, count)
}
onEntriesRemoved: (index, count) => {
// Note QtCharts crash when calling removePoints() for points that don't exist.
// Additionally it may decide to ignore values we add, e.g. if we try to add an Inf or undefined value for whatever reason
// So, even though in theory the series should always 1:1 reflect the model, it may not do so in practice and we'll have to make sure not crash here
// Remove the leading 0-value entry
if (consumerDelegate.lowerSeries.count > 0) {
consumerDelegate.lowerSeries.removePoints(0, 1);
}
if (consumerDelegate.upperSeries.count > 0) {
consumerDelegate.upperSeries.removePoints(0, 1);
}
print("removing:", index, count, "from", consumerDelegate.lowerSeries.count, consumerDelegate.upperSeries.count)
if (consumerDelegate.lowerSeries.count >= index + count) {
consumerDelegate.lowerSeries.removePoints(index, count)
}
if (consumerDelegate.upperSeries.count >= index + count) {
consumerDelegate.upperSeries.removePoints(index, count)
}
// Add the leading 0-value entry back
consumerDelegate.lowerSeries.insert(0, consumerDelegate.series.upperSeries.at(0).x, 0)
consumerDelegate.upperSeries.insert(0, consumerDelegate.series.upperSeries.at(0).x, 0)
zeroSeries.shrink()
}
}
// We'll have to make sure all the consumers have their data ready before adding
Timer {
id: addTimer
interval: 1000
repeat: false
onTriggered: consumerDelegate.addEntries(index, count)
property int index
property var count
function addEntries(index, count) {
addTimer.index = index
addTimer.count = count
start()
}
}
Component.onCompleted: {
series = chartView.createSeries(ChartView.SeriesTypeArea, thing.name, dateTimeAxis, valueAxis)
lowerSeries = lineSeriesComponent.createObject(series)
upperSeries = lineSeriesComponent.createObject(series)
series.color = NymeaUtils.generateColor(Style.generationBaseColor, index)
series.opacity = Qt.binding(function() {
return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3
})
series.borderWidth = 0;
series.borderColor = series.color
// Add a first point at 0 value
lowerSeries.insert(0, new Date().getTime(), 0)
upperSeries.insert(0, new Date().getTime(), 0)
}
Component.onDestruction: {
chartView.removeSeries(series)
}
}
}
}
RowLayout {
id: legend
anchors { left: parent.left; bottom: parent.bottom; right: parent.right }
anchors.leftMargin: chartView.plotArea.x
height: Style.smallIconSize
anchors.margins: Style.margins
Repeater {
model: root.consumers
delegate: Item {
id: legendDelegate
Layout.fillWidth: true
Layout.fillHeight: true
readonly property Thing thing: root.consumers.get(index)
opacity: d.selectedSeries == null || d.selectedSeries === consumersRepeater.itemAt(index).series ? 1 : 0.3
MouseArea {
anchors.fill: parent
anchors.topMargin: -Style.smallMargins
anchors.bottomMargin: -Style.smallMargins
onClicked: d.selectSeries(consumersRepeater.itemAt(index).series)
}
Row {
anchors.centerIn: parent
spacing: Style.smallMargins
ColorIcon {
name: app.interfacesToIcon(legendDelegate.thing.thingClass.interfaces)
size: Style.smallIconSize
color: index >= 0 ? NymeaUtils.generateColor(Style.generationBaseColor, index) : "white"
}
Label {
text: legendDelegate.thing.name
width: Math.max(0, legendDelegate.width - x)
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
font: Style.smallFont
visible: legend.width / root.consumers.count >= 80
}
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
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
preventStealing: tooltipping || dragging
property int startMouseX: 0
property bool dragging: false
property bool tooltipping: false
property var startDatetime: null
Timer {
interval: 300
running: mouseArea.pressed
onTriggered: {
if (!mouseArea.dragging) {
mouseArea.tooltipping = true
}
}
}
onReleased: {
mouseArea.tooltipping = false;
if (mouseArea.dragging) {
mouseArea.dragging = false;
for (var i = 0; i < consumersRepeater.count; i++) {
if (consumersRepeater.itemAt(i).logs.fetchingData) {
wheelStopTimer.start()
return;
}
}
powerBalanceLogs.fetchLogs()
logsLoader.fetchLogs()
// for (var i = 0; i < consumersRepeater.count; i++) {
// consumersRepeater.itemAt(i).logs.fetchLogs()
// }
}
}
onPressed: {
startMouseX = mouseX
startDatetime = d.now
}
onDoubleClicked: {
if (selectionTabs.currentIndex == 0) {
return;
}
var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
selectionTabs.currentIndex--
d.now = new Date(Math.min(new Date().getTime(), timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000))
powerBalanceLogs.fetchLogs()
logsLoader.fetchLogs()
}
onMouseXChanged: {
if (!pressed || mouseArea.tooltipping) {
return;
}
if (Math.abs(startMouseX - mouseX) < 10) {
return;
}
dragging = true
var dragDelta = startMouseX - mouseX
var totalTime = d.endTime.getTime() - d.startTime.getTime()
// dragDelta : timeDelta = width : totalTime
var timeDelta = dragDelta * totalTime / mouseArea.width
print("dragging", dragDelta, totalTime, mouseArea.width)
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta)))
}
onWheel: (wheel) => {
startDatetime = d.now
var totalTime = d.endTime.getTime() - d.startTime.getTime()
// pixelDelta : timeDelta = width : totalTime
var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width
print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width)
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta)))
wheelStopTimer.restart()
}
Timer {
id: wheelStopTimer
interval: 300
repeat: false
onTriggered: {
for (var i = 0; i < consumersRepeater.count; i++) {
if (consumersRepeater.itemAt(i).logs.fetchingData) {
wheelStopTimer.start()
return;
}
}
powerBalanceLogs.fetchLogs()
logsLoader.fetchLogs()
// for (var i = 0; i < consumersRepeater.count; i++) {
// consumersRepeater.itemAt(i).logs.fetchLogs()
// }
}
}
Rectangle {
height: parent.height
width: 1
color: Style.foregroundColor
x: Math.min(mouseArea.width - 1, Math.max(0, mouseArea.mouseX))
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
}
NymeaToolTip {
id: toolTip
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
backgroundItem: chartView
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
property int idx: Math.min(d.visibleValues, Math.max(0, Math.round(mouseArea.mouseX * d.visibleValues / mouseArea.width)))
property var timestamp: new Date(Math.min(d.endTime.getTime(), Math.max(d.startTime, d.startTime.getTime() + (idx * d.sampleRate * 60000))))
property PowerBalanceLogEntry entry: powerBalanceLogs.find(timestamp)
property int xOnRight: Math.max(0, mouseArea.mouseX) + Style.smallMargins
property int xOnLeft: Math.min(mouseArea.width, mouseArea.mouseX) - Style.smallMargins - width
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
property double maxValue: toolTip.entry ? Math.max(0, entry.consumption) : 0
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
ColumnLayout {
id: tooltipLayout
anchors {
left: parent.left
top: parent.top
margins: Style.smallMargins
}
Label {
text: toolTip.entry ? toolTip.entry.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat) : 0
font: Style.smallFont
}
RowLayout {
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: consumptionSeries.color
}
Label {
property double rawValue: toolTip.entry ? toolTip.entry.consumption : 0
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
property string unit: rawValue >= 1000 ? "kW" : "W"
text: "%1: %2 %3".arg(qsTr("Total")).arg(displayValue.toFixed(2)).arg(unit)
font: Style.extraSmallFont
}
}
Repeater {
model: consumersRepeater.count
delegate: RowLayout {
readonly property Item chartItem: consumersRepeater.itemAt(index)
id: consumerToolTipDelegate
opacity: d.selectedSeries == null || d.selectedSeries === chartItem.series ? 1 : 0.3
Rectangle {
width: Style.extraSmallFont.pixelSize
height: width
color: index >= 0 ? NymeaUtils.generateColor(Style.generationBaseColor, index) : "white"
}
Label {
property ThingPowerLogEntry entry: toolTip.idx >= 0 ? chartItem.logs.find(toolTip.timestamp) : null
property double rawValue: entry ? entry.currentPower : 0
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
property string unit: rawValue >= 1000 ? "kW" : "W"
text: "%1: %2 %3".arg(chartItem.thing.name).arg(displayValue.toFixed(2)).arg(unit)
font: Style.extraSmallFont
}
}
}
}
}
}
}
}
}