459 lines
18 KiB
QML
459 lines
18 KiB
QML
import QtQuick 2.3
|
|
import QtQuick.Layouts 1.2
|
|
import QtQuick.Controls 2.3
|
|
import QtCharts 2.3
|
|
import Nymea 1.0
|
|
import "qrc:/ui/components/"
|
|
|
|
StatsBase {
|
|
id: root
|
|
|
|
property EnergyManager energyManager: null
|
|
property var colors: null
|
|
|
|
property ThingsProxy consumers: null
|
|
|
|
Connections {
|
|
target: consumers
|
|
onCountChanged: root.update()
|
|
}
|
|
|
|
Connections {
|
|
target: engine.thingManager
|
|
onFetchingDataChanged: root.update()
|
|
}
|
|
Connections {
|
|
target: engine.tagsManager
|
|
onBusyChanged: root.update()
|
|
}
|
|
|
|
function update() {
|
|
if (engine.thingManager.fetchingData || engine.tagsManager.busy) {
|
|
return
|
|
}
|
|
powerLogs.loadingInhibited = true
|
|
|
|
var thingIds = []
|
|
for (var i = 0; i < consumers.count; i++) {
|
|
thingIds.push(consumers.get(i).id)
|
|
}
|
|
powerLogs.thingIds = thingIds
|
|
|
|
var config = root.configs[selectionTabs.currentValue.config]
|
|
// print("config:", config.startTime(), config.sampleList(), config.sampleListNames())
|
|
|
|
powerLogs.sampleRate = config.sampleRate
|
|
powerLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000)
|
|
|
|
chartView.reset();
|
|
|
|
powerLogs.loadingInhibited = false
|
|
}
|
|
|
|
ThingPowerLogs {
|
|
id: powerLogs
|
|
engine: _engine
|
|
loadingInhibited: true
|
|
|
|
property var sampleList: null
|
|
|
|
onFetchingDataChanged: {
|
|
if (!fetchingData) {
|
|
var config = root.configs[selectionTabs.currentValue.config]
|
|
|
|
chartView.reset()
|
|
|
|
// First grouping log entries by timestamp
|
|
var groupedEntries = []
|
|
var groupedEntry = {}
|
|
for (var i = powerLogs.count - 1; i >= 0; i--) {
|
|
var entry = powerLogs.get(i);
|
|
// print("grouping entry:", entry.timestamp, "current group entry", groupedEntry.timestamp, groupedEntry.hasOwnProperty("timestamp"))
|
|
if (!groupedEntry.hasOwnProperty("timestamp")) {
|
|
groupedEntry.timestamp = entry.timestamp;
|
|
// print("Starting new groupentry", groupedEntry.timestamp, entry.timestamp)
|
|
}
|
|
if (groupedEntry.timestamp.getTime() !== entry.timestamp.getTime()) {
|
|
if (groupedEntries.length > config.count) {
|
|
break;
|
|
}
|
|
// print("finalizing grouped entry", groupedEntry.timestamp)
|
|
groupedEntries.unshift(groupedEntry);
|
|
groupedEntry = {
|
|
timestamp: entry.timestamp
|
|
}
|
|
// print("Starting new groupentry", groupedEntry.timestamp, entry.timestamp)
|
|
}
|
|
groupedEntry[entry.thingId] = entry.totalConsumption
|
|
}
|
|
if (groupedEntry.hasOwnProperty("timestamp") && groupedEntries.length <= config.count) {
|
|
// print("finalizing grouped entry", groupedEntry.timestamp)
|
|
groupedEntries.unshift(groupedEntry)
|
|
}
|
|
|
|
var labels = []
|
|
var entries = []
|
|
|
|
var newestLogTimestamp = powerLogs.count > 0 ? powerLogs.get(powerLogs.count - 1).timestamp : new Date();
|
|
|
|
for (var i = 0; i < config.count; i++) {
|
|
var groupedEntry = groupedEntries[groupedEntries.length - i - 1]
|
|
// print("have grouped entry:", groupedEntry ? groupedEntry.timestamp : "null")
|
|
|
|
// if it's the first, let's add a generated entry which shows the total from the newest log to the current live value
|
|
if (i == 0) {
|
|
var liveEntry = {}
|
|
for (var j = 0; j < consumers.count; j++) {
|
|
var consumer = consumers.get(j)
|
|
var liveLogEntry = powerLogs.liveEntry(consumer.id)
|
|
// print("Got consumer:", consumer.id, consumer.name, liveLogEntry ? liveLogEntry.timestamp : "-")
|
|
var value = liveLogEntry ? liveLogEntry.totalConsumption : 0;
|
|
if (groupedEntry) {
|
|
value -= groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0
|
|
}
|
|
liveEntry[consumer.id] = value
|
|
valueAxis.adjustMax(value)
|
|
}
|
|
|
|
// print("Adding live entry", JSON.stringify(liveEntry))
|
|
entries.unshift(liveEntry)
|
|
}
|
|
|
|
// Add the actual entry
|
|
var graphEntry = {}
|
|
var labelTime = new Date();
|
|
|
|
if (groupedEntry) {
|
|
var previousGroupedEntry = groupedEntries[groupedEntries.length - i - 2]
|
|
for (var j = 0; j < consumers.count; j++) {
|
|
var consumer = consumers.get(j)
|
|
var value = groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0
|
|
if (previousGroupedEntry) {
|
|
var previousValue = previousGroupedEntry.hasOwnProperty(consumer.id) ? previousGroupedEntry[consumer.id] : 0
|
|
value -= previousValue
|
|
}
|
|
graphEntry[consumer.id] = value
|
|
valueAxis.adjustMax(value)
|
|
}
|
|
labelTime = groupedEntry.timestamp
|
|
} else {
|
|
for (var j = 0; j < consumers.count; j++) {
|
|
var consumer = consumers.get(j)
|
|
graphEntry[consumer.id] = 0
|
|
}
|
|
labelTime = new Date(newestLogTimestamp.getTime() - config.sampleRate * i * 60000)
|
|
}
|
|
|
|
// print("Adding entry:", labelTime, config.toLabel(labelTime), JSON.stringify(graphEntry))
|
|
entries.unshift(graphEntry)
|
|
labels.unshift(labelTime)
|
|
|
|
// Given we've added 2 entries for the first run but only one label, we'll add the missing label
|
|
// at the end. This will shift the labels by one entries but that's ok because the logs timestamp
|
|
// is when the sample was created, but for the user it's better to show the the consumption values
|
|
// *during* that sample, not *before* the sample
|
|
if (i == config.count - 1) {
|
|
labelTime = new Date(labelTime.getTime() - config.sampleRate * 60000)
|
|
// print("Adding oldest entry label", labelTime, config.sampleRate, config.toLabel(labelTime))
|
|
labels.unshift(labelTime)
|
|
}
|
|
|
|
}
|
|
|
|
// print("assigning categories:", labels)
|
|
categoryAxis.timestamps = labels
|
|
|
|
for (var i = 0; i < entries.length; i++) {
|
|
var entry = entries[i]
|
|
// print("Adding entry", JSON.stringify(entry))
|
|
for (var j = 0; j < consumers.count; j++) {
|
|
var consumer = consumers.get(j)
|
|
barSeries.thingBarSetMap[consumer.id].append(entry[consumer.id])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onEntriesAdded: {
|
|
if (fetchingData) {
|
|
return
|
|
}
|
|
|
|
chartView.animationOptions = ChartView.NoAnimation
|
|
|
|
for (var i = 0; i < entries.length; i++) {
|
|
var entry = entries[i]
|
|
var thing = engine.thingManager.things.getThing(entry.thingId)
|
|
// print("Adding new sample. thing:", thing.name);
|
|
// print("Timestamp:", entry.timestamp, entry.totalConsumption)
|
|
// update current last
|
|
var barSet = barSeries.thingBarSetMap[thing.id]
|
|
var lastTimestamp = categoryAxis.timestamps[categoryAxis.count - 1]
|
|
var previous = powerLogs.find(entry.thingId, lastTimestamp)
|
|
var previousValue = previous ? previous.totalConsumption : 0
|
|
// print("previousValue:", previousValue, "newValue:", entry.totalConsumption, "diff", entry.totalConsumption - previousValue)
|
|
barSet.replace(barSet.count - 1, entry.totalConsumption - previousValue)
|
|
|
|
// remove the oldest
|
|
barSet.remove(0, 1)
|
|
|
|
// and add a new one (always 0 for a start)
|
|
barSet.append(0)
|
|
}
|
|
|
|
var labels = categoryAxis.timestamps
|
|
labels.splice(0, 1)
|
|
labels.push(entries[0].timestamp)
|
|
categoryAxis.timestamps = labels
|
|
|
|
chartView.animationOptions = NymeaUtils.chartsAnimationOptions
|
|
}
|
|
|
|
onLiveEntryChanged: {
|
|
if (powerLogs.fetchingData) {
|
|
return
|
|
}
|
|
|
|
// print("live entry changed", entry.thingId, entry.timestamp)
|
|
var previous = powerLogs.find(entry.thingId, new Date(categoryAxis.timestamps[categoryAxis.timestamps.length - 1]))
|
|
var previousValue = previous ? previous.totalConsumption : 0
|
|
var barSet = barSeries.thingBarSetMap[entry.thingId]
|
|
|
|
if (!barSet) {
|
|
return
|
|
}
|
|
|
|
barSet.replace(barSet.count - 1, entry.totalConsumption - previousValue)
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
|
|
Label {
|
|
Layout.fillWidth: true
|
|
Layout.margins: Style.smallMargins
|
|
horizontalAlignment: Text.AlignHCenter
|
|
text: qsTr("Consumers totals")
|
|
|
|
}
|
|
|
|
SelectionTabs {
|
|
id: selectionTabs
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Style.smallMargins
|
|
Layout.rightMargin: Style.smallMargins
|
|
currentIndex: 0
|
|
model: ListModel {
|
|
Component.onCompleted: {
|
|
append({modelData: qsTr("Hours"), config: "hours" })
|
|
append({modelData: qsTr("Days"), config: "days" })
|
|
append({modelData: qsTr("Weeks"), config: "weeks" })
|
|
append({modelData: qsTr("Months"), config: "months" })
|
|
append({modelData: qsTr("Years"), config: "years" })
|
|
// append({modelData: qsTr("Minutes"), config: "minutes" })
|
|
|
|
selectionTabs.currentIndex = 1
|
|
}
|
|
}
|
|
onCurrentValueChanged: {
|
|
root.update()
|
|
}
|
|
}
|
|
|
|
|
|
ChartView {
|
|
id: chartView
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
// margins.left: 0
|
|
margins.right: 0
|
|
margins.bottom: 0
|
|
margins.top: 0
|
|
|
|
backgroundColor: "transparent"
|
|
legend.alignment: Qt.AlignBottom
|
|
legend.font: Style.extraSmallFont
|
|
legend.labelColor: Style.foregroundColor
|
|
|
|
function reset() {
|
|
chartView.animationOptions = ChartView.NoAnimation
|
|
barSeries.clear();
|
|
valueAxis.max = 0
|
|
var map = {}
|
|
for (var j = 0; j < consumers.count; j++) {
|
|
var consumer = consumers.get(j)
|
|
var barSet = barSeries.append(consumer.name, [])
|
|
barSet.color = root.colors[j % root.colors.length]
|
|
barSet.borderColor = barSet.color
|
|
barSet.borderWith = 0
|
|
map[consumer.id] = barSet
|
|
}
|
|
barSeries.thingBarSetMap = map
|
|
chartView.animationOptions = NymeaUtils.chartsAnimationOptions
|
|
}
|
|
|
|
Item {
|
|
id: labelsLayout
|
|
x: Style.smallMargins
|
|
y: chartView.plotArea.y
|
|
height: chartView.plotArea.height
|
|
width: chartView.plotArea.x - x
|
|
Repeater {
|
|
model: valueAxis.tickCount
|
|
delegate: Label {
|
|
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
|
width: parent.width - Style.smallMargins
|
|
horizontalAlignment: Text.AlignRight
|
|
text: ((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))).toFixed(1) + "kWh"
|
|
verticalAlignment: Text.AlignTop
|
|
font: Style.extraSmallFont
|
|
color: Style.foregroundColor
|
|
}
|
|
}
|
|
}
|
|
|
|
BarSeries {
|
|
id: barSeries
|
|
axisX: BarCategoryAxis {
|
|
id: categoryAxis
|
|
labelsColor: Style.foregroundColor
|
|
labelsFont: Style.extraSmallFont
|
|
gridVisible: false
|
|
gridLineColor: Style.tileOverlayColor
|
|
lineVisible: false
|
|
titleVisible: false
|
|
shadesVisible: false
|
|
|
|
categories: {
|
|
var ret = []
|
|
for (var i = 0; i < timestamps.length; i++) {
|
|
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i]))
|
|
}
|
|
return ret
|
|
}
|
|
|
|
property var timestamps: []
|
|
}
|
|
axisY: ValueAxis {
|
|
id: valueAxis
|
|
min: 0
|
|
gridLineColor: Style.tileOverlayColor
|
|
labelsVisible: false
|
|
labelsColor: Style.foregroundColor
|
|
labelsFont: Style.extraSmallFont
|
|
lineVisible: false
|
|
titleVisible: false
|
|
shadesVisible: false
|
|
|
|
function adjustMax(newValue) {
|
|
if (max < newValue) {
|
|
max = Math.ceil(newValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
property var thingBarSetMap: ({})
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouseArea
|
|
anchors.fill: parent
|
|
anchors.leftMargin: chartView.x + chartView.plotArea.x
|
|
anchors.topMargin: chartView.y + chartView.plotArea.y
|
|
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
|
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
|
|
|
hoverEnabled: true
|
|
|
|
Timer {
|
|
interval: 300
|
|
running: mouseArea.pressed
|
|
onTriggered: mouseArea.preventStealing = true
|
|
}
|
|
onReleased: mouseArea.preventStealing = false
|
|
|
|
NymeaToolTip {
|
|
id: toolTip
|
|
|
|
backgroundItem: chartView
|
|
backgroundRect: Qt.rect(chartView.plotArea.x + toolTip.x, chartView.plotArea.y + toolTip.y, toolTip.width, toolTip.height)
|
|
|
|
property int idx: Math.max(0, Math.min(categoryAxis.count -1, Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)))
|
|
visible: mouseArea.containsMouse || mouseArea.preventStealing
|
|
|
|
x: Math.min(idx * mouseArea.width / categoryAxis.count, mouseArea.width - width)
|
|
property double setMaxValue: {
|
|
var max = 0;
|
|
for (var i = 0; i < consumers.count; i++) {
|
|
var consumer = consumers.get(i)
|
|
max = barSeries.thingBarSetMap.hasOwnProperty(consumer.id) ? Math.max(max, barSeries.thingBarSetMap[consumer.id].at(idx)) : 0
|
|
}
|
|
return max
|
|
}
|
|
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
|
|
|
|
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
|
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
|
|
|
ColumnLayout {
|
|
id: tooltipLayout
|
|
anchors {
|
|
left: parent.left
|
|
top: parent.top
|
|
margins: Style.smallMargins
|
|
}
|
|
Label {
|
|
text: toolTip.idx >= 0 && categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : ""
|
|
font: Style.smallFont
|
|
}
|
|
|
|
Repeater {
|
|
model: ListModel {
|
|
id: toolTipModel
|
|
property var entries: {
|
|
var unsorted = []
|
|
for (var i = 0; i < consumers.count; i++) {
|
|
var consumer = consumers.get(i)
|
|
var entry = {
|
|
name: consumer.name,
|
|
value: barSeries.thingBarSetMap[consumer.id].at(toolTip.idx).toFixed(2),
|
|
indexInModel: i
|
|
}
|
|
unsorted.push(entry)
|
|
}
|
|
return unsorted
|
|
}
|
|
onEntriesChanged: {
|
|
clear();
|
|
var unsorted = entries;
|
|
for (var i = 0; i < unsorted.length; i++) {
|
|
var j = 0;
|
|
while (j < count && get(j).value > unsorted[i].value) {
|
|
j++;
|
|
}
|
|
insert(j, unsorted[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
delegate: RowLayout {
|
|
Rectangle {
|
|
width: Style.extraSmallFont.pixelSize
|
|
height: width
|
|
color: root.colors[model.indexInModel % root.colors.length]
|
|
}
|
|
Label {
|
|
text: "%1: %2 kWh".arg(model.name).arg(model.value)
|
|
font: Style.extraSmallFont
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|