736 lines
32 KiB
QML
736 lines
32 KiB
QML
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
*
|
|
* Copyright (C) 2013 - 2024, nymea GmbH
|
|
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
|
|
*
|
|
* This file is part of nymea-app.
|
|
*
|
|
* nymea-app is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* nymea-app is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with nymea-app. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls
|
|
import QtCharts
|
|
import Nymea
|
|
|
|
import "qrc:/ui/components/"
|
|
|
|
StatsBase {
|
|
id: root
|
|
|
|
property EnergyManager energyManager: null
|
|
|
|
property bool titleVisible: true
|
|
|
|
property string gridIcon
|
|
property string pvIcon
|
|
property string homeIcon
|
|
|
|
property ThingsProxy producers: ThingsProxy {
|
|
engine: _engine
|
|
shownInterfaces: ["smartmeterproducer"]
|
|
}
|
|
|
|
readonly property bool hasProducers: producers.count > 0
|
|
|
|
QtObject {
|
|
id: d
|
|
property var config: root.configs[selectionTabs.currentValue.config]
|
|
property int startOffset: 0
|
|
|
|
property var selectedSet: null
|
|
|
|
property date startTime: root.calculateTimestamp(config.startTime(), config.sampleRate, startOffset)
|
|
property date endTime: root.calculateTimestamp(config.startTime(), config.sampleRate, startOffset + config.count)
|
|
|
|
property bool fetchPending: false
|
|
property bool loading: fetchPending || wheelStopTimer.running || powerBalanceLogs.fetchingData
|
|
onLoadingChanged: {
|
|
if (!loading) {
|
|
refresh()
|
|
}
|
|
}
|
|
|
|
onConfigChanged: valueAxis.max = 1
|
|
onStartOffsetChanged: {
|
|
refresh()
|
|
}
|
|
|
|
function selectSet(set) {
|
|
if (d.selectedSet === set) {
|
|
d.selectedSet = null
|
|
} else {
|
|
d.selectedSet = set
|
|
}
|
|
}
|
|
|
|
function sampleMatches(entry, expectedTimestamp) {
|
|
if (!entry) {
|
|
return false
|
|
}
|
|
var maxDistance = d.config.sampleRate * 60 * 1000 / 2
|
|
return Math.abs(entry.timestamp.getTime() - expectedTimestamp.getTime()) <= maxDistance
|
|
}
|
|
|
|
function refresh() {
|
|
if (powerBalanceLogs.loadingInhibited) {
|
|
return;
|
|
}
|
|
|
|
|
|
var upcomingTimestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.config.count)
|
|
// print("refreshing config start", d.config.startTime(), "upcoming:", upcomingTimestamp, "fetchPending", d.fetchPending)
|
|
for (var i = 0; i < d.config.count; i++) {
|
|
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i + 1)
|
|
var previousTimestamp = root.calculateTimestamp(timestamp, d.config.sampleRate, -1)
|
|
var entry = powerBalanceLogs.find(timestamp)
|
|
var previousEntry = powerBalanceLogs.find(previousTimestamp);
|
|
var hasEntry = sampleMatches(entry, timestamp)
|
|
var hasPreviousEntry = sampleMatches(previousEntry, previousTimestamp)
|
|
if (timestamp < upcomingTimestamp && hasEntry && hasPreviousEntry) {
|
|
var consumption = entry.totalConsumption
|
|
var production = entry.totalProduction
|
|
var acquisition = entry.totalAcquisition
|
|
var returned = entry.totalReturn
|
|
consumption -= previousEntry.totalConsumption
|
|
production -= previousEntry.totalProduction
|
|
acquisition -= previousEntry.totalAcquisition
|
|
returned -= previousEntry.totalReturn
|
|
consumptionSet.replace(i, consumption)
|
|
productionSet.replace(i, production)
|
|
acquisitionSet.replace(i, acquisition)
|
|
returnSet.replace(i, returned)
|
|
valueAxis.adjustMax(consumption)
|
|
valueAxis.adjustMax(production)
|
|
valueAxis.adjustMax(acquisition)
|
|
valueAxis.adjustMax(returned)
|
|
} else if (timestamp.getTime() == upcomingTimestamp.getTime() && hasPreviousEntry) {
|
|
// print("it's today!")
|
|
var consumption = energyManager.totalConsumption
|
|
var production = energyManager.totalProduction
|
|
var acquisition = energyManager.totalAcquisition
|
|
var returned = energyManager.totalReturn
|
|
consumption -= previousEntry.totalConsumption
|
|
production -= previousEntry.totalProduction
|
|
acquisition -= previousEntry.totalAcquisition
|
|
returned -= previousEntry.totalReturn
|
|
consumptionSet.replace(i, consumption)
|
|
productionSet.replace(i, production)
|
|
acquisitionSet.replace(i, acquisition)
|
|
returnSet.replace(i, returned)
|
|
valueAxis.adjustMax(consumption)
|
|
valueAxis.adjustMax(production)
|
|
valueAxis.adjustMax(acquisition)
|
|
valueAxis.adjustMax(returned)
|
|
} else {
|
|
consumptionSet.replace(i, 0)
|
|
productionSet.replace(i, 0)
|
|
acquisitionSet.replace(i, 0)
|
|
returnSet.replace(i, 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
Label {
|
|
Layout.fillWidth: true
|
|
Layout.margins: Style.smallMargins
|
|
horizontalAlignment: Text.AlignHCenter
|
|
text: qsTr("Totals")
|
|
visible: root.titleVisible
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: pageStack.push(Qt.resolvedUrl("PowerBalanceStatsPage.qml"), {energyManager: root.energyManager, producers: root.producers})
|
|
}
|
|
}
|
|
|
|
SelectionTabs {
|
|
id: selectionTabs
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Style.smallMargins
|
|
Layout.rightMargin: Style.smallMargins
|
|
currentIndex: 1
|
|
model: ListModel {
|
|
ListElement { modelData: qsTr("Hours"); config: "hours" }
|
|
ListElement { modelData: qsTr("Days"); config: "days" }
|
|
ListElement { modelData: qsTr("Weeks"); config: "weeks" }
|
|
ListElement { modelData: qsTr("Months"); config: "months" }
|
|
ListElement { modelData: qsTr("Years"); config: "years" }
|
|
// ListElement { modelData: qsTr("Minutes"); config: "minutes" }
|
|
}
|
|
onTabSelected: {
|
|
d.startOffset = 0
|
|
powerBalanceLogs.fetchLogs()
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: energyManager
|
|
onPowerBalanceChanged: {
|
|
// print("updating because of power balance change. fetchingData", powerBalanceLogs.fetchingData, "fetchPending", d.fetchPending)
|
|
d.refresh();
|
|
}
|
|
}
|
|
|
|
PowerBalanceLogs {
|
|
id: powerBalanceLogs
|
|
engine: _engine
|
|
startTime: root.calculateTimestamp(d.startTime, d.config.sampleRate, -d.config.count)
|
|
endTime: root.calculateTimestamp(d.startTime, d.config.sampleRate, d.config.count)
|
|
sampleRate: d.config.sampleRate
|
|
Component.onCompleted: fetchLogs()
|
|
|
|
onFetchingDataChanged: {
|
|
if (!fetchingData) {
|
|
d.fetchPending = false
|
|
d.refresh()
|
|
}
|
|
}
|
|
|
|
onEntriesAdded: {
|
|
if (fetchingData) {
|
|
return
|
|
}
|
|
// Update the timeline by faking a left/right scroll
|
|
d.startOffset--
|
|
d.startOffset++
|
|
//d.refresh()
|
|
}
|
|
}
|
|
|
|
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: d.config.toRangeLabel(d.startTime)
|
|
font: Style.smallFont
|
|
opacity: d.startOffset < -d.config.count ? .5 : 0
|
|
Behavior on opacity { NumberAnimation {} }
|
|
}
|
|
|
|
ChartView {
|
|
id: chartView
|
|
animationOptions: ChartView.NoAnimation
|
|
anchors.fill: parent
|
|
|
|
backgroundColor: "transparent"
|
|
|
|
legend.visible: false
|
|
legend.alignment: Qt.AlignBottom
|
|
legend.font: Style.extraSmallFont
|
|
legend.labelColor: Style.foregroundColor
|
|
|
|
margins.left: Math.max(Style.smallMargins * 2, valueLabelMetrics.width + Style.smallMargins * 2)
|
|
margins.right: 0
|
|
margins.bottom: Style.smallIconSize + Style.margins
|
|
margins.top: 0
|
|
|
|
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
|
|
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.endTime) && d.startOffset != 0
|
|
font: Style.smallFont
|
|
opacity: .5
|
|
Behavior on opacity { NumberAnimation {}}
|
|
}
|
|
|
|
TextMetrics {
|
|
id: valueLabelMetrics
|
|
font: Style.extraSmallFont
|
|
text: (valueAxis.max).toFixed(1) + "kWh"
|
|
}
|
|
|
|
Item {
|
|
id: labelsLayout
|
|
x: Style.smallMargins
|
|
y: chartView.plotArea.y
|
|
height: chartView.plotArea.height
|
|
width: Math.max(0, chartView.margins.left - Style.smallMargins)
|
|
|
|
Repeater {
|
|
model: valueAxis.tickCount
|
|
delegate: Label {
|
|
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
|
width: parent.width - Style.smallMargins
|
|
horizontalAlignment: Text.AlignRight
|
|
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
|
|
verticalAlignment: Text.AlignTop
|
|
font: Style.extraSmallFont
|
|
}
|
|
}
|
|
}
|
|
|
|
BarSeries {
|
|
id: barSeries
|
|
axisX: BarCategoryAxis {
|
|
id: categoryAxis
|
|
labelsColor: Style.foregroundColor
|
|
labelsFont: Style.extraSmallFont
|
|
gridVisible: false
|
|
gridLineColor: Style.tileOverlayColor
|
|
lineVisible: false
|
|
titleVisible: false
|
|
shadesVisible: false
|
|
|
|
categories: {
|
|
var ret = []
|
|
// print("Updating categories from", d.config.startTime())
|
|
for (var i = 0; i < d.config.count; i++) {
|
|
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + i);
|
|
// print("*** adding", timestamp, d.startOffset, i)
|
|
ret.push(d.config.toLabel(timestamp))
|
|
}
|
|
return ret;
|
|
}
|
|
}
|
|
axisY: ValueAxis {
|
|
id: valueAxis
|
|
min: 0
|
|
gridLineColor: Style.tileOverlayColor
|
|
labelsVisible: false
|
|
labelsColor: Style.foregroundColor
|
|
labelsFont: Style.extraSmallFont
|
|
lineVisible: false
|
|
titleVisible: false
|
|
shadesVisible: false
|
|
|
|
function adjustMax(newValue) {
|
|
if (max < newValue) {
|
|
// print("adjusting to new max", newValue)
|
|
max = newValue // Math.ceil(newValue / 100) * 100
|
|
}
|
|
}
|
|
}
|
|
|
|
BarSet {
|
|
id: consumptionSet
|
|
label: qsTr("Consumed")
|
|
color: Qt.rgba(Style.powerConsumptionColor.r, Style.powerConsumptionColor.g, Style.powerConsumptionColor.b, opacity)
|
|
borderColor: color
|
|
borderWidth: 0
|
|
property real opacity: d.selectedSet == null || d.selectedSet == consumptionSet ? 1 : 0.3
|
|
values: {
|
|
var ret = []
|
|
for (var i = 0; i < d.config.count; i++) {
|
|
ret.push(0)
|
|
}
|
|
return ret
|
|
}
|
|
}
|
|
BarSet {
|
|
id: productionSet
|
|
label: qsTr("Produced")
|
|
color: Qt.rgba(Style.powerSelfProductionConsumptionColor.r, Style.powerSelfProductionConsumptionColor.g, Style.powerSelfProductionConsumptionColor.b, opacity)
|
|
borderColor: color
|
|
borderWidth: 0
|
|
property real opacity: d.selectedSet == null || d.selectedSet == productionSet ? 1 : 0.3
|
|
values: {
|
|
var ret = []
|
|
for (var i = 0; i < d.config.count; i++) {
|
|
ret.push(0)
|
|
}
|
|
return ret
|
|
}
|
|
}
|
|
BarSet {
|
|
id: acquisitionSet
|
|
label: qsTr("From grid")
|
|
color: Qt.rgba(Style.powerAcquisitionColor.r, Style.powerAcquisitionColor.g, Style.powerAcquisitionColor.b, opacity)
|
|
borderColor: color
|
|
borderWidth: 0
|
|
property real opacity: d.selectedSet == null || d.selectedSet == acquisitionSet ? 1 : 0.3
|
|
values: {
|
|
var ret = []
|
|
for (var i = 0; i < d.config.count; i++) {
|
|
ret.push(0)
|
|
}
|
|
return ret
|
|
}
|
|
}
|
|
BarSet {
|
|
id: returnSet
|
|
label: qsTr("To grid")
|
|
color: Qt.rgba(Style.powerReturnColor.r, Style.powerReturnColor.g, Style.powerReturnColor.b, opacity)
|
|
borderColor: color
|
|
borderWidth: 0
|
|
property real opacity: d.selectedSet == null || d.selectedSet == returnSet ? 1 : 0.3
|
|
values: {
|
|
var ret = []
|
|
for (var i = 0; i < d.config.count; i++) {
|
|
ret.push(0)
|
|
}
|
|
return ret
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
id: legend
|
|
anchors { left: parent.left; bottom: parent.bottom; right: parent.right }
|
|
anchors.leftMargin: chartView.plotArea.x
|
|
height: Style.smallIconSize
|
|
anchors.margins: Style.margins
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.topMargin: -Style.smallMargins
|
|
anchors.bottomMargin: -Style.smallMargins
|
|
onClicked: d.selectSet(consumptionSet)
|
|
}
|
|
|
|
Row {
|
|
anchors.centerIn: parent
|
|
spacing: Style.smallMargins
|
|
opacity: consumptionSet.opacity
|
|
ColorIcon {
|
|
name: root.homeIcon === "" ? "qrc:/icons/powersocket.svg": root.homeIcon
|
|
size: Style.smallIconSize
|
|
color: Style.powerConsumptionColor
|
|
}
|
|
Label {
|
|
width: parent.parent.width - x
|
|
elide: Text.ElideRight
|
|
visible: legend.width > 500
|
|
text: qsTr("Consumed")
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
font: Style.smallFont
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.topMargin: -Style.smallMargins
|
|
anchors.bottomMargin: -Style.smallMargins
|
|
onClicked: d.selectSet(productionSet)
|
|
}
|
|
Row {
|
|
anchors.centerIn: parent
|
|
spacing: Style.smallMargins
|
|
opacity: productionSet.opacity
|
|
ColorIcon {
|
|
name: root.pvIcon === "" ? "qrc:/icons/weathericons/weather-clear-day.svg" : root.pvIcon
|
|
size: Style.smallIconSize
|
|
color: Style.powerSelfProductionConsumptionColor
|
|
}
|
|
Label {
|
|
width: parent.parent.width - x
|
|
elide: Text.ElideRight
|
|
visible: legend.width > 500
|
|
text: qsTr("Produced")
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
font: Style.smallFont
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.topMargin: -Style.smallMargins
|
|
anchors.bottomMargin: -Style.smallMargins
|
|
onClicked: d.selectSet(acquisitionSet)
|
|
}
|
|
Row {
|
|
anchors.centerIn: parent
|
|
spacing: Style.smallMargins
|
|
opacity: acquisitionSet.opacity
|
|
Row {
|
|
ColorIcon {
|
|
name: root.gridIcon === "" ? "qrc:/icons/power-grid.svg" : root.gridIcon
|
|
size: Style.smallIconSize
|
|
color: Style.powerAcquisitionColor
|
|
}
|
|
ColorIcon {
|
|
name: "arrow-down"
|
|
size: Style.smallIconSize
|
|
color: Style.powerAcquisitionColor
|
|
}
|
|
}
|
|
Label {
|
|
width: parent.parent.width - x
|
|
elide: Text.ElideRight
|
|
visible: legend.width > 500
|
|
text: qsTr("From grid")
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
font: Style.smallFont
|
|
}
|
|
}
|
|
}
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
anchors.topMargin: -Style.smallMargins
|
|
anchors.bottomMargin: -Style.smallMargins
|
|
onClicked: d.selectSet(returnSet)
|
|
}
|
|
Row {
|
|
anchors.centerIn: parent
|
|
spacing: Style.smallMargins
|
|
opacity: returnSet.opacity
|
|
Row {
|
|
ColorIcon {
|
|
name: root.gridIcon === "" ? "qrc:/icons/power-grid.svg" : root.gridIcon
|
|
size: Style.smallIconSize
|
|
color: Style.powerReturnColor
|
|
}
|
|
ColorIcon {
|
|
name: "arrow-up"
|
|
size: Style.smallIconSize
|
|
color: Style.powerReturnColor
|
|
}
|
|
}
|
|
Label {
|
|
width: parent.parent.width - x
|
|
elide: Text.ElideRight
|
|
visible: legend.width > 500
|
|
text: qsTr("To grid")
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
font: Style.smallFont
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
|
anchors.topMargin: chartView.y + chartView.plotArea.y
|
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
z: -1
|
|
|
|
Rectangle {
|
|
height: parent.height + Style.margins * 2
|
|
y: -Style.smallMargins
|
|
radius: Style.smallCornerRadius
|
|
width: chartView.plotArea.width / categoryAxis.count
|
|
color: Style.tileBackgroundColor
|
|
property int idx: Math.min(Math.max(0,Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)), categoryAxis.count - 1)
|
|
visible: toolTip.visible
|
|
|
|
x: idx * parent.width / categoryAxis.count
|
|
Behavior on x { enabled: toolTip.animationsEnabled; NumberAnimation { duration: Style.animationDuration } }
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouseArea
|
|
anchors.fill: parent
|
|
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
|
anchors.topMargin: chartView.y + chartView.plotArea.y
|
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
|
|
hoverEnabled: true
|
|
preventStealing: tooltipping || dragging
|
|
|
|
property int startMouseX: 0
|
|
property bool dragging: false
|
|
property bool tooltipping: false
|
|
property int dragStartOffset: 0
|
|
|
|
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
|
|
dragStartOffset = d.startOffset
|
|
}
|
|
|
|
onDoubleClicked: {
|
|
var idx = Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1
|
|
var timestamp = root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + idx)
|
|
selectionTabs.currentIndex--
|
|
var startTime = d.config.startTime()
|
|
d.startOffset = (timestamp.getTime() - startTime.getTime()) / (d.config.sampleRate * 60 * 1000)
|
|
powerBalanceLogs.fetchLogs();
|
|
}
|
|
|
|
onMouseXChanged: {
|
|
if (!pressed || mouseArea.tooltipping) {
|
|
return;
|
|
}
|
|
if (Math.abs(startMouseX - mouseX) < 10) {
|
|
return;
|
|
}
|
|
dragging = true
|
|
|
|
var dragDelta = startMouseX - mouseX
|
|
var slotWidth = mouseArea.width / d.config.count
|
|
var offset = Math.floor(dragDelta / slotWidth);
|
|
d.startOffset = Math.min(dragStartOffset + offset, 0)
|
|
d.fetchPending = true;
|
|
}
|
|
|
|
property int wheelDelta: 0
|
|
onWheel: (wheel) => {
|
|
wheelDelta += wheel.pixelDelta.x
|
|
var slotWidth = mouseArea.width / d.config.count
|
|
while (wheelDelta > slotWidth) {
|
|
d.startOffset--
|
|
wheelDelta -= slotWidth
|
|
}
|
|
while (wheelDelta < -slotWidth) {
|
|
d.startOffset = Math.min(d.startOffset + 1, 0)
|
|
wheelDelta += slotWidth
|
|
}
|
|
d.fetchPending = true;
|
|
wheelStopTimer.restart()
|
|
}
|
|
|
|
Timer {
|
|
id: wheelStopTimer
|
|
interval: 300
|
|
repeat: false
|
|
onTriggered: powerBalanceLogs.fetchLogs()
|
|
}
|
|
|
|
NymeaToolTip {
|
|
id: toolTip
|
|
|
|
backgroundItem: chartView
|
|
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
|
|
|
|
property int idx: visible ? Math.min(d.config.count -1, Math.max(0, Math.ceil(mouseArea.mouseX * d.config.count / mouseArea.width) - 1)) : 0
|
|
property date timestamp: root.calculateTimestamp(d.config.startTime(), d.config.sampleRate, d.startOffset + idx)
|
|
|
|
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
|
|
|
property int chartWidth: chartView.plotArea.width
|
|
property int barWidth: chartWidth / categoryAxis.count
|
|
|
|
x: chartWidth - (idx * barWidth + barWidth + Style.smallMargins) > width ?
|
|
idx * barWidth + barWidth + Style.smallMargins
|
|
: idx * barWidth - Style.smallMargins - width
|
|
property double setMaxValue: d.startOffset !== undefined ? Math.max(consumptionSet.at(idx),
|
|
productionSet.at(idx),
|
|
acquisitionSet.at(idx),
|
|
returnSet.at(idx)) : 0
|
|
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
|
|
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
|
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
|
|
|
ColumnLayout {
|
|
id: tooltipLayout
|
|
anchors {
|
|
left: parent.left
|
|
top: parent.top
|
|
margins: Style.smallMargins
|
|
}
|
|
|
|
Label {
|
|
text: d.config.toLongLabel(toolTip.timestamp)
|
|
font: Style.smallFont
|
|
}
|
|
|
|
RowLayout {
|
|
visible: root.hasProducers
|
|
Rectangle {
|
|
width: Style.extraSmallFont.pixelSize
|
|
height: width
|
|
color: Style.powerConsumptionColor
|
|
}
|
|
Label {
|
|
text: d.startOffset !== undefined ? qsTr("Consumed: %1 kWh").arg(consumptionSet.at(toolTip.idx).toFixed(2)) : ""
|
|
font: Style.extraSmallFont
|
|
}
|
|
}
|
|
RowLayout {
|
|
visible: root.hasProducers
|
|
Rectangle {
|
|
width: Style.extraSmallFont.pixelSize
|
|
height: width
|
|
color: Style.powerSelfProductionConsumptionColor
|
|
}
|
|
Label {
|
|
text: d.startOffset !== undefined ? qsTr("Produced: %1 kWh").arg(productionSet.at(toolTip.idx).toFixed(2)) : ""
|
|
font: Style.extraSmallFont
|
|
}
|
|
}
|
|
RowLayout {
|
|
Rectangle {
|
|
width: Style.extraSmallFont.pixelSize
|
|
height: width
|
|
color: Style.powerAcquisitionColor
|
|
}
|
|
Label {
|
|
text: d.startOffset !== undefined ? qsTr("From grid: %1 kWh").arg(acquisitionSet.at(toolTip.idx).toFixed(2)) :""
|
|
font: Style.extraSmallFont
|
|
}
|
|
}
|
|
RowLayout {
|
|
Rectangle {
|
|
width: Style.extraSmallFont.pixelSize
|
|
height: width
|
|
color: Style.powerReturnColor
|
|
}
|
|
Label {
|
|
text: d.startOffset !== undefined ? qsTr("To grid: %1 kWh").arg(returnSet.at(toolTip.idx).toFixed(2)) : ""
|
|
font: Style.extraSmallFont
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|