diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index a5c0392c..b6fe22ac 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -294,5 +294,9 @@ ui/images/sensors/o3.svg ui/images/sensors/pm10.svg ui/images/sensors/no2.svg + ui/images/power-grid.svg + ui/images/arrow-up.svg + ui/images/plus.svg + ui/images/minus.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index dc9eeaa6..0d8ec39f 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -252,7 +252,6 @@ ui/devicepages/CoolingThingPage.qml ui/devicepages/EvChargerThingPage.qml ui/components/NymeaSpinBox.qml - ui/mainviews/EnergyPieChartDelegate.qml ui/mainviews/energy/PowerConsumptionBalanceHistory.qml ui/mainviews/energy/PowerProductionBalanceHistory.qml ui/mainviews/energy/ConsumersBarChart.qml @@ -279,5 +278,7 @@ ui/components/ActivityIndicator.qml ui/system/zigbee/ZigbeeNodePage.qml ui/utils/AirQualityIndex.qml + ui/mainviews/energy/PowerBalanceHistory.qml + ui/mainviews/energy/CurrentPowerBalancePieChart.qml diff --git a/nymea-app/ui/images/arrow-up.svg b/nymea-app/ui/images/arrow-up.svg new file mode 100644 index 00000000..5317d273 --- /dev/null +++ b/nymea-app/ui/images/arrow-up.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/minus.svg b/nymea-app/ui/images/minus.svg new file mode 100644 index 00000000..5db7dc93 --- /dev/null +++ b/nymea-app/ui/images/minus.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/plus.svg b/nymea-app/ui/images/plus.svg new file mode 100644 index 00000000..5ddecdc7 --- /dev/null +++ b/nymea-app/ui/images/plus.svg @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/images/power-grid.svg b/nymea-app/ui/images/power-grid.svg new file mode 100644 index 00000000..e766be64 --- /dev/null +++ b/nymea-app/ui/images/power-grid.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/mainviews/EnergyView.qml b/nymea-app/ui/mainviews/EnergyView.qml index e3555292..3b0652b2 100644 --- a/nymea-app/ui/mainviews/EnergyView.qml +++ b/nymea-app/ui/mainviews/EnergyView.qml @@ -108,37 +108,58 @@ MainViewBase { id: energyGrid width: parent.width property int rawColumns: Math.floor(flickable.width / 300) - columns: Math.max(1, rawColumns - (rawColumns % 2)) + columns: Math.min(3, Math.max(1, rawColumns /*- (rawColumns % 2)*/)) rowSpacing: 0 columnSpacing: 0 - CurrentConsumptionBalancePieChart { +// CurrentConsumptionBalancePieChart { +// Layout.fillWidth: true +// Layout.preferredHeight: width +// energyManager: energyManager +// visible: producers.count > 0 +// animationsEnabled: Qt.application.active && root.isCurrentItem +// } +// CurrentProductionBalancePieChart { +// Layout.fillWidth: true +// Layout.preferredHeight: width +// energyManager: energyManager +// visible: producers.count > 0 +// animationsEnabled: Qt.application.active && root.isCurrentItem +// } + CurrentPowerBalancePieChart { Layout.fillWidth: true Layout.preferredHeight: width energyManager: energyManager - visible: producers.count > 0 + visible: rootMeter != null || producers.count > 0 animationsEnabled: Qt.application.active && root.isCurrentItem } - CurrentProductionBalancePieChart { + + PowerBalanceHistory { + Layout.fillWidth: true + Layout.preferredHeight: width + visible: rootMeter != null || producers.count > 0 + } + + PowerBalanceStats { Layout.fillWidth: true Layout.preferredHeight: width energyManager: energyManager - visible: producers.count > 0 - animationsEnabled: Qt.application.active && root.isCurrentItem + visible: rootMeter != null || producers.count > 0 + producers: producers } - PowerConsumptionBalanceHistory { - Layout.fillWidth: true - Layout.preferredHeight: width - visible: producers.count > 0 - } +// PowerConsumptionBalanceHistory { +// Layout.fillWidth: true +// Layout.preferredHeight: width +// visible: producers.count > 0 +// } - PowerProductionBalanceHistory { - Layout.fillWidth: true - Layout.preferredHeight: width - visible: producers.count > 0 - } +// PowerProductionBalanceHistory { +// Layout.fillWidth: true +// Layout.preferredHeight: width +// visible: producers.count > 0 +// } ConsumersPieChart { Layout.fillWidth: true @@ -153,19 +174,11 @@ MainViewBase { ConsumersHistory { Layout.fillWidth: true Layout.preferredHeight: width - visible: consumers.count > 0 || rootMeter != null + visible: consumers.count > 0 colors: root.thingColors consumers: consumers } - PowerBalanceStats { - Layout.fillWidth: true - Layout.preferredHeight: width - energyManager: energyManager - visible: rootMeter != null || producers.count > 0 - producers: producers - } - ConsumerStats { Layout.fillWidth: true Layout.preferredHeight: width diff --git a/nymea-app/ui/mainviews/energy/ConsumerStats.qml b/nymea-app/ui/mainviews/energy/ConsumerStats.qml index ca5db828..2e73eec1 100644 --- a/nymea-app/ui/mainviews/energy/ConsumerStats.qml +++ b/nymea-app/ui/mainviews/energy/ConsumerStats.qml @@ -160,12 +160,12 @@ StatsBase { anchors.fill: parent spacing: 0 - Label { - Layout.fillWidth: true - Layout.margins: Style.smallMargins - horizontalAlignment: Text.AlignHCenter - text: qsTr("Consumers totals") - } +// Label { +// Layout.fillWidth: true +// Layout.margins: Style.smallMargins +// horizontalAlignment: Text.AlignHCenter +// text: qsTr("Consumers totals") +// } SelectionTabs { id: selectionTabs @@ -207,9 +207,10 @@ StatsBase { backgroundColor: "transparent" // margins.left: 0 margins.right: 0 - margins.bottom: 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 @@ -309,6 +310,29 @@ StatsBase { } } + RowLayout { + 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) + ColorIcon { + name: app.interfacesToIcon(legendDelegate.thing.thingClass.interfaces) + size: Style.smallIconSize + color: index >= 0 ? NymeaUtils.generateColor(Style.generationBaseColor, index) : "white" + anchors.centerIn: parent + } + } + } + } + Item { anchors.fill: parent anchors.leftMargin: chartView.x + chartView.plotArea.x diff --git a/nymea-app/ui/mainviews/energy/ConsumersHistory.qml b/nymea-app/ui/mainviews/energy/ConsumersHistory.qml index 41ec4758..68b051b7 100644 --- a/nymea-app/ui/mainviews/energy/ConsumersHistory.qml +++ b/nymea-app/ui/mainviews/energy/ConsumersHistory.qml @@ -111,12 +111,12 @@ Item { anchors.fill: parent spacing: 0 - Label { - Layout.fillWidth: true - Layout.margins: Style.smallMargins - horizontalAlignment: Text.AlignHCenter - text: qsTr("Consumers history") - } +// Label { +// Layout.fillWidth: true +// Layout.margins: Style.smallMargins +// horizontalAlignment: Text.AlignHCenter +// text: qsTr("Consumers history") +// } SelectionTabs { id: selectionTabs @@ -186,9 +186,10 @@ Item { backgroundColor: "transparent" margins.left: 0 margins.right: 0 - margins.bottom: 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 @@ -470,6 +471,31 @@ Item { } } + RowLayout { + 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) + ColorIcon { + name: app.interfacesToIcon(legendDelegate.thing.thingClass.interfaces) + size: Style.smallIconSize + color: index >= 0 ? NymeaUtils.generateColor(Style.generationBaseColor, index) : "white" + anchors.centerIn: parent + } + } + } + + } + + MouseArea { id: mouseArea anchors.fill: parent diff --git a/nymea-app/ui/mainviews/energy/ConsumersPieChart.qml b/nymea-app/ui/mainviews/energy/ConsumersPieChart.qml index db8de374..ef8cffe2 100644 --- a/nymea-app/ui/mainviews/energy/ConsumersPieChart.qml +++ b/nymea-app/ui/mainviews/energy/ConsumersPieChart.qml @@ -11,7 +11,7 @@ ChartView { id: root backgroundColor: "transparent" animationOptions: animationsEnabled ? NymeaUtils.chartsAnimationOptions : ChartView.NoAnimation - title: qsTr("Consumers balance") +// title: qsTr("Consumers balance") titleColor: Style.foregroundColor legend.visible: false @@ -66,6 +66,7 @@ ChartView { id: d property var thingsColorMap: ({}) property PieSlice unknownSlice: null + property PieSlice idleSlice: null property double consumersSummation: 0 } @@ -73,6 +74,9 @@ ChartView { function updateConsumers() { root.animationOptions = ChartView.NoAnimation consumersBalanceSeries.clear(); + d.unknownSlice = null + d.idleSlice = null + print("cleared consumers pie chart") if (engine.thingManager.fetchingData) { return; @@ -102,6 +106,11 @@ ChartView { d.unknownSlice.color = Style.gray d.unknownSlice.borderColor = Style.gray d.unknownSlice.borderWidth = 0 + } else { + d.idleSlice = consumersBalanceSeries.append(qsTr(""), 0.00001) + d.idleSlice.color = Style.tooltipBackgroundColor + d.idleSlice.borderColor = d.idleSlice.color + d.idleSlice.borderWidth = 0 } d.thingsColorMap = colorMap diff --git a/nymea-app/ui/mainviews/energy/CurrentPowerBalancePieChart.qml b/nymea-app/ui/mainviews/energy/CurrentPowerBalancePieChart.qml new file mode 100644 index 00000000..79618d41 --- /dev/null +++ b/nymea-app/ui/mainviews/energy/CurrentPowerBalancePieChart.qml @@ -0,0 +1,867 @@ +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 +import "qrc:/ui/components" + +Item { + id: root + + property bool animationsEnabled: false + property EnergyManager energyManager: null + + readonly property double fromGrid: Math.max(0, energyManager.currentPowerAcquisition) + readonly property double fromStorage: -Math.min(0, energyManager.currentPowerStorage) + readonly property double toStorage: -Math.min(0, -energyManager.currentPowerStorage) + readonly property double fromProduction: energyManager.currentPowerConsumption - fromGrid - fromStorage + readonly property double toGrid: Math.max(0, - energyManager.currentPowerAcquisition) + + QtObject { + id: d + function formatValue(value) { + var ret + if (value >= 1000) { + ret = (value / 1000).toFixed(1) + "kW" + } else { + ret = value.toFixed(1) + "W" + } + return ret + } + + property double progress: 0 + onProgressChanged: canvas.requestPaint() + + property int chartSize: width / 2.5 + + property point acquisitionPos: Qt.point(chartSize/2 + Style.margins, chartSize/2 + Style.margins) + property point productionPos: Qt.point(root.width - (chartSize/2 + Style.margins), chartSize/2 + Style.margins) + property point storagePos: Qt.point(chartSize/2 + Style.margins, root.height - (chartSize/2 + Style.margins)) + property point consumptionPos: batteries.count > 0 || producers.count === 0 + ? Qt.point(root.width - (chartSize/2 + Style.margins), root.height - (chartSize/2 + Style.margins)) + : Qt.point(root.width / 2, root.height - (chartSize/2 + Style.margins)) + } + + ThingsProxy { + id: batteries + engine: _engine + shownInterfaces: ["energystorage"] + } + ThingsProxy { + id: producers + engine: _engine + shownInterfaces: ["smartmeterproducer"] + } + + NumberAnimation { + id: progressAnimation + target: d + property: "progress" + from: 0 + to: 1 + running: root.animationsEnabled + loops: Animation.Infinite + duration: 5000 + } + + Canvas { + id: canvas + anchors.fill: parent + + onPaint: { + var ctx = getContext("2d"); + + var solarPos = Qt.point(d.productionPos.x - width / 2, d.productionPos.y - height / 2) + var storagePos = Qt.point(d.storagePos.x - width / 2, d.storagePos.y - width / 2) + var consumptionPos = Qt.point(d.consumptionPos.x - width / 2, d.consumptionPos.y - height / 2) + var gridPos = Qt.point(d.acquisitionPos.x - width / 2, d.acquisitionPos.y - height / 2) + + ctx.save(); + ctx.reset() + + ctx.translate(width / 2, height / 2); + + ctx.strokeStyle = Style.foregroundColor + ctx.fillStyle = Style.foregroundColor + ctx.lineWidth = 2 + + var biggest = Math.max( + Math.abs(energyManager.currentPowerAcquisition), + Math.abs(energyManager.currentPowerConsumption), + Math.abs(energyManager.currentPowerProduction), + Math.abs(energyManager.currentPowerStorage) + ) + var size + + + if (root.toGrid > 0) { + size = root.toGrid / biggest + drawDottedCurve(ctx, solarPos, gridPos, size, Style.yellow) + } + + if (energyManager.currentPowerProduction < 0 && root.fromProduction) { + size = root.fromProduction / biggest + drawDottedCurve(ctx, solarPos, consumptionPos, size, Style.green) + } + + if (batteries.count > 0) { + if (energyManager.currentPowerStorage > 0) { + if (energyManager.currentPowerProduction < 0) { + size = Math.abs(energyManager.currentPowerStorage) / biggest + drawDottedCurve(ctx, solarPos, storagePos, size, Style.purple) + } else { + size = Math.abs(energyManager.currentPowerStorage) / biggest + drawDottedCurve(ctx, gridPos, storagePos, size, Style.purple) + } + } + + if (energyManager.currentPowerStorage < 0) { + size = Math.abs(energyManager.currentPowerStorage) / biggest + drawDottedCurve(ctx, storagePos, consumptionPos, size, Style.orange) + } + } + + if (energyManager.currentPowerAcquisition > 0) { + size = Math.abs(energyManager.currentPowerAcquisition) / biggest + drawDottedCurve(ctx, gridPos, consumptionPos, size, Style.red) + } + + ctx.restore(); + } + + function bezierCurvePoint(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t) { + var x = Math.pow(1-t, 3)*p0x + 3*Math.pow(1-t, 2)*t*p1x + 3*(1-t)*Math.pow(t, 2)*p2x + Math.pow(t, 3)*p3x; + var y = Math.pow(1-t, 3)*p0y + 3*Math.pow(1-t, 2)*t*p1y + 3*(1-t)*Math.pow(t, 2)*p2y + Math.pow(t, 3)*p3y; + return Qt.point(x, y) + } + + function circlePoint(center, radius, angle) { + var x = center.x + radius * Math.cos(angle * 2 * Math.PI / 360) + var y = center.y + radius * Math.sin(angle * 2 * Math.PI / 360) + return Qt.point(x, y) + } + + function drawDottedCurve(ctx, start, end, size, color) { + var c1 = getControlPoint(start) + var c2 = getControlPoint(end) + ctx.fillStyle = color + ctx.strokeStyle = color + var count = 10; + for (var i = 1; i <= count; i++) { + var offset = 1 / count; + var progress = d.progress + i * offset + if (progress > 1) + progress -= 1 + var point = bezierCurvePoint(start.x, start.y, c1.x, c1.y, c2.x, c2.y, end.x, end.y, progress) +// print("painting", d.progress, point.x, point.y) + ctx.beginPath(); + ctx.arc(point.x, point.y, Math.max(1, size * 5), 0, 2 *Math.PI) + ctx.stroke(); + ctx.fill(); + ctx.closePath(); + + } + + } + + function getControlPoint(point) { + return Qt.point(point.x * .1, point.y * .1) + } + + } + + Item { + id: acquisitionItem + x: d.acquisitionPos.x - width / 2 + y: d.acquisitionPos.y - height / 2 + width: d.chartSize + height: d.chartSize + + Rectangle { + anchors.centerIn: parent + width: acquisitionChart.plotArea.width + height: acquisitionChart.plotArea.height + color: Style.backgroundColor + radius: width / 2 + } + + ColumnLayout { + anchors.centerIn: parent + width: acquisitionChart.plotArea.width * 0.8 + ColorIcon { + Layout.alignment: Qt.AlignHCenter + size: Style.bigIconSize + // color: Style.red + name: "/ui/images/power-grid.svg" + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: d.formatValue(Math.abs(energyManager.currentPowerAcquisition)) +// color: energyManager.currentPowerAcquisition >= 0 ? Style.red : Style.yellow + } + } + + + ChartView { + id: acquisitionChart + anchors.fill: parent + legend.visible: false + margins { left: 0; top: 0; right: 0; bottom: 0 } + backgroundColor: "transparent" + animationOptions: root.animationsEnabled ? NymeaUtils.chartsAnimationOptions : ChartView.NoAnimation + + PieSeries { + size: 1 + holeSize: 0.8 + + PieSlice { + color: Style.red + borderColor: color + borderWidth: 0 + value: root.fromGrid + } + PieSlice { + color: Style.yellow + borderColor: color + borderWidth: 0 + value: root.toGrid + } + PieSlice { + color: Style.tooltipBackgroundColor + borderColor: color + borderWidth: 0 + value: energyManager.currentPowerAcquisition == 0 ? 1 : 0 + } + } + } + } + + + Item { + id: productionItem + x: d.productionPos.x - width / 2 + y: d.productionPos.y - height / 2 + width: d.chartSize + height: d.chartSize + visible: producers.count > 0 + + Rectangle { + anchors.centerIn: parent + width: productionChart.plotArea.width + height: productionChart.plotArea.height + color: Style.backgroundColor + radius: width / 2 + } + + ColumnLayout { + anchors.centerIn: parent + width: productionChart.plotArea.width * 0.8 + ColorIcon { + Layout.alignment: Qt.AlignHCenter + size: Style.bigIconSize + // color: Style.yellow + name: "/ui/images/weathericons/weather-clear-day.svg" + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: d.formatValue(Math.abs(energyManager.currentPowerProduction)) + // color: energyManager.currentPowerAcquisition >= 0 ? Style.red : Style.green + } + } + + + ChartView { + id: productionChart + anchors.fill: parent + legend.visible: false + backgroundColor: "transparent" + margins { left: 0; top: 0; right: 0; bottom: 0 } + animationOptions: root.animationsEnabled ? NymeaUtils.chartsAnimationOptions : ChartView.NoAnimation + + PieSeries { + size: 1 + holeSize: 0.8 + + PieSlice { + color: Style.green + borderColor: color + borderWidth: 0 + value: root.fromProduction + } + PieSlice { + color: Style.purple + borderColor: color + borderWidth: 0 + value: root.toStorage + } + PieSlice { + color: Style.yellow + borderColor: color + borderWidth: 0 + value: root.toGrid + } + PieSlice { + color: Style.tooltipBackgroundColor + borderColor: color + borderWidth: 0 + value: energyManager.currentPowerProduction == 0 ? 1 : 0 + } + } + } + } + + Item { + id: consumptionItem + x: d.consumptionPos.x - width / 2 + y: d.consumptionPos.y - height / 2 + width: d.chartSize + height: d.chartSize + + Rectangle { + anchors.centerIn: parent + width: consumptionChart.plotArea.width + height: consumptionChart.plotArea.height + color: Style.backgroundColor + radius: width / 2 + } + + ColumnLayout { + anchors.centerIn: parent + width: consumptionChart.plotArea.width * 0.8 + ColorIcon { + Layout.alignment: Qt.AlignHCenter + size: Style.bigIconSize + // color: Style.blue + name: "/ui/images/powersocket.svg" + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: d.formatValue(energyManager.currentPowerConsumption) + // color: energyManager.currentPowerAcquisition >= 0 ? Style.red : Style.green + } + } + + ChartView { + id: consumptionChart + anchors.fill: parent + margins { left: 0; top: 0; right: 0; bottom: 0 } + legend.visible: false + backgroundColor: "transparent" + animationOptions: root.animationsEnabled ? NymeaUtils.chartsAnimationOptions : ChartView.NoAnimation + + PieSeries { + size: 1 + holeSize: 0.8 + + PieSlice { + color: Style.red + borderColor: color + borderWidth: 0 + value: root.fromGrid + } + PieSlice { + color: Style.green + borderColor: color + borderWidth: 0 + value: root.fromProduction + } + PieSlice { + color: Style.orange + borderColor: color + borderWidth: 0 + value: root.fromStorage + } + } + } + } + + + Item { + id: batteryItem + x: d.storagePos.x - width / 2 + y: d.storagePos.y - height / 2 + width: d.chartSize + height: d.chartSize + visible: batteries.count > 0 + + Rectangle { + anchors.centerIn: parent + width: batteryChart.plotArea.width + height: batteryChart.plotArea.height + color: Style.backgroundColor + radius: width / 2 + } + + ColumnLayout { + anchors.centerIn: parent + width: productionChart.plotArea.width * 0.8 + ColorIcon { + Layout.alignment: Qt.AlignHCenter + size: Style.bigIconSize + // color: Style.purple + name: "/ui/images/battery/battery-" + NymeaUtils.pad(Math.round(batteryChart.averageLevel / 10) * 10, 3) + ".svg" + } + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: d.formatValue(Math.abs(energyManager.currentPowerStorage)) + // color: energyManager.currentPowerStorage >= 0 ? Style.green : Style.red + } + } + + Label { + anchors.horizontalCenter: parent.horizontalCenter + y: batteryChart.y + batteryChart.plotArea.height * .2 + horizontalAlignment: Text.AlignHCenter + font: Style.smallFont + text: batteryChart.averageLevel + "%" +// color: energyManager.currentPowerStorage >= 0 ? Style.green : Style.red + } + + ChartView { + id: batteryChart + anchors.fill: parent + margins { left: 0; top: 0; right: 0; bottom: 0 } + legend.visible: false + backgroundColor: "transparent" + animationOptions: root.animationsEnabled ? NymeaUtils.chartsAnimationOptions : ChartView.NoAnimation + + property double totalCapacity: { + var totalCapacity = 0; + for (var i = 0; i < batteriesRepeater.count; i++) { + totalCapacity += batteriesRepeater.itemAt(i).capacityState.value + } + return totalCapacity; + } + property double averageLevel: { + if (batteriesRepeater.count == 0) { + return 0; + } + + var averageLevel = 0; + for (var i = 0; i < batteriesRepeater.count; i++) { + averageLevel += batteriesRepeater.itemAt(i).batteryLevelState.value + } + averageLevel /= batteriesRepeater.count + return averageLevel; + } + + Repeater { + id: batteriesRepeater + model: batteries + delegate: Item { + property Thing thing: batteries.get(index) + property State batteryLevelState: thing.stateByName("batteryLevel") + property State capacityState: thing.stateByName("capacity") + } + } + + PieSeries { + id: batterySeries + size: 1 + holeSize: 0.8 + + PieSlice { + color: energyManager.currentPowerStorage == 0 + ? Style.foregroundColor + : root.toStorage > 0 + ? Style.purple + : Style.orange + borderColor: color + borderWidth: 0 + value: batteryChart.averageLevel + } + PieSlice { + color: Style.tooltipBackgroundColor + borderColor: color + borderWidth: 0 + value: 100 - batteryChart.averageLevel + } + } + } + } + +} + +//ChartView { +// id: consumptionPieChart +// backgroundColor: "transparent" +// animationOptions: animationsEnabled ? NymeaUtils.chartsAnimationOptions : ChartView.NoAnimation +// title: qsTr("My energy mix") +// titleColor: Style.foregroundColor +// legend.visible: false + +// margins.left: 0 +// margins.right: 0 +// margins.bottom: 0 +// margins.top: 0 + +// property bool animationsEnabled: true +// property EnergyManager energyManager: null + +// ThingsProxy { +// id: batteries +// engine: _engine +// shownInterfaces: ["energystorage"] +// } + +// PieSeries { +// id: consumptionBalanceSeries +// size: 0.88 +// holeSize: 0.7 + +// property double fromGrid: Math.max(0, energyManager.currentPowerAcquisition) +// property double fromStorage: -Math.min(0, energyManager.currentPowerStorage) +// property double toStorage: -Math.min(0, -energyManager.currentPowerStorage) +// property double fromProduction: energyManager.currentPowerConsumption - fromGrid - fromStorage +// property double toGrid: Math.max(0, - energyManager.currentPowerAcquisition) + +// PieSlice { +// color: Style.red +// borderColor: color +// borderWidth: 0 +// value: consumptionBalanceSeries.fromGrid +// } +// PieSlice { +// color: Style.green +// borderColor: color +// borderWidth: 0 +// value: consumptionBalanceSeries.fromProduction +// } +// PieSlice { +// color: Style.purple +// borderColor: color +// borderWidth: 0 +// value: consumptionBalanceSeries.fromStorage +// } +// PieSlice { +// color: Style.yellow +// borderColor: color +// borderWidth: 0 +// value: consumptionBalanceSeries.toGrid +// } +// PieSlice { +// color: Style.orange +// borderColor: color +// borderWidth: 0 +// value: consumptionBalanceSeries.toStorage +// } + +// PieSlice { +// color: Style.tooltipBackgroundColor +// borderColor: color +// borderWidth: 0 +// value: consumptionBalanceSeries.fromGrid == 0 && consumptionBalanceSeries.fromProduction == 0 && consumptionBalanceSeries.fromStorage == 0 ? 1 : 0 +// } +// } + +// Item { +// id: centerItem + +// x: consumptionPieChart.plotArea.x + (consumptionPieChart.plotArea.width - width) / 2 +// y: consumptionPieChart.plotArea.y + (consumptionPieChart.plotArea.height - height) / 2 +// width: consumptionPieChart.plotArea.width * 0.65 +// height: width + +//// Rectangle { +//// anchors.fill: parent +//// color: "white" +//// } + +// QtObject { +// id: d +// property double progress: 0 +// onProgressChanged: canvas.requestPaint() +// } + +// NumberAnimation { +// id: progressAnimation +// target: d +// property: "progress" +// from: 0 +// to: 1 +// running: true +// loops: Animation.Infinite +// duration: 5000 +// } + + +// Canvas { +// id: canvas +// anchors.fill: parent + +// property int itemCount: batteries.count > 0 ? 4 : 3 + +// ColorIcon { +// property var point: canvas.circlePoint(Qt.point(canvas.width / 2, canvas.height / 2), canvas.height / 2, -90) +// x: point.x - width / 2 +// y: point.y - height / 2 +// name: "weathericons/weather-clear-day" +// } +// ColorIcon { +// property var point: canvas.circlePoint(Qt.point(canvas.width / 2, canvas.height / 2), canvas.height / 2, -90 + 360 / canvas.itemCount) +// x: point.x - width / 2 +// y: point.y - height / 2 +// name: "battery/battery-080" +// visible: batteries.count > 0 +// } +// ColorIcon { +// property var point: canvas.circlePoint(Qt.point(canvas.width / 2, canvas.height / 2), canvas.height / 2, -90 + 360 / canvas.itemCount * (batteries.count > 0 ? 2 : 1)) +// x: point.x - width / 2 +// y: point.y - height / 2 +// name: "things" +// } +// ColorIcon { +// property var point: canvas.circlePoint(Qt.point(canvas.width / 2, canvas.height / 2), canvas.height / 2, -90 + 360 / canvas.itemCount * (batteries.count > 0 ? 3 : 2)) +// x: point.x - width / 2 +// y: point.y - height / 2 +// name: "energy" +// } + +// onPaint: { +// var ctx = getContext("2d"); + +// var solarPos = circlePoint(Qt.point(0, 0), height / 2, -90) +// var storagePos = circlePoint(Qt.point(0, 0), height / 2, -90 / 360 * itemCount * 1) +// var consumptionPos = circlePoint(Qt.point(0, 0), height / 2, -90 + 360 / itemCount * (batteries.count > 0 ? 2 : 1)) +// var gridPos = circlePoint(Qt.point(0, 0), height / 2, -90 + 360 / itemCount * (batteries.count > 0 ? 3 : 2)) + +// ctx.save(); +// ctx.reset() + +// ctx.translate(width / 2, height / 2); + +// ctx.strokeStyle = Style.foregroundColor +// ctx.fillStyle = Style.foregroundColor +// ctx.lineWidth = 2 + +//// ctx.beginPath(); +//// ctx.moveTo(0, -height / 2); +//// ctx.bezierCurveTo(0, -height / 10, -width / 10, 0, -width / 2, 0) +//// ctx.stroke(); +//// ctx.closePath(); + +//// ctx.beginPath(); +//// ctx.moveTo(-width / 2, 0); +//// ctx.bezierCurveTo(-width / 10, 0, 0, height / 10, 0, height / 2) +//// ctx.stroke(); +//// ctx.closePath(); + +//// ctx.beginPath(); +//// ctx.moveTo(0, height / 2); +//// ctx.bezierCurveTo(0, height / 10, width / 10, 0, width / 2, 0) +//// ctx.stroke(); +//// ctx.closePath(); + +//// ctx.beginPath(); +//// ctx.moveTo(width / 2, 0); +//// ctx.bezierCurveTo(width / 10, 0, 0, -height / 10, 0, -height / 2) +//// ctx.stroke(); +//// ctx.closePath(); + +// var size = Math.abs(energyManager.currentPowerAcquisition) / Math.abs(energyManager.currentPowerProduction) +// drawDottedCurve(ctx, solarPos, gridPos, size) + +// size = Math.abs(energyManager.currentPowerConsumption) / Math.abs(energyManager.currentPowerProduction) +// drawDottedCurve(ctx, solarPos, consumptionPos, size) + +// if (batteries.count > 0) { +// size = Math.abs(energyManager.currentPowerStorage) / Math.abs(energyManager.currentPowerProduction) +// drawDottedCurve(ctx, solarPos, storagePos, size) + +// if (energyManager.currentPowerStorage < 0) { +// size = Math.abs(energyManager.currentPowerStorage) / Math.abs(energyManager.currentPowerConsumption) +// drawDottedCurve(ctx, storagePos, consumptionPos, size) +// } +// } + +// if (energyManager.currentPowerAcquisition > 0) { +// size = Math.abs(energyManager.currentPowerAcquisition) / Math.abs(energyManager.currentPowerConsumption) +// drawDottedCurve(ctx, gridPos, consumptionPos, size) +// } + +//// var count = 5; +//// for (var i = 1; i <= count; i++) { +//// var offset = 1 / count; +//// var progress = d.progress + i * offset +//// if (progress > 1) +//// progress -= 1 +//// var point = bezierCurvePoint(width / 2, 0, width / 10, 0, 0, -height / 10, 0, -height / 2, progress) +//// // print("painting", d.progress, point.x, point.y) +//// ctx.beginPath(); +//// ctx.arc(point.x, point.y, 4, 0, 2 *Math.PI) +//// ctx.stroke(); +//// ctx.closePath(); + +//// } + + +// ctx.restore(); +// } + +// function bezierCurvePoint(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t) { +// var x = Math.pow(1-t, 3)*p0x + 3*Math.pow(1-t, 2)*t*p1x + 3*(1-t)*Math.pow(t, 2)*p2x + Math.pow(t, 3)*p3x; +// var y = Math.pow(1-t, 3)*p0y + 3*Math.pow(1-t, 2)*t*p1y + 3*(1-t)*Math.pow(t, 2)*p2y + Math.pow(t, 3)*p3y; +// return Qt.point(x, y) +// } + +// function circlePoint(center, radius, angle) { +// var x = center.x + radius * Math.cos(angle * 2 * Math.PI / 360) +// var y = center.y + radius * Math.sin(angle * 2 * Math.PI / 360) +// return Qt.point(x, y) +// } + +// function drawDottedCurve(ctx, start, end, size) { +// var c1 = getControlPoint(start) +// var c2 = getControlPoint(end) +// var count = 10; +// for (var i = 1; i <= count; i++) { +// var offset = 1 / count; +// var progress = d.progress + i * offset +// if (progress > 1) +// progress -= 1 +// var point = bezierCurvePoint(start.x, start.y, c1.x, c1.y, c2.x, c2.y, end.x, end.y, progress) +// // print("painting", d.progress, point.x, point.y) +// ctx.beginPath(); +// ctx.arc(point.x, point.y, size * 5, 0, 2 *Math.PI) +// ctx.stroke(); +// ctx.fill(); +// ctx.closePath(); + +// } + +// } + +// function getControlPoint(point) { +// return Qt.point(point.x * .1, point.y * .1) +// } + +// } + +// } + + +// Column { +// id: centerLayout +// x: consumptionPieChart.plotArea.x + (consumptionPieChart.plotArea.width - width) / 2 +// y: consumptionPieChart.plotArea.y + (consumptionPieChart.plotArea.height - height) / 2 +// width: consumptionPieChart.plotArea.width * 0.65 +//// height: consumptionPieChart.plotArea.height * 0.65 +// height: childrenRect.height +// spacing: Style.smallMargins + +// visible: false + +// ColumnLayout { +// width: parent.width +// spacing: 0 +// Label { +// text: qsTr("Consumption") +// 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.smallFont +// color: Style.blue +// } +// } +// ColumnLayout { +// width: parent.width +// spacing: 0 +// Label { +// text: qsTr("Production") +// font: Style.smallFont +// Layout.fillWidth: true +// horizontalAlignment: Text.AlignHCenter +// } + +// Label { +// property double absValue: Math.abs(energyManager.currentPowerProduction) +// text: "%1 %2" +// .arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) +// .arg(absValue > 1000 ? "kW" : "W") +// Layout.fillWidth: true +// horizontalAlignment: Text.AlignHCenter +//// font: Style.bigFont +// color: Style.yellow + +// } +// } + + +// ColumnLayout { +// width: parent.width +// spacing: 0 +// Label { +// text: qsTr("From grid") +// Layout.fillWidth: true +// horizontalAlignment: Text.AlignHCenter +// font: Style.extraSmallFont +// } +// Label { +// property double absValue: consumptionBalanceSeries.fromGrid +// color: Style.red +// text: "%1 %2" +// .arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) +// .arg(absValue > 1000 ? "kW" : "W") +// Layout.fillWidth: true +// horizontalAlignment: Text.AlignHCenter +// font: Style.smallFont +// } +// } + + +// ColumnLayout { +// width: parent.width +// spacing: 0 +// Label { +// text: qsTr("From self production") +// Layout.fillWidth: true +// horizontalAlignment: Text.AlignHCenter +// font: Style.extraSmallFont +// } +// Label { +// color: Style.green +// property double absValue: consumptionBalanceSeries.fromProduction +// text: "%1 %2".arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) +// .arg(absValue > 1000 ? "kW" : "W") +// Layout.fillWidth: true +// horizontalAlignment: Text.AlignHCenter +// font: Style.smallFont +// } +// } +// ColumnLayout { +// width: parent.width +// spacing: 0 +// visible: batteries.count > 0 +// Label { +// text: energyManager.currentPowerStorage < 0 ? qsTr("From battery") : qsTr("To battery") +// Layout.fillWidth: true +// horizontalAlignment: Text.AlignHCenter +// font: Style.extraSmallFont +// } +// Label { +// color: value < 0 ? Style.purple : Style.orange +// property double value: energyManager.currentPowerStorage +// property double absValue: Math.abs(energyManager.currentPowerStorage) +// text: "%1 %2".arg((absValue / (absValue > 1000 ? 1000 : 1)).toFixed(1)) +// .arg(absValue > 1000 ? "kW" : "W") +// Layout.fillWidth: true +// horizontalAlignment: Text.AlignHCenter +// font: Style.smallFont +// } +// } +// } +//} diff --git a/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml b/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml new file mode 100644 index 00000000..5ba51c01 --- /dev/null +++ b/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml @@ -0,0 +1,791 @@ +import QtQuick 2.0 +import QtCharts 2.2 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 +import Nymea 1.0 +import "qrc:/ui/components" + +Item { + id: root + + 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() + } + + property ThingsProxy batteries: ThingsProxy { + engine: _engine + shownInterfaces: ["energystorage"] + } + + QtObject { + id: d + property date now: new Date() + + 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 + } + + } + + Connections { + target: powerBalanceLogs + + onEntriesAdded: { +// print("entries added", index, entries.length) + for (var i = 0; i < entries.length; i++) { + var entry = entries[i] +// print("got entry", entry.timestamp) + + zeroSeries.ensureValue(entry.timestamp) + // For debugging, to see if the other maths line up with the plain production graph + productionSeries.insertEntry(index + i, entry) + consumptionSeries.insertEntry(index + i, entry) + selfProductionConsumptionSeries.insertEntry(index + i, entry) + toStorageSeries.insertEntry(index + i, entry) + fromStorageSeries.insertEntry(index + i, entry) + returnSeries.insertEntry(index + i, entry) + acquisitionSeries.insertEntry(index + i, entry) + if (entry.timestamp > d.now && new Date().getTime() - d.now.getTime() < 120000) { + d.now = entry.timestamp + } + } + } + + onEntriesRemoved: { + acquisitionUpperSeries.removePoints(index, count) + returnUpperSeries.removePoints(index, count) + fromStorageUpperSeries.removePoints(index, count) + toStorageUpperSeries.removePoints(index, count) + selfProductionConsumptionUpperSeries.removePoints(index, count) + productionSeries.removePoints(index, count) + consumptionSeries.removePoints(index, count) + zeroSeries.shrink() + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + +// Label { +// Layout.fillWidth: true +// Layout.margins: Style.smallMargins +// horizontalAlignment: Text.AlignHCenter +// text: qsTr("My production history") +// } + + 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() + } + } + + 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: 0 + margins.right: 0 + margins.bottom: Style.smallIconSize + Style.margins + margins.top: 0 + + legend.alignment: Qt.AlignBottom + legend.labelColor: Style.foregroundColor + legend.font: Style.extraSmallFont + legend.visible: false + + 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 && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.startTime) + 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 && (powerBalanceLogs.count == 0 || powerBalanceLogs.get(0).timestamp > d.now) + font: Style.smallFont + opacity: .5 + } + + ValueAxis { + id: valueAxis + min: 0 + max: Math.ceil(Math.max(-powerBalanceLogs.minValue, powerBalanceLogs.maxValue) / 100) * 100 + labelFormat: "" + gridLineColor: Style.tileOverlayColor + labelsVisible: false + lineVisible: false + titleVisible: false + shadesVisible: false + } + 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))) / 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: selfProductionConsumptionSeries + axisX: dateTimeAxis + axisY: valueAxis + color: Style.green +// borderWidth: 2 + borderColor: color + name: qsTr("From self production") + // visible: false + + 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: selfProductionConsumptionUpperSeries + } + + + function calculateValue(entry) { + return Math.max(0, -entry.production) - Math.max(0, -entry.acquisition) - Math.max(0, entry.storage) + } + + function addEntry(entry) { + selfProductionConsumptionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) + } + function insertEntry(index, entry) { + selfProductionConsumptionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry)) + } + } + + AreaSeries { + id: toStorageSeries + axisX: dateTimeAxis + axisY: valueAxis + color: Style.purple + borderWidth: 0 + borderColor: color + visible: root.batteries.count > 0 + name: qsTr("To battery") + + + function calculateValue(entry) { + return selfProductionConsumptionSeries.calculateValue(entry) + Math.max(0, entry.storage); + } + + function addEntry(entry) { + toStorageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) + } + function insertEntry(index, entry) { + toStorageUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry)) + } + + lowerSeries: selfProductionConsumptionUpperSeries + upperSeries: LineSeries { + id: toStorageUpperSeries + } + } + + + AreaSeries { + id: returnSeries + axisX: dateTimeAxis + axisY: valueAxis + color: Style.yellow + borderWidth: 0 + borderColor: color + name: qsTr("To grid") + // visible: false + + function calculateValue(entry) { + return toStorageSeries.calculateValue(entry) + Math.max(0, -entry.acquisition) + } + function addEntry(entry) { + returnUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) + } + function insertEntry(index, entry) { + returnUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry)) + } + + lowerSeries: toStorageUpperSeries + upperSeries: LineSeries { + id: returnUpperSeries + } + } + + AreaSeries { + id: fromStorageSeries + axisX: dateTimeAxis + axisY: valueAxis + color: Style.orange + borderWidth: 0 + borderColor: color + name: qsTr("From battery") + visible: root.batteries.count > 0 + + lowerSeries: selfProductionConsumptionUpperSeries + upperSeries: LineSeries { + id: fromStorageUpperSeries + } + + function calculateValue(entry) { + return selfProductionConsumptionSeries.calculateValue(entry) + Math.abs(Math.min(0, entry.storage)); + } + + function addEntry(entry) { + fromStorageUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) + } + function insertEntry(index, entry) { + fromStorageUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry)) + } + } + + + AreaSeries { + id: acquisitionSeries + axisX: dateTimeAxis + axisY: valueAxis + color: Style.red + borderWidth: 0 + borderColor: color + name: qsTr("From grid") + // visible: false + + lowerSeries: fromStorageUpperSeries + upperSeries: LineSeries { + id: acquisitionUpperSeries + } + + function calculateValue(entry) { + return fromStorageSeries.calculateValue(entry) + Math.max(0, entry.acquisition) + } + function addEntry(entry) { + acquisitionUpperSeries.append(entry.timestamp.getTime(), calculateValue(entry)) + } + function insertEntry(index, entry) { + acquisitionUpperSeries.insert(index, entry.timestamp.getTime(), calculateValue(entry)) + } + } + + LineSeries { + id: productionSeries + axisX: dateTimeAxis + axisY: valueAxis + color: Style.white + width: 1 + name: "Total production" + + function calculateValue(entry) { + return Math.abs(Math.min(0, entry.production)) + } + function addEntry(entry) { + append(entry.timestamp.getTime(), calculateValue(entry)) + } + function insertEntry(index, entry) { + insert(index, entry.timestamp.getTime(), calculateValue(entry)) + } + } + + LineSeries { + id: consumptionSeries + axisX: dateTimeAxis + axisY: valueAxis + color: Style.red + width: 1 + name: "Total consumption" + visible: false + + function calculateValue(entry) { + return Math.max(0, entry.consumption) + } + function addEntry(entry) { + append(entry.timestamp.getTime(), calculateValue(entry)) + } + function insertEntry(index, entry) { + insert(index, entry.timestamp.getTime(), calculateValue(entry)) + } + } + + + } + + RowLayout { + anchors { left: parent.left; bottom: parent.bottom; right: parent.right } + anchors.leftMargin: chartView.plotArea.x + height: Style.smallIconSize + anchors.margins: Style.margins + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ColorIcon { + name: "weathericons/weather-clear-day" + size: Style.smallIconSize + color: Style.green + anchors.centerIn: parent + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Row { + anchors.centerIn: parent + ColorIcon { + name: "power-grid" + size: Style.smallIconSize + color: Style.red + } + ColorIcon { + name: "arrow-down" + size: Style.smallIconSize + color: Style.red + } + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Row { + anchors.centerIn: parent + ColorIcon { + name: "power-grid" + size: Style.smallIconSize + color: Style.yellow + } + ColorIcon { + name: "arrow-up" + size: Style.smallIconSize + color: Style.yellow + } + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: batteries.count > 0 + Row { + anchors.centerIn: parent + ColorIcon { + name: "battery/battery-080" + size: Style.smallIconSize + color: Style.purple + } + ColorIcon { + name: "plus" + size: Style.smallIconSize + color: Style.purple + } + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: batteries.count > 0 + Row { + anchors.centerIn: parent + ColorIcon { + name: "battery/battery-040" + size: Style.smallIconSize + color: Style.orange + } + ColorIcon { + name: "minus" + size: Style.smallIconSize + color: Style.orange + } + } + } + } + + 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: { + if (mouseArea.dragging) { + powerBalanceLogs.fetchLogs() + mouseArea.dragging = false; + } + + mouseArea.tooltipping = false; + } + + 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() + } + + 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: { + 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: powerBalanceLogs.fetchLogs() + } + + Rectangle { + height: parent.height + width: 1 + color: Style.foregroundColor + x: Math.min(mouseArea.width, 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.mouseX, mouseArea.width) - Style.smallMargins - width + x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft + property double maxValue: toolTip.entry ? Math.max(0, -entry.production) : 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 + width: parent.width + anchors { + left: parent.left + top: parent.top + margins: Style.smallMargins + } + Label { + text: toolTip.entry.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat) + font: Style.smallFont + } + + Label { + Layout.fillWidth: true + elide: Text.ElideRight + property double value: toolTip.entry + ? (toolTip.entry.acquisition >= 0 ? toolTip.entry.consumption : Math.max(0, -toolTip.entry.production)) + : 0 + property bool translate: value >= 1000 + property double translatedValue: value / (translate ? 1000 : 1) + text: toolTip.entry.acquisition >= 0 ? qsTr("Consumed: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") + : qsTr("Produced: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") + font: Style.smallFont + } +// Label { +// property double value: toolTip.entry ? toolTip.entry.consumption : 0 +// property bool translate: value >= 1000 +// property double translatedValue: value / (translate ? 1000 : 1) +// text: qsTr("Total consumption: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") +// font: Style.extraSmallFont +// } + + RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: toolTip.entry.acquisition >= 0 ? Style.red : Style.yellow + } + + Label { + Layout.fillWidth: true + elide: Text.ElideRight + // Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings + Component.onCompleted: lowerSeries = returnSeries.lowerSeries + property XYSeries lowerSeries: null + + property double value: toolTip.entry ? Math.abs(toolTip.entry.acquisition) : 0 + property bool translate: value >= 1000 + property double translatedValue: value / (translate ? 1000 : 1) + text: toolTip.entry.acquisition >= 0 ? qsTr("From grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") + : qsTr("To grid: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") + font: Style.extraSmallFont + } + } + RowLayout { + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: Style.green + } + + Label { + Layout.fillWidth: true + elide: Text.ElideRight + // Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings + Component.onCompleted: lowerSeries = selfProductionConsumptionSeries.lowerSeries + property XYSeries lowerSeries: null + + property double value: toolTip.entry ? Math.min(Math.max(0, toolTip.entry.consumption), -toolTip.entry.production) : 0 + property bool translate: value >= 1000 + property double translatedValue: value / (translate ? 1000 : 1) + text: toolTip.entry.acquisition >= 0 ? qsTr("From self production: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") + : qsTr("Consumed: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") + font: Style.extraSmallFont + } + } + RowLayout { + visible: root.batteries.count > 0 + Rectangle { + width: Style.extraSmallFont.pixelSize + height: width + color: toolTip.entry.storage > 0 ? Style.purple : Style.orange + } + + Label { + Layout.fillWidth: true + elide: Text.ElideRight + // Workaround for Qt bug that lowerSeries is non-notifyable and throws warnings + Component.onCompleted: lowerSeries = toStorageSeries.lowerSeries + property XYSeries lowerSeries: null + + property double value: toolTip.entry ? Math.abs(toolTip.entry.storage) : 0 + property bool translate: value >= 1000 + property double translatedValue: value / (translate ? 1000 : 1) + text: toolTip.entry.storage > 0 ? qsTr("To battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") : + qsTr("From battery: %1 %2").arg(translatedValue.toFixed(2)).arg(translate ? "kW" : "W") + font: Style.extraSmallFont + } + } + } + } + } + + } + } + + +} + + + diff --git a/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml b/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml index 9b291389..55d36029 100644 --- a/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml +++ b/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml @@ -106,12 +106,12 @@ StatsBase { anchors.fill: parent spacing: 0 - Label { - Layout.fillWidth: true - Layout.margins: Style.smallMargins - horizontalAlignment: Text.AlignHCenter - text: qsTr("Totals") - } +// Label { +// Layout.fillWidth: true +// Layout.margins: Style.smallMargins +// horizontalAlignment: Text.AlignHCenter +// text: qsTr("Totals") +// } SelectionTabs { id: selectionTabs @@ -186,13 +186,15 @@ StatsBase { anchors.fill: parent backgroundColor: "transparent" + + legend.visible: false legend.alignment: Qt.AlignBottom legend.font: Style.extraSmallFont legend.labelColor: Style.foregroundColor // margins.left: 0 margins.right: 0 - margins.bottom: 0 + margins.bottom: Style.smallIconSize + Style.margins margins.top: 0 ActivityIndicator { @@ -290,7 +292,7 @@ StatsBase { BarSet { id: productionSet label: qsTr("Produced") - color: Style.yellow + color: Style.green borderColor: color borderWidth: 0 values: { @@ -301,7 +303,6 @@ StatsBase { return ret } } - BarSet { id: acquisitionSet label: qsTr("From grid") @@ -319,7 +320,7 @@ StatsBase { BarSet { id: returnSet label: qsTr("To grid") - color: Style.green + color: Style.yellow borderColor: color borderWidth: 0 values: { @@ -333,6 +334,71 @@ StatsBase { } } + RowLayout { + anchors { left: parent.left; bottom: parent.bottom; right: parent.right } + anchors.leftMargin: chartView.plotArea.x + height: Style.smallIconSize + anchors.margins: Style.margins + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ColorIcon { + name: "powersocket" + size: Style.smallIconSize + color: Style.blue + anchors.centerIn: parent + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ColorIcon { + name: "weathericons/weather-clear-day" + size: Style.smallIconSize + color: Style.green + anchors.centerIn: parent + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Row { + anchors.centerIn: parent + ColorIcon { + name: "power-grid" + size: Style.smallIconSize + color: Style.red + } + ColorIcon { + name: "arrow-down" + size: Style.smallIconSize + color: Style.red + } + } + } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Row { + anchors.centerIn: parent + ColorIcon { + name: "power-grid" + size: Style.smallIconSize + color: Style.yellow + } + ColorIcon { + name: "arrow-up" + size: Style.smallIconSize + color: Style.yellow + } + } + } + } + + Item { anchors.fill: parent anchors.leftMargin: chartView.x + chartView.plotArea.x