import QtQuick 2.3 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.2 import QtCharts 2.3 import Nymea 1.0 import "qrc:/ui/components/" StatsBase { id: root property EnergyManager energyManager: null property ThingsProxy producers: ThingsProxy { engine: _engine shownInterfaces: ["smartmeterproducer"] } readonly property bool hasProducers: producers.count > 0 QtObject { id: d property BarSet consumptionSet: null property BarSet productionSet: null property BarSet acquisitionSet: null property BarSet returnSet: null } function reload() { if (selectionTabs.currentValue === undefined) { return } if (engine.thingManager.fetchingData) { return; } var config = root.configs[selectionTabs.currentValue.config] print("Loading Power Balance Stats with 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 chartView.reset(); } Connections { target: engine.thingManager onFetchingDataChanged: { print("Thingmanager loaded", engine.thingManager.fetchingData) if (!engine.thingManager.fetchingData) root.reload() } } 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: { root.reload() } } 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)) if (root.hasProducers) { var consumption = root.energyManager.totalConsumption - (start ? start.totalConsumption : 0) d.consumptionSet.replace(d.consumptionSet.count - 1, consumption) valueAxis.adjustMax(consumption) var production = root.energyManager.totalProduction - (start ? start.totalProduction : 0) d.productionSet.replace(d.productionSet.count - 1, production) valueAxis.adjustMax(production) } var acquisition = root.energyManager.totalAcquisition - (start ? start.totalAcquisition : 0) if (d.acquisitionSet) { d.acquisitionSet.replace(d.acquisitionSet.count - 1, acquisition) } valueAxis.adjustMax(acquisition) var ret = root.energyManager.totalReturn - (start ? start.totalReturn : 0) if (d.returnSet) { d.returnSet.replace(d.returnSet.count - 1, ret) } valueAxis.adjustMax(ret) } } PowerBalanceLogs { id: powerBalanceLogs engine: _engine loadingInhibited: true onFetchingDataChanged: { if (!fetchingData) { chartView.animationOptions = ChartView.NoAnimation chartView.reset(); 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 = calculateSampleStart(newestLogTimestamp, config.sampleRate, i) } // 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 = NymeaUtils.chartsAnimationOptions for (var i = 0; i < entries.length; i++) { print("Appending to set", JSON.stringify(entries[i])) if (root.hasProducers) { 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 if (root.hasProducers) { d.consumptionSet.remove(0, 1); d.consumptionSet.append(consumptionValue) d.productionSet.remove(0, 1); d.productionSet.append(productionValue) } d.acquisitionSet.remove(0, 1); d.acquisitionSet.append(acquisitionValue) d.returnSet.remove(0, 1); d.returnSet.append(returnValue) chartView.animationOptions = NymeaUtils.chartsAnimationOptions } } 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 function reset() { barSeries.clear(); valueAxis.max = 0 if (root.hasProducers) { 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.yellow 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.green d.returnSet.borderColor = d.returnSet.color d.returnSet.borderWidth = 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 } } } } } } Item { anchors.fill: parent anchors.leftMargin: chartView.x + chartView.plotArea.x anchors.topMargin: chartView.y + chartView.plotArea.y anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y z: -1 Rectangle { height: parent.height + Style.margins * 2 y: -Style.smallMargins radius: Style.smallCornerRadius width: chartView.plotArea.width / categoryAxis.count color: Style.tileBackgroundColor property int idx: Math.min(Math.max(0,Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)), categoryAxis.count - 1) visible: toolTip.visible x: idx * parent.width / categoryAxis.count Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } } } } MouseArea { id: mouseArea anchors.fill: parent anchors.leftMargin: chartView.x + chartView.plotArea.x anchors.topMargin: chartView.y + 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 NymeaToolTip { id: toolTip backgroundItem: chartView backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height) property int idx: Math.min(Math.max(0,Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)), categoryAxis.count - 1) visible: mouseArea.containsMouse || mouseArea.preventStealing property int chartWidth: chartView.plotArea.width property int barWidth: chartWidth / categoryAxis.count x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ? idx * barWidth + barWidth + Style.smallMargins : idx * barWidth - Style.smallMargins - width property double setMaxValue: Math.max(d.consumptionSet ? d.consumptionSet.at(idx) : 0, d.productionSet ? d.productionSet.at(idx) : 0, d.acquisitionSet ? d.acquisitionSet.at(idx) : 0, d.returnSet ? 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 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 { visible: root.hasProducers Rectangle { width: Style.extraSmallFont.pixelSize height: width color: Style.blue } Label { text: toolTip.visible && d.consumptionSet ? qsTr("Consumed: %1 kWh").arg(d.consumptionSet.at(toolTip.idx).toFixed(2)) : "" font: Style.extraSmallFont } } RowLayout { visible: root.hasProducers Rectangle { width: Style.extraSmallFont.pixelSize height: width color: Style.yellow } Label { text: toolTip.visible && 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: toolTip.visible && 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.green } Label { text: toolTip.visible && d.returnSet ? qsTr("To grid: %1 kWh").arg(d.returnSet.at(toolTip.idx).toFixed(2)) : "" font: Style.extraSmallFont } } } } } }