From 6d049101f4b22b1d54801d9efe8944e4b7fb4b81 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 6 Dec 2021 00:33:02 +0100 Subject: [PATCH] Improvements in the energy view --- libnymea-app/energy/energylogs.cpp | 2 +- libnymea-app/energy/powerbalancelogs.cpp | 7 +- libnymea-app/models/barseriesadapter.cpp | 2 +- libnymea-app/thingsproxy.cpp | 2 - nymea-app/resources.qrc | 1 + nymea-app/ui/mainviews/EnergyView.qml | 24 +- .../ui/mainviews/energy/ConsumerStats.qml | 254 +++++++++++++--- .../ui/mainviews/energy/ConsumersHistory.qml | 102 ++++++- .../ui/mainviews/energy/ConsumersPieChart.qml | 156 ++++++++++ .../CurrentConsumptionBalancePieChart.qml | 2 +- .../CurrentProductionBalancePieChart.qml | 2 +- .../ui/mainviews/energy/PowerBalanceStats.qml | 272 ++++++++++++++---- .../energy/PowerConsumptionBalanceHistory.qml | 99 ++++++- .../energy/PowerProductionBalanceHistory.qml | 99 ++++++- nymea-app/ui/mainviews/energy/StatsBase.qml | 155 ++++------ 15 files changed, 976 insertions(+), 203 deletions(-) create mode 100644 nymea-app/ui/mainviews/energy/ConsumersPieChart.qml diff --git a/libnymea-app/energy/energylogs.cpp b/libnymea-app/energy/energylogs.cpp index 03101c8a..b05bbb8a 100644 --- a/libnymea-app/energy/energylogs.cpp +++ b/libnymea-app/energy/energylogs.cpp @@ -183,9 +183,9 @@ void EnergyLogs::appendEntry(EnergyLogEntry *entry) beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); m_list.append(entry); endInsertRows(); + emit countChanged(); emit entryAdded(entry); emit entriesAdded({entry}); - emit countChanged(); } void EnergyLogs::appendEntries(const QList &entries) diff --git a/libnymea-app/energy/powerbalancelogs.cpp b/libnymea-app/energy/powerbalancelogs.cpp index ff14c1a2..deed7c2b 100644 --- a/libnymea-app/energy/powerbalancelogs.cpp +++ b/libnymea-app/energy/powerbalancelogs.cpp @@ -81,8 +81,6 @@ QString PowerBalanceLogs::logsName() const void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry) { - appendEntry(entry); - if (entry->consumption() < m_minValue) { m_minValue = entry->consumption(); emit minValueChanged(); @@ -117,11 +115,12 @@ void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry) emit maxValueChanged(); } + appendEntry(entry); } EnergyLogEntry *PowerBalanceLogs::find(const QDateTime ×tamp) const { -// qWarning() << "Finding log entry for timestamp:" << timestamp; + qWarning() << "Finding log entry for timestamp:" << timestamp; int oldest = 0; int newest = rowCount() - 1; EnergyLogEntry *entry = nullptr; @@ -131,7 +130,7 @@ EnergyLogEntry *PowerBalanceLogs::find(const QDateTime ×tamp) const EnergyLogEntry *newestEntry = get(newest); int middle = (newest - oldest) / 2 + oldest; EnergyLogEntry *middleEntry = get(middle); -// qWarning() << "Oldest:" << oldestEntry->timestamp().toString() << "Middle:" << middleEntry->timestamp().toString() << "Newest:" << newestEntry->timestamp().toString() << ":" << (newest - oldest); + qWarning() << "Oldest:" << oldestEntry->timestamp().toString() << "Middle:" << middleEntry->timestamp().toString() << "Newest:" << newestEntry->timestamp().toString() << ":" << (newest - oldest); if (timestamp <= oldestEntry->timestamp()) { return oldestEntry; } diff --git a/libnymea-app/models/barseriesadapter.cpp b/libnymea-app/models/barseriesadapter.cpp index 778907a2..c0af096a 100644 --- a/libnymea-app/models/barseriesadapter.cpp +++ b/libnymea-app/models/barseriesadapter.cpp @@ -180,7 +180,7 @@ void BarSeriesAdapter::logEntryAdded(LogEntry *entry) m_timeslots[slotIdx].entries.append(entry); m_set->replace(slotIdx, m_timeslots[slotIdx].value()); - qDebug() << "Adding entry" << entry->timestamp() << "timestlot" << timeSlotStart << "at" << slotIdx << "value" << m_timeslots[slotIdx].value(); +// qDebug() << "Adding entry" << entry->timestamp() << "timestlot" << timeSlotStart << "at" << slotIdx << "value" << m_timeslots[slotIdx].value(); // if (!m_timeslots.contains(timeSlotStart)) { // TimeSlot timeslot; diff --git a/libnymea-app/thingsproxy.cpp b/libnymea-app/thingsproxy.cpp index e992ba92..fdb424fc 100644 --- a/libnymea-app/thingsproxy.cpp +++ b/libnymea-app/thingsproxy.cpp @@ -85,8 +85,6 @@ void ThingsProxy::setParentProxy(ThingsProxy *parentProxy) if (!m_engine) { return; } - setSortRole(Things::RoleName); - sort(0); connect(m_parentProxy, SIGNAL(countChanged()), this, SIGNAL(countChanged())); connect(m_parentProxy, &QAbstractItemModel::dataChanged, this, [this]() { if (m_engine) { diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index cad4d04c..46cdf94f 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -271,5 +271,6 @@ ui/system/PackageListPage.qml ui/mainviews/energy/StatsBase.qml ui/mainviews/energy/EnergySettingsPage.qml + ui/mainviews/energy/ConsumersPieChart.qml diff --git a/nymea-app/ui/mainviews/EnergyView.qml b/nymea-app/ui/mainviews/EnergyView.qml index 9e17ee51..95a23ec6 100644 --- a/nymea-app/ui/mainviews/EnergyView.qml +++ b/nymea-app/ui/mainviews/EnergyView.qml @@ -95,7 +95,7 @@ MainViewBase { anchors.fill: parent anchors.margins: app.margins / 2 contentHeight: energyGrid.childrenRect.height - visible: energyMeters.count > 0 + visible: !engine.thingManager.fetchingData && engine.jsonRpcClient.experiences.hasOwnProperty("Energy") topMargin: root.topMargin // GridLayout directly in a flickable causes problems at initialisation @@ -117,6 +117,7 @@ MainViewBase { Layout.fillWidth: true Layout.preferredHeight: width energyManager: energyManager + visible: producers.count > 0 } CurrentProductionBalancePieChart { Layout.fillWidth: true @@ -128,6 +129,7 @@ MainViewBase { PowerConsumptionBalanceHistory { Layout.fillWidth: true Layout.preferredHeight: width + visible: producers.count > 0 } PowerProductionBalanceHistory { @@ -136,7 +138,7 @@ MainViewBase { visible: producers.count > 0 } - ConsumersBarChart { + ConsumersPieChart { Layout.fillWidth: true Layout.preferredHeight: width energyManager: energyManager @@ -144,6 +146,15 @@ MainViewBase { colors: root.thingColors consumers: consumers } + +// ConsumersBarChart { +// Layout.fillWidth: true +// Layout.preferredHeight: width +// energyManager: energyManager +// visible: consumers.count > 0 +// colors: root.thingColors +// consumers: consumers +// } ConsumersHistory { Layout.fillWidth: true Layout.preferredHeight: width @@ -157,6 +168,7 @@ MainViewBase { Layout.preferredHeight: width energyManager: energyManager } + ConsumerStats { Layout.fillWidth: true Layout.preferredHeight: width @@ -172,12 +184,18 @@ MainViewBase { EmptyViewPlaceholder { anchors.centerIn: parent width: parent.width - app.margins * 2 - visible: !engine.jsonRpcClient.experiences.hasOwnProperty("Energy") + visible: !engine.thingManager.fetchingData && !engine.jsonRpcClient.experiences.hasOwnProperty("Energy") title: qsTr("Energy plugin not installed installed.") text: qsTr("This %1 system does not have the energy extensions installed.").arg(Configuration.systemName) imageSource: "../images/smartmeter.svg" buttonText: qsTr("Install energy plugin") + buttonVisible: packagesFilterModel.count > 0 onButtonClicked: pageStack.push(Qt.resolvedUrl("../system/PackageListPage.qml"), {filter: "nymea-experience-plugin-energy"}) + PackagesFilterModel { + id: packagesFilterModel + packages: engine.systemController.packages + nameFilter: "nymea-experience-plugin-energy" + } } EmptyViewPlaceholder { anchors.centerIn: parent diff --git a/nymea-app/ui/mainviews/energy/ConsumerStats.qml b/nymea-app/ui/mainviews/energy/ConsumerStats.qml index b4f6430c..f4215d49 100644 --- a/nymea-app/ui/mainviews/energy/ConsumerStats.qml +++ b/nymea-app/ui/mainviews/energy/ConsumerStats.qml @@ -43,14 +43,12 @@ StatsBase { // print("config:", config.startTime(), config.sampleList(), config.sampleListNames()) powerLogs.sampleRate = config.sampleRate - powerLogs.startTime = config.startTime() - powerLogs.sampleList = config.sampleList() + powerLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000) barSeries.clear(); barSeries.thingBarSetMap = ({}) valueAxis.max = 0 - categoryAxis.categories = config.sampleListNames() chartView.animationOptions = ChartView.SeriesAnimations @@ -66,45 +64,132 @@ StatsBase { onFetchingDataChanged: { if (!fetchingData) { - barSeries.clear(); - for (var j = 0; j < consumers.count; j++) { - // Note: Needs to be let, not var so the lambda capture below copies it instead of capturing the reference - let consumer = consumers.get(j) -// print("ConsumerStats: Adding thing:", consumer.name) - let totalEnergyConsumedState = consumer.stateByName("totalEnergyConsumed") -// print("Adding consumer:", consumer.name, consumer.id) - var consumptionValues = [] - for (var i = 0; i < sampleList.length; i++) { - var start = powerLogs.find(consumer.id, new Date(sampleList[i])) - var startValue = start !== null ? start.totalConsumption : 0 - var end = i < sampleList.length -1 ? powerLogs.find(consumer.id, new Date(sampleList[i+1])) : null - var endValue = end !== null ? end.totalConsumption : 0 - if (i == sampleList.length - 1) { - endValue = totalEnergyConsumedState.value + var config = root.configs[selectionTabs.currentValue.config] + + // First grouping log entries by timestamp + var groupedEntries = [] + var groupedEntry = {} + for (var i = powerLogs.count - 1; i >= 0; i--) { + var entry = powerLogs.get(i); +// print("grouping entry:", entry.timestamp, "current group entry", groupedEntry.timestamp, groupedEntry.hasOwnProperty("timestamp")) + if (!groupedEntry.hasOwnProperty("timestamp")) { + groupedEntry.timestamp = entry.timestamp; +// print("Starting new groupentry", groupedEntry.timestamp, entry.timestamp) + } + if (groupedEntry.timestamp.getTime() !== entry.timestamp.getTime()) { + if (groupedEntries.length > config.count) { + break; + } +// print("finalizing grouped entry", groupedEntry.timestamp) + groupedEntries.unshift(groupedEntry); + groupedEntry = { + timestamp: entry.timestamp + } +// print("Starting new groupentry", groupedEntry.timestamp, entry.timestamp) + } + groupedEntry[entry.thingId] = entry.totalConsumption + } + if (groupedEntry.hasOwnProperty("timestamp") && groupedEntries.length <= config.count) { +// print("finalizing grouped entry", groupedEntry.timestamp) + groupedEntries.unshift(groupedEntry) + } + + + + chartView.animationOptions = ChartView.NoAnimation + + + var labels = [] + var entries = [] + + var newestLogTimestamp = powerLogs.count > 0 ? powerLogs.get(powerLogs.count - 1).timestamp : new Date(); + + for (var i = 0; i < config.count; i++) { + var groupedEntry = groupedEntries[groupedEntries.length - i - 1] +// print("have grouped entry:", groupedEntry ? groupedEntry.timestamp : "null") + + // 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 = {} + for (var j = 0; j < consumers.count; j++) { + var consumer = consumers.get(j) +// print("Got consumer:", consumer.id, consumer.name) + var value = consumer.stateByName("totalEnergyConsumed").value; + if (groupedEntry) { + value -= groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0 + } + liveEntry[consumer.id] = value + valueAxis.adjustMax(value) } -// print("adding sample", new Date(sampleList[i]), start ? start.timestamp : "X", " - ", end ? end.timestamp : "X") -// print("values. start:", startValue, "end", endValue, "diff", endValue - startValue) - var consumptionValue = endValue - startValue -// print("Value", consumptionValue) - consumptionValues.push(consumptionValue) - valueAxis.adjustMax(consumptionValue) +// print("Adding live entry", JSON.stringify(liveEntry)) + entries.unshift(liveEntry) } - let barSet = barSeries.append(consumer.name, consumptionValues) - barSet.color = root.colors[j % root.colors.length] - barSet.borderWidth = 0 - barSet.borderColor = barSet.color - barSeries.thingBarSetMap[consumer] = barSet - totalEnergyConsumedState.onValueChanged.connect(function() { - var sampleList = root.configs[selectionTabs.currentValue.config].sampleList() - var lastSample = sampleList[sampleList.length - 1] -// print("sampleList:", powerLogs.sampleList) - var start = powerLogs.find(consumer.id, new Date(lastSample)) -// print("consumer value changed:", consumer.name, totalEnergyConsumedState.value, start.timestamp, start.totalConsumption) - var barSet = barSeries.thingBarSetMap[consumer] - barSet.replace(barSet.count - 1, totalEnergyConsumedState.value - start.totalConsumption) - }) + // Add the actual entry + var graphEntry = {} + var labelTime = new Date(); + + if (groupedEntry) { + var previousGroupedEntry = groupedEntries[groupedEntries.length - i - 2] + for (var j = 0; j < consumers.count; j++) { + var consumer = consumers.get(j) + var value = groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0 + if (previousGroupedEntry) { + var previousValue = previousGroupedEntry.hasOwnProperty(consumer.id) ? previousGroupedEntry[consumer.id] : 0 + value -= previousValue + } + graphEntry[consumer.id] = value + valueAxis.adjustMax(value) + } + labelTime = groupedEntry.timestamp + } else { + for (var j = 0; j < consumers.count; j++) { + var consumer = consumers.get(j) + graphEntry[consumer.id] = 0 + } + labelTime = new Date(newestLogTimestamp.getTime() - config.sampleRate * i * 60000) + } + +// print("Adding entry:", labelTime, config.toLabel(labelTime), JSON.stringify(graphEntry)) + 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) + } + + } + +// print("assigning categories:", labels) + categoryAxis.timestamps = labels + + var map = {} + for (var j = 0; j < consumers.count; j++) { + var consumer = consumers.get(j) + var barSet = barSeries.append(consumer.name, []) + barSet.color = root.colors[j % root.colors.length] + barSet.borderColor = barSet.color + barSet.borderWith = 0 + map[consumer.id] = barSet + } + barSeries.thingBarSetMap = map + + chartView.animationOptions = ChartView.SeriesAnimations + + for (var i = 0; i < entries.length; i++) { + var entry = entries[i] +// print("Adding entry", JSON.stringify(entry)) + for (var j = 0; j < consumers.count; j++) { + var consumer = consumers.get(j) + barSeries.thingBarSetMap[consumer.id].append(entry[consumer.id]) + } } } } @@ -143,7 +228,7 @@ StatsBase { Layout.fillWidth: true Layout.margins: Style.smallMargins horizontalAlignment: Text.AlignHCenter - text: qsTr("Consumers statistics") + text: qsTr("Consumers totals") } @@ -155,13 +240,14 @@ StatsBase { currentIndex: 0 model: ListModel { Component.onCompleted: { - append({modelData: qsTr("Months"), config: "months" }) - append({modelData: qsTr("Weeks"), config: "weeks" }) - append({modelData: qsTr("Days"), config: "days" }) 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 = 2 + selectionTabs.currentIndex = 1 } } onCurrentValueChanged: { @@ -217,6 +303,16 @@ StatsBase { 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 @@ -238,6 +334,78 @@ StatsBase { property var thingBarSetMap: ({}) } + + + 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 + + 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: { + var max = 0; + for (var i = 0; i < consumers.count; i++) { + var consumer = consumers.get(i) + max = barSeries.thingBarSetMap.hasOwnProperty(consumer.id) ? Math.max(max, barSeries.thingBarSetMap[consumer.id].at(idx)) : 0 + } + return max + } + 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: categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : "" + font: Style.smallFont + } + + Repeater { + model: consumers + delegate: RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: root.colors[index % root.colors.length] + } + Label { + text: barSeries.thingBarSetMap.hasOwnProperty(model.id) ? "%1: %2 kWh".arg(model.name).arg(barSeries.thingBarSetMap[model.id].at(toolTip.idx).toFixed(2)) : "" + font: Style.extraSmallFont + } + } + } + } + } + } } } } diff --git a/nymea-app/ui/mainviews/energy/ConsumersHistory.qml b/nymea-app/ui/mainviews/energy/ConsumersHistory.qml index bf5abe6b..1832c461 100644 --- a/nymea-app/ui/mainviews/energy/ConsumersHistory.qml +++ b/nymea-app/ui/mainviews/energy/ConsumersHistory.qml @@ -141,7 +141,9 @@ ChartView { series.borderColor = series.color // print("Adding thingId series", thing.id, thing.name) - d.thingsSeriesMap[thing.id] = series + var map = d.thingsSeriesMap + map[thing.id] = series + d.thingsSeriesMap = map consumerThingIds.push(thing.id) } thingPowerLogs.thingIds = consumerThingIds; @@ -235,4 +237,102 @@ ChartView { consumptionUpperSeries.append(entry.timestamp.getTime(), entry.consumption) } } + + MouseArea { + id: mouseArea + anchors.fill: parent + anchors.leftMargin: root.plotArea.x + anchors.topMargin: root.plotArea.y + anchors.rightMargin: root.width - root.plotArea.width - root.plotArea.x + anchors.bottomMargin: root.height - root.plotArea.height - root.plotArea.y + + hoverEnabled: true + + Rectangle { + height: parent.height + width: 1 + color: Style.foregroundColor + x: mouseArea.mouseX + visible: mouseArea.containsMouse + } + + Item { + id: toolTip + visible: mouseArea.containsMouse + + property int idx: consumptionUpperSeries.count - Math.floor(mouseArea.mouseX * consumptionUpperSeries.count / mouseArea.width) + property int seriesIndex: consumptionUpperSeries.count - idx + + property int xOnRight: mouseArea.mouseX + Style.smallMargins + property int xOnLeft: mouseArea.mouseX - Style.smallMargins - width + x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft + property double maxValue: consumptionUpperSeries.at(seriesIndex).y + 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 + + property date timestamp: new Date(consumptionUpperSeries.at(seriesIndex).x) + + 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.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat) + font: Style.smallFont + } + RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: consumptionSeries.color + } + Label { + property double rawValue: consumptionUpperSeries.at(toolTip.seriesIndex).y + 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: consumers + delegate: RowLayout { + id: consumerToolTipDelegate + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: root.colors[index % root.colors.length] + } + + Label { + property ThingPowerLogEntry entry: thingPowerLogs.find(model.id, toolTip.timestamp) + 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(model.name).arg(displayValue.toFixed(2)).arg(unit) + font: Style.extraSmallFont + } + } + } + } + } + } } diff --git a/nymea-app/ui/mainviews/energy/ConsumersPieChart.qml b/nymea-app/ui/mainviews/energy/ConsumersPieChart.qml new file mode 100644 index 00000000..897b5929 --- /dev/null +++ b/nymea-app/ui/mainviews/energy/ConsumersPieChart.qml @@ -0,0 +1,156 @@ +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtGraphicalEffects 1.0 +import QtCharts 2.2 +import Nymea 1.0 + +ChartView { + id: root + backgroundColor: "transparent" + animationOptions: ChartView.SeriesAnimations + title: qsTr("Consumers balance") + titleColor: Style.foregroundColor + legend.visible: false + + property EnergyManager energyManager: null + property ThingsProxy consumers: null + property var colors: null + + Connections { + target: engine.thingManager + onFetchingDataChanged: { + if (!engine.thingManager.fetchingData) { + updateConsumers() + } + } + } + + Connections { + target: root.consumers + onCountChanged: { + if (!engine.thingManager.fetchingData) { + updateConsumers() + } + } + } + + Connections { + target: energyManager + onPowerBalanceChanged: { + var consumption = energyManager.currentPowerConsumption + for (var i = 0; i < consumers.count; i++) { + consumption -= consumers.get(i).stateByName("currentPower").value + } + d.unknownSlice.value = consumption + } + } + + QtObject { + id: d + property var thingsColorMap: ({}) + property PieSlice unknownSlice: null + } + + function updateConsumers() { + consumersBalanceSeries.clear(); + + var unknownConsumption = energyManager.currentPowerConsumption + + var colorMap = {} + for (var i = 0; i < consumers.count; i++) { + var consumer = consumers.get(i) + colorMap[consumer] = root.colors[i % root.colors.length] + let currentPowerState = consumer.stateByName("currentPower") + let slice = consumersBalanceSeries.append(consumer.name, currentPowerState.value) + slice.color = root.colors[i % root.colors.length] + currentPowerState.valueChanged.connect(function() { + slice.value = currentPowerState.value + }) + unknownConsumption -= currentPowerState.value + } + + d.unknownSlice = consumersBalanceSeries.append(qsTr("Unknown"), unknownConsumption) + d.unknownSlice.color = Style.gray + + d.thingsColorMap = colorMap + } + + PieSeries { + id: consumersBalanceSeries + size: 0.9 + holeSize: 0.7 + } + + + ColumnLayout { + id: centerLayout + x: root.plotArea.x + (root.plotArea.width - width) / 2 + y: root.plotArea.y + (root.plotArea.height - height) / 2 + width: root.plotArea.width * 0.65 +// height: root.plotArea.height * 0.65 + spacing: Style.smallMargins + property int maximumHeight: root.plotArea.height * 0.65 + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + Label { + text: qsTr("Total") + font: Style.smallFont + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + + Label { + text: "%1 %2" + .arg((energyManager.currentPowerConsumption / (energyManager.currentPowerConsumption > 1000 ? 1000 : 1)).toFixed(1)) + .arg(energyManager.currentPowerConsumption > 1000 ? "kW" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.bigFont + + } + } + + ListView { + Layout.fillWidth: true + Layout.preferredHeight: count * (Style.smallMargins + Style.extraSmallFont.pixelSize + Style.smallFont.pixelSize) + Layout.maximumHeight: centerLayout.maximumHeight - y + clip: true + spacing: Style.smallMargins + + model: ThingsProxy { + id: sortedConsumers + engine: _engine + parentProxy: root.consumers + sortStateName: "currentPower" + sortOrder: Qt.DescendingOrder + } + delegate: ColumnLayout { + width: parent.width + spacing: 0 + Label { + text: model.name + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.extraSmallFont + } + Label { + property Thing consumer: sortedConsumers.get(index) + property State currentPowerState: consumer ? consumer.stateByName("currentPower") : null + property double value: currentPowerState ? currentPowerState.value : 0 + + color: d.thingsColorMap[consumer] + text: "%1 %2" + .arg((value / (value > 1000 ? 1000 : 1)).toFixed(1)) + .arg(value > 1000 ? "kWh" : "W") + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: Style.smallFont + } + } + } + } +} diff --git a/nymea-app/ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml b/nymea-app/ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml index 2c230112..d6850203 100644 --- a/nymea-app/ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml +++ b/nymea-app/ui/mainviews/energy/CurrentConsumptionBalancePieChart.qml @@ -10,7 +10,7 @@ ChartView { id: consumptionPieChart backgroundColor: "transparent" animationOptions: ChartView.SeriesAnimations - title: qsTr("Current power consumption balance") + title: qsTr("My energy mix") titleColor: Style.foregroundColor legend.visible: false diff --git a/nymea-app/ui/mainviews/energy/CurrentProductionBalancePieChart.qml b/nymea-app/ui/mainviews/energy/CurrentProductionBalancePieChart.qml index 70db7831..98da7bd9 100644 --- a/nymea-app/ui/mainviews/energy/CurrentProductionBalancePieChart.qml +++ b/nymea-app/ui/mainviews/energy/CurrentProductionBalancePieChart.qml @@ -10,7 +10,7 @@ ChartView { id: productionPieChart backgroundColor: "transparent" animationOptions: ChartView.SeriesAnimations - title: qsTr("Current power production balance") + title: qsTr("My energy production") titleColor: Style.foregroundColor legend.visible: false diff --git a/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml b/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml index 3eab9d1c..55d68305 100644 --- a/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml +++ b/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml @@ -25,7 +25,7 @@ StatsBase { Layout.fillWidth: true Layout.margins: Style.smallMargins horizontalAlignment: Text.AlignHCenter - text: qsTr("Energy consumption statistics") + text: qsTr("Totals") } @@ -36,54 +36,56 @@ StatsBase { Layout.rightMargin: Style.smallMargins model: ListModel { Component.onCompleted: { - append({modelData: qsTr("Months"), config: "months" }) - append({modelData: qsTr("Weeks"), config: "weeks" }) - append({modelData: qsTr("Days"), config: "days" }) 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 = 2 + selectionTabs.currentIndex = 1 } } onCurrentValueChanged: { var config = root.configs[currentValue.config] - print("config:", config.startTime(), config.sampleList(), config.sampleListNames()) + print("config:", config.startTime(), config.sampleRate) powerBalanceLogs.loadingInhibited = true powerBalanceLogs.sampleRate = config.sampleRate - powerBalanceLogs.startTime = config.startTime() - powerBalanceLogs.sampleList = config.sampleList() + 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 - categoryAxis.categories = config.sampleListNames() - chartView.animationOptions = ChartView.SeriesAnimations } } Connections { target: energyManager onPowerBalanceChanged: { - var start = powerBalanceLogs.get(powerBalanceLogs.count - 1) - // print("balance changed:", d.consumptionSet, powerBalanceLogs, powerBalanceLogs.count) - // print("updating", start.timestamp, root.energyManager.totalConsumption - (start ? start.totalConsumption : 0)) + var start = powerBalanceLogs.get(powerBalanceLogs.count - 1 ) +// print("balance changed:", d.consumptionSet, powerBalanceLogs, powerBalanceLogs.count) +// print("updating", start.timestamp, start.totalConsumption, 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)) @@ -96,48 +98,102 @@ StatsBase { engine: _engine loadingInhibited: true - property var sampleList: minutesList - onFetchingDataChanged: { if (!fetchingData) { - if (powerBalanceLogs.count == 0) { - valueAxis.adjustMax(root.energyManager.totalConsumption) - valueAxis.adjustMax(root.energyManager.totalAcquisition) - valueAxis.adjustMax(root.energyManager.totalProduction) - valueAxis.adjustMax(root.energyManager.totalReturn) + chartView.animationOptions = ChartView.NoAnimation - for (var i = 0; i < sampleList.length; i++) { - d.consumptionSet.append(i == sampleList.length - 1 ? root.energyManager.totalConsumption : 0) - d.productionSet.append(i == sampleList.length - 1 ? root.energyManager.totalProduction : 0) - d.acquisitionSet.append(i == sampleList.length - 1 ? root.energyManager.totalAcquisition : 0) - d.returnSet.append(i == sampleList.length - 1 ? root.energyManager.totalReturn : 0) + 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) } - return; + + // 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) } - for (var i = 0; i < sampleList.length; i++) { - var start = powerBalanceLogs.find(new Date(sampleList[i])) - var end = null; - if (i+1 < sampleList.length) { - end = powerBalanceLogs.find(new Date(sampleList[i+1])) - } -// print("** stats for:", new Date(sampleList[i]), /*start, end, */"start:", start ? start.totalConsumption : 0, "end:", end ? end.totalConsumption : root.energyManager.totalConsumption) - var consumptionValue = (end != null ? end.totalConsumption : root.energyManager.totalConsumption) - (start ? start.totalConsumption : 0) - var productionValue = (end != null ? end.totalProduction : root.energyManager.totalProduction) - (start ? start.totalProduction : 0) - var acquisitionValue = (end != null ? end.totalAcquisition : root.energyManager.totalAcquisition) - (start ? start.totalAcquisition : 0) - var returnValue = (end != null ? end.totalReturn : root.energyManager.totalReturn) - (start ? start.totalReturn : 0) +// print("assigning categories:", labels) + categoryAxis.timestamps = labels - valueAxis.adjustMax(consumptionValue) - valueAxis.adjustMax(productionValue) - valueAxis.adjustMax(acquisitionValue) - valueAxis.adjustMax(returnValue) - - d.consumptionSet.append(consumptionValue) - d.productionSet.append(productionValue) - d.acquisitionSet.append(acquisitionValue) - d.returnSet.append(returnValue) + 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) } + } } @@ -146,14 +202,23 @@ StatsBase { return } + 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 - categoryAxis.categories = configs[selectionTabs.currentValue.config].sampleListNames() + + var timestamps = categoryAxis.timestamps; + timestamps.splice(0, 1) + timestamps.push(entry.timestamp) + categoryAxis.timestamps = timestamps + d.consumptionSet.append(consumptionValue) d.productionSet.append(productionValue) d.acquisitionSet.append(acquisitionValue) @@ -171,7 +236,7 @@ StatsBase { id: chartView Layout.fillWidth: true Layout.fillHeight: true - animationOptions: ChartView.NoAnimations + animationOptions: ChartView.NoAnimation backgroundColor: "transparent" legend.alignment: Qt.AlignBottom @@ -214,6 +279,17 @@ StatsBase { 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 @@ -233,6 +309,106 @@ StatsBase { } } } + + 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 + + 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: powerBalanceLogs.count + ":" + categoryAxis.count + ":" + toolTip.idx +// } + + Label { + text: 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 + } + } + } + } + } } } } diff --git a/nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml b/nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml index fed938bb..139ab05c 100644 --- a/nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml +++ b/nymea-app/ui/mainviews/energy/PowerConsumptionBalanceHistory.qml @@ -12,7 +12,7 @@ ChartView { margins.bottom: 0 margins.top: 0 - title: qsTr("Power consumption balance history") + title: qsTr("My consumption history") titleColor: Style.foregroundColor legend.alignment: Qt.AlignBottom @@ -229,4 +229,101 @@ ChartView { } } + MouseArea { + id: mouseArea + anchors.fill: parent + anchors.leftMargin: root.plotArea.x + anchors.topMargin: root.plotArea.y + anchors.rightMargin: root.width - root.plotArea.width - root.plotArea.x + anchors.bottomMargin: root.height - root.plotArea.height - root.plotArea.y + + hoverEnabled: true + + Rectangle { + height: parent.height + width: 1 + color: Style.foregroundColor + x: mouseArea.mouseX + visible: mouseArea.containsMouse + } + + Item { + id: toolTip + visible: mouseArea.containsMouse + + property int idx: consumptionUpperSeries.count - (Math.floor(mouseArea.mouseX * consumptionUpperSeries.count / mouseArea.width)) + property int seriesIndex: consumptionUpperSeries.count - idx + + property int xOnRight: mouseArea.mouseX + Style.smallMargins + property int xOnLeft: mouseArea.mouseX - Style.smallMargins - width + x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft + property double maxValue: consumptionUpperSeries.at(seriesIndex).y + 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 + + 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: new Date(consumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat) + font: Style.smallFont + } + + RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: Style.green + } + + Label { + text: qsTr("Self production: %1 kW").arg(selfProductionUpperSeries.at(toolTip.seriesIndex).y.toFixed(2)) + font: Style.extraSmallFont + } + } + RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: Style.orange + } + + Label { + text: qsTr("From battery: %1 kW").arg(storageUpperSeries.at(toolTip.seriesIndex).y.toFixed(2)) + font: Style.extraSmallFont + } + } + RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: Style.red + } + + Label { + text: qsTr("From grid: %1 kW").arg(acquisitionUpperSeries.at(toolTip.seriesIndex).y.toFixed(2)) + font: Style.extraSmallFont + } + } + } + } + } } diff --git a/nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml b/nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml index ab005009..4d920f66 100644 --- a/nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml +++ b/nymea-app/ui/mainviews/energy/PowerProductionBalanceHistory.qml @@ -12,7 +12,7 @@ ChartView { margins.bottom: 0 margins.top: 0 - title: qsTr("Power production balance history") + title: qsTr("My production history") titleColor: Style.foregroundColor legend.alignment: Qt.AlignBottom @@ -222,4 +222,101 @@ ChartView { } } + MouseArea { + id: mouseArea + anchors.fill: parent + anchors.leftMargin: root.plotArea.x + anchors.topMargin: root.plotArea.y + anchors.rightMargin: root.width - root.plotArea.width - root.plotArea.x + anchors.bottomMargin: root.height - root.plotArea.height - root.plotArea.y + + hoverEnabled: true + + Rectangle { + height: parent.height + width: 1 + color: Style.foregroundColor + x: mouseArea.mouseX + visible: mouseArea.containsMouse + } + + Item { + id: toolTip + visible: mouseArea.containsMouse + + property int idx: productionUpperSeries.count - Math.floor(mouseArea.mouseX * productionUpperSeries.count / mouseArea.width) + property int seriesIndex: productionUpperSeries.count - idx + + property int xOnRight: mouseArea.mouseX + Style.smallMargins + property int xOnLeft: mouseArea.mouseX - Style.smallMargins - width + x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft + property double maxValue: productionUpperSeries.at(seriesIndex).y + 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 + + 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: new Date(selfConsumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat) + font: Style.smallFont + } + + RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: Style.green + } + + Label { + text: qsTr("consumed: %1 kW").arg(selfConsumptionUpperSeries.at(toolTip.seriesIndex).y.toFixed(2)) + font: Style.extraSmallFont + } + } + RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: Style.orange + } + + Label { + text: qsTr("To battery: %1 kW").arg(storageUpperSeries.at(toolTip.seriesIndex).y.toFixed(2)) + font: Style.extraSmallFont + } + } + RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: Style.red + } + + Label { + text: qsTr("To grid: %1 kW").arg(acquisitionUpperSeries.at(toolTip.seriesIndex).y.toFixed(2)) + font: Style.extraSmallFont + } + } + } + } + } } diff --git a/nymea-app/ui/mainviews/energy/StatsBase.qml b/nymea-app/ui/mainviews/energy/StatsBase.qml index 45059172..94b0de27 100644 --- a/nymea-app/ui/mainviews/energy/StatsBase.qml +++ b/nymea-app/ui/mainviews/energy/StatsBase.qml @@ -4,42 +4,55 @@ import Nymea 1.0 Item { id: root - property int minutesCount: 10 - property int hoursCount: 12 - property int daysCount: 7 + property int minutesCount: 9 + property int hoursCount: 11 + property int daysCount: 6 property int weeksCount: 12 - property int monthsCount: 12 + property int monthsCount: 11 + property int yearsCount: 5 property var configs: ({ minutes: { + count: minutesCount, startTime: minutesStart, sampleRate: EnergyLogs.SampleRate1Min, - sampleList: minutesList, - sampleListNames: minutesListNames + toLabel: minuteLabel, + toLongLabel: minuteLongLabel }, hours: { + count: hoursCount, startTime: hoursStart, sampleRate: EnergyLogs.SampleRate1Hour, - sampleList: hoursList, - sampleListNames: hoursListNames + toLabel: hourLabel, + toLongLabel: hourLongLabel }, days: { + count: daysCount, startTime: daysStart, sampleRate: EnergyLogs.SampleRate1Day, - sampleList: daysList, - sampleListNames: daysListNames + toLabel: dayLabel, + toLongLabel: dayLongLabel }, weeks: { + count: weeksCount, startTime: weeksStart, sampleRate: EnergyLogs.SampleRate1Week, - sampleList: weeksList, - sampleListNames: weeksListNames + toLabel: weekLabel, + toLongLabel: weekLongLabel }, months: { + count: monthsCount, startTime: monthsStart, sampleRate: EnergyLogs.SampleRate1Month, - sampleList: monthsList, - sampleListNames: monthsListNames + toLabel: monthLabel, + toLongLabel: monthLongLabel + }, + years: { + count: yearsCount, + startTime: yearStart, + sampleRate: EnergyLogs.SampleRate1Year, + toLabel: yearLabel, + toLongLabel: yearLabel } }) @@ -48,45 +61,24 @@ Item { d.setMinutes(d.getMinutes() - minutesCount + 1, 0, 0) return d; } - function minutesList() { - var ret = [] - var startTime = minutesStart(); - for (var i = 0; i < minutesCount; i++) { - var last = new Date(startTime) - ret.push(last.setTime(last.getTime() + i * 60 * 1000)) - } - return ret; + function minuteLabel(date) { + return date.toLocaleString(Qt.locale(), "hh:mm") } - function minutesListNames() { - var ret = [] - var list = minutesList() - for (var i = 0; i < list.length; i++) { - ret.push(new Date(list[i]).toLocaleString(Qt.locale(), "hh:mm")) - } - return ret; + function minuteLongLabel(date) { + return date.toLocaleString(Qt.locale(), Locale.ShortFormat) } + function hoursStart() { var d = new Date(); d.setHours(d.getHours() - hoursCount + 1, 0, 0, 0) return d; } - function hoursList() { - var ret = [] - var startTime = hoursStart(); - for (var i = 0; i < hoursCount; i++) { - var last = new Date(startTime) - ret.push(last.setTime(last.getTime() + i * 60 * 60 * 1000)) - } - return ret; + function hourLabel(date) { + return date.toLocaleString(Qt.locale(), "hh") } - function hoursListNames() { - var ret = []; - var list = hoursList(); - for (var i = 0; i < list.length; i++) { - ret.push(new Date(list[i]).toLocaleString(Qt.locale(), "hh")); - } - return ret; + function hourLongLabel(date) { + return date.toLocaleString(Qt.locale(), Locale.ShortFormat) } function daysStart() { @@ -95,24 +87,11 @@ Item { d.setDate(d.getDate() - daysCount + 1); return d; } - - function daysList() { - var ret = [] - var startTime = daysStart(); - for (var i = 0; i < daysCount; i++) { - var last = new Date(startTime) - ret.push(last.setDate(last.getDate() + i)) - } - return ret; + function dayLabel(date) { + return date.toLocaleString(Qt.locale(), "ddd") } - - function daysListNames() { - var ret = [] - var list = daysList(); - for (var i = 0; i < list.length; i++) { - ret.push(new Date(list[i]).toLocaleString(Qt.locale(), "ddd")) - } - return ret; + function dayLongLabel(date) { + return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat) } function weeksStart() { @@ -121,57 +100,41 @@ Item { d.setDate(d.getDate() - d.getDay() - weeksCount * 7); return d } - function weeksList() { - var ret = [] - var startTime = weeksStart(); - for (var i = 0; i < weeksCount; i++) { - var last = new Date(startTime) - ret.push(last.setDate(last.getDate() + i * 7)) - } - return ret; + function weekLabel(date) { + var yearStart = new Date(); + yearStart.setHours(0,0,0,0); + yearStart.setDate(1); + yearStart.setMonth(0); + return Math.ceil((((date - yearStart) / 86400000) + 1)/7) } - function weeksListNames() { - var ret = [] - var list = weeksList(); - for (var i = 0; i < list.length; i++) { - var d = new Date(list[i]) - var dayNum = d.getDay() || 7; - d.setDate(d.getDate() + 4 - dayNum); - ret.push(Math.ceil((((d - yearStart()) / 86400000) + 1)/7)) - } - return ret; + function weekLongLabel(date) { + var endDate = new Date(date) + endDate.setDate(endDate.getDate() + 6) + return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + " - " + endDate.toLocaleDateString(Qt.locale(), Locale.ShortFormat) } + function monthsStart() { var d = new Date(); d.setHours(0,0,0,0); d.setMonth(d.getMonth() - monthsCount + 1, 1); return d; } - function monthsList() { - var ret = [] - var startTime = monthsStart(); - for (var i = 0; i < monthsCount; i++) { - var last = new Date(startTime) - ret.push(last.setMonth(last.getMonth() + i)) - } - return ret; + function monthLabel(date) { + return date.toLocaleString(Qt.locale(), "MMM") } - function monthsListNames() { - var ret = [] - var list = monthsList(); - for (var i = 0; i < list.length; i++) { - ret.push(new Date(list[i]).toLocaleString(Qt.locale(), "MMM")) - } - return ret; + function monthLongLabel(date) { + return date.toLocaleString(Qt.locale(), "MMMM yyyy") } function yearStart() { var d = new Date(); d.setHours(0,0,0,0); - d.setDate(1); - d.setMonth(0); + d.setFullYear(d.getFullYear() - yearsCount + 1, 0, 1) return d; } + function yearLabel(date) { + return date.toLocaleString(Qt.locale(), "yyyy") + } }