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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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