Improvements in the energy view
This commit is contained in:
parent
da9e5d8d7f
commit
6d049101f4
@ -183,9 +183,9 @@ void EnergyLogs::appendEntry(EnergyLogEntry *entry)
|
||||
beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
|
||||
m_list.append(entry);
|
||||
endInsertRows();
|
||||
emit countChanged();
|
||||
emit entryAdded(entry);
|
||||
emit entriesAdded({entry});
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void EnergyLogs::appendEntries(const QList<EnergyLogEntry *> &entries)
|
||||
|
||||
@ -81,8 +81,6 @@ QString PowerBalanceLogs::logsName() const
|
||||
|
||||
void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry)
|
||||
{
|
||||
appendEntry(entry);
|
||||
|
||||
if (entry->consumption() < m_minValue) {
|
||||
m_minValue = entry->consumption();
|
||||
emit minValueChanged();
|
||||
@ -117,11 +115,12 @@ void PowerBalanceLogs::addEntry(PowerBalanceLogEntry *entry)
|
||||
emit maxValueChanged();
|
||||
}
|
||||
|
||||
appendEntry(entry);
|
||||
}
|
||||
|
||||
EnergyLogEntry *PowerBalanceLogs::find(const QDateTime ×tamp) const
|
||||
{
|
||||
// qWarning() << "Finding log entry for timestamp:" << timestamp;
|
||||
qWarning() << "Finding log entry for timestamp:" << timestamp;
|
||||
int oldest = 0;
|
||||
int newest = rowCount() - 1;
|
||||
EnergyLogEntry *entry = nullptr;
|
||||
@ -131,7 +130,7 @@ EnergyLogEntry *PowerBalanceLogs::find(const QDateTime ×tamp) const
|
||||
EnergyLogEntry *newestEntry = get(newest);
|
||||
int middle = (newest - oldest) / 2 + oldest;
|
||||
EnergyLogEntry *middleEntry = get(middle);
|
||||
// qWarning() << "Oldest:" << oldestEntry->timestamp().toString() << "Middle:" << middleEntry->timestamp().toString() << "Newest:" << newestEntry->timestamp().toString() << ":" << (newest - oldest);
|
||||
qWarning() << "Oldest:" << oldestEntry->timestamp().toString() << "Middle:" << middleEntry->timestamp().toString() << "Newest:" << newestEntry->timestamp().toString() << ":" << (newest - oldest);
|
||||
if (timestamp <= oldestEntry->timestamp()) {
|
||||
return oldestEntry;
|
||||
}
|
||||
|
||||
@ -180,7 +180,7 @@ void BarSeriesAdapter::logEntryAdded(LogEntry *entry)
|
||||
|
||||
m_timeslots[slotIdx].entries.append(entry);
|
||||
m_set->replace(slotIdx, m_timeslots[slotIdx].value());
|
||||
qDebug() << "Adding entry" << entry->timestamp() << "timestlot" << timeSlotStart << "at" << slotIdx << "value" << m_timeslots[slotIdx].value();
|
||||
// qDebug() << "Adding entry" << entry->timestamp() << "timestlot" << timeSlotStart << "at" << slotIdx << "value" << m_timeslots[slotIdx].value();
|
||||
|
||||
// if (!m_timeslots.contains(timeSlotStart)) {
|
||||
// TimeSlot timeslot;
|
||||
|
||||
@ -85,8 +85,6 @@ void ThingsProxy::setParentProxy(ThingsProxy *parentProxy)
|
||||
if (!m_engine) {
|
||||
return;
|
||||
}
|
||||
setSortRole(Things::RoleName);
|
||||
sort(0);
|
||||
connect(m_parentProxy, SIGNAL(countChanged()), this, SIGNAL(countChanged()));
|
||||
connect(m_parentProxy, &QAbstractItemModel::dataChanged, this, [this]() {
|
||||
if (m_engine) {
|
||||
|
||||
@ -271,5 +271,6 @@
|
||||
<file>ui/system/PackageListPage.qml</file>
|
||||
<file>ui/mainviews/energy/StatsBase.qml</file>
|
||||
<file>ui/mainviews/energy/EnergySettingsPage.qml</file>
|
||||
<file>ui/mainviews/energy/ConsumersPieChart.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -95,7 +95,7 @@ MainViewBase {
|
||||
anchors.fill: parent
|
||||
anchors.margins: app.margins / 2
|
||||
contentHeight: energyGrid.childrenRect.height
|
||||
visible: energyMeters.count > 0
|
||||
visible: !engine.thingManager.fetchingData && engine.jsonRpcClient.experiences.hasOwnProperty("Energy")
|
||||
topMargin: root.topMargin
|
||||
|
||||
// GridLayout directly in a flickable causes problems at initialisation
|
||||
@ -117,6 +117,7 @@ MainViewBase {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
energyManager: energyManager
|
||||
visible: producers.count > 0
|
||||
}
|
||||
CurrentProductionBalancePieChart {
|
||||
Layout.fillWidth: true
|
||||
@ -128,6 +129,7 @@ MainViewBase {
|
||||
PowerConsumptionBalanceHistory {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
visible: producers.count > 0
|
||||
}
|
||||
|
||||
PowerProductionBalanceHistory {
|
||||
@ -136,7 +138,7 @@ MainViewBase {
|
||||
visible: producers.count > 0
|
||||
}
|
||||
|
||||
ConsumersBarChart {
|
||||
ConsumersPieChart {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
energyManager: energyManager
|
||||
@ -144,6 +146,15 @@ MainViewBase {
|
||||
colors: root.thingColors
|
||||
consumers: consumers
|
||||
}
|
||||
|
||||
// ConsumersBarChart {
|
||||
// Layout.fillWidth: true
|
||||
// Layout.preferredHeight: width
|
||||
// energyManager: energyManager
|
||||
// visible: consumers.count > 0
|
||||
// colors: root.thingColors
|
||||
// consumers: consumers
|
||||
// }
|
||||
ConsumersHistory {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
@ -157,6 +168,7 @@ MainViewBase {
|
||||
Layout.preferredHeight: width
|
||||
energyManager: energyManager
|
||||
}
|
||||
|
||||
ConsumerStats {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: width
|
||||
@ -172,12 +184,18 @@ MainViewBase {
|
||||
EmptyViewPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - app.margins * 2
|
||||
visible: !engine.jsonRpcClient.experiences.hasOwnProperty("Energy")
|
||||
visible: !engine.thingManager.fetchingData && !engine.jsonRpcClient.experiences.hasOwnProperty("Energy")
|
||||
title: qsTr("Energy plugin not installed installed.")
|
||||
text: qsTr("This %1 system does not have the energy extensions installed.").arg(Configuration.systemName)
|
||||
imageSource: "../images/smartmeter.svg"
|
||||
buttonText: qsTr("Install energy plugin")
|
||||
buttonVisible: packagesFilterModel.count > 0
|
||||
onButtonClicked: pageStack.push(Qt.resolvedUrl("../system/PackageListPage.qml"), {filter: "nymea-experience-plugin-energy"})
|
||||
PackagesFilterModel {
|
||||
id: packagesFilterModel
|
||||
packages: engine.systemController.packages
|
||||
nameFilter: "nymea-experience-plugin-energy"
|
||||
}
|
||||
}
|
||||
EmptyViewPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
|
||||
@ -43,14 +43,12 @@ StatsBase {
|
||||
// print("config:", config.startTime(), config.sampleList(), config.sampleListNames())
|
||||
|
||||
powerLogs.sampleRate = config.sampleRate
|
||||
powerLogs.startTime = config.startTime()
|
||||
powerLogs.sampleList = config.sampleList()
|
||||
powerLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000)
|
||||
|
||||
barSeries.clear();
|
||||
barSeries.thingBarSetMap = ({})
|
||||
|
||||
valueAxis.max = 0
|
||||
categoryAxis.categories = config.sampleListNames()
|
||||
|
||||
chartView.animationOptions = ChartView.SeriesAnimations
|
||||
|
||||
@ -66,45 +64,132 @@ StatsBase {
|
||||
|
||||
onFetchingDataChanged: {
|
||||
if (!fetchingData) {
|
||||
barSeries.clear();
|
||||
for (var j = 0; j < consumers.count; j++) {
|
||||
// Note: Needs to be let, not var so the lambda capture below copies it instead of capturing the reference
|
||||
let consumer = consumers.get(j)
|
||||
// print("ConsumerStats: Adding thing:", consumer.name)
|
||||
let totalEnergyConsumedState = consumer.stateByName("totalEnergyConsumed")
|
||||
// print("Adding consumer:", consumer.name, consumer.id)
|
||||
var consumptionValues = []
|
||||
for (var i = 0; i < sampleList.length; i++) {
|
||||
var start = powerLogs.find(consumer.id, new Date(sampleList[i]))
|
||||
var startValue = start !== null ? start.totalConsumption : 0
|
||||
var end = i < sampleList.length -1 ? powerLogs.find(consumer.id, new Date(sampleList[i+1])) : null
|
||||
var endValue = end !== null ? end.totalConsumption : 0
|
||||
if (i == sampleList.length - 1) {
|
||||
endValue = totalEnergyConsumedState.value
|
||||
var config = root.configs[selectionTabs.currentValue.config]
|
||||
|
||||
// First grouping log entries by timestamp
|
||||
var groupedEntries = []
|
||||
var groupedEntry = {}
|
||||
for (var i = powerLogs.count - 1; i >= 0; i--) {
|
||||
var entry = powerLogs.get(i);
|
||||
// print("grouping entry:", entry.timestamp, "current group entry", groupedEntry.timestamp, groupedEntry.hasOwnProperty("timestamp"))
|
||||
if (!groupedEntry.hasOwnProperty("timestamp")) {
|
||||
groupedEntry.timestamp = entry.timestamp;
|
||||
// print("Starting new groupentry", groupedEntry.timestamp, entry.timestamp)
|
||||
}
|
||||
if (groupedEntry.timestamp.getTime() !== entry.timestamp.getTime()) {
|
||||
if (groupedEntries.length > config.count) {
|
||||
break;
|
||||
}
|
||||
// print("finalizing grouped entry", groupedEntry.timestamp)
|
||||
groupedEntries.unshift(groupedEntry);
|
||||
groupedEntry = {
|
||||
timestamp: entry.timestamp
|
||||
}
|
||||
// print("Starting new groupentry", groupedEntry.timestamp, entry.timestamp)
|
||||
}
|
||||
groupedEntry[entry.thingId] = entry.totalConsumption
|
||||
}
|
||||
if (groupedEntry.hasOwnProperty("timestamp") && groupedEntries.length <= config.count) {
|
||||
// print("finalizing grouped entry", groupedEntry.timestamp)
|
||||
groupedEntries.unshift(groupedEntry)
|
||||
}
|
||||
|
||||
|
||||
|
||||
chartView.animationOptions = ChartView.NoAnimation
|
||||
|
||||
|
||||
var labels = []
|
||||
var entries = []
|
||||
|
||||
var newestLogTimestamp = powerLogs.count > 0 ? powerLogs.get(powerLogs.count - 1).timestamp : new Date();
|
||||
|
||||
for (var i = 0; i < config.count; i++) {
|
||||
var groupedEntry = groupedEntries[groupedEntries.length - i - 1]
|
||||
// print("have grouped entry:", groupedEntry ? groupedEntry.timestamp : "null")
|
||||
|
||||
// if it's the first, let's add a generated entry which shows the total from the newest log to the current live value
|
||||
if (i == 0) {
|
||||
var liveEntry = {}
|
||||
for (var j = 0; j < consumers.count; j++) {
|
||||
var consumer = consumers.get(j)
|
||||
// print("Got consumer:", consumer.id, consumer.name)
|
||||
var value = consumer.stateByName("totalEnergyConsumed").value;
|
||||
if (groupedEntry) {
|
||||
value -= groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0
|
||||
}
|
||||
liveEntry[consumer.id] = value
|
||||
valueAxis.adjustMax(value)
|
||||
}
|
||||
|
||||
// print("adding sample", new Date(sampleList[i]), start ? start.timestamp : "X", " - ", end ? end.timestamp : "X")
|
||||
// print("values. start:", startValue, "end", endValue, "diff", endValue - startValue)
|
||||
var consumptionValue = endValue - startValue
|
||||
// print("Value", consumptionValue)
|
||||
consumptionValues.push(consumptionValue)
|
||||
valueAxis.adjustMax(consumptionValue)
|
||||
// print("Adding live entry", JSON.stringify(liveEntry))
|
||||
entries.unshift(liveEntry)
|
||||
}
|
||||
let barSet = barSeries.append(consumer.name, consumptionValues)
|
||||
barSet.color = root.colors[j % root.colors.length]
|
||||
barSet.borderWidth = 0
|
||||
barSet.borderColor = barSet.color
|
||||
barSeries.thingBarSetMap[consumer] = barSet
|
||||
|
||||
totalEnergyConsumedState.onValueChanged.connect(function() {
|
||||
var sampleList = root.configs[selectionTabs.currentValue.config].sampleList()
|
||||
var lastSample = sampleList[sampleList.length - 1]
|
||||
// print("sampleList:", powerLogs.sampleList)
|
||||
var start = powerLogs.find(consumer.id, new Date(lastSample))
|
||||
// print("consumer value changed:", consumer.name, totalEnergyConsumedState.value, start.timestamp, start.totalConsumption)
|
||||
var barSet = barSeries.thingBarSetMap[consumer]
|
||||
barSet.replace(barSet.count - 1, totalEnergyConsumedState.value - start.totalConsumption)
|
||||
})
|
||||
// Add the actual entry
|
||||
var graphEntry = {}
|
||||
var labelTime = new Date();
|
||||
|
||||
if (groupedEntry) {
|
||||
var previousGroupedEntry = groupedEntries[groupedEntries.length - i - 2]
|
||||
for (var j = 0; j < consumers.count; j++) {
|
||||
var consumer = consumers.get(j)
|
||||
var value = groupedEntry.hasOwnProperty(consumer.id) ? groupedEntry[consumer.id] : 0
|
||||
if (previousGroupedEntry) {
|
||||
var previousValue = previousGroupedEntry.hasOwnProperty(consumer.id) ? previousGroupedEntry[consumer.id] : 0
|
||||
value -= previousValue
|
||||
}
|
||||
graphEntry[consumer.id] = value
|
||||
valueAxis.adjustMax(value)
|
||||
}
|
||||
labelTime = groupedEntry.timestamp
|
||||
} else {
|
||||
for (var j = 0; j < consumers.count; j++) {
|
||||
var consumer = consumers.get(j)
|
||||
graphEntry[consumer.id] = 0
|
||||
}
|
||||
labelTime = new Date(newestLogTimestamp.getTime() - config.sampleRate * i * 60000)
|
||||
}
|
||||
|
||||
// print("Adding entry:", labelTime, config.toLabel(labelTime), JSON.stringify(graphEntry))
|
||||
entries.unshift(graphEntry)
|
||||
labels.unshift(labelTime)
|
||||
|
||||
// Given we've added 2 entries for the first run but only one label, we'll add the missing label
|
||||
// at the end. This will shift the labels by one entries but that's ok because the logs timestamp
|
||||
// is when the sample was created, but for the user it's better to show the the consumption values
|
||||
// *during* that sample, not *before* the sample
|
||||
if (i == config.count - 1) {
|
||||
labelTime = new Date(labelTime.getTime() - config.sampleRate * 60000)
|
||||
// print("Adding oldest entry label", labelTime, config.sampleRate, config.toLabel(labelTime))
|
||||
labels.unshift(labelTime)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// print("assigning categories:", labels)
|
||||
categoryAxis.timestamps = labels
|
||||
|
||||
var map = {}
|
||||
for (var j = 0; j < consumers.count; j++) {
|
||||
var consumer = consumers.get(j)
|
||||
var barSet = barSeries.append(consumer.name, [])
|
||||
barSet.color = root.colors[j % root.colors.length]
|
||||
barSet.borderColor = barSet.color
|
||||
barSet.borderWith = 0
|
||||
map[consumer.id] = barSet
|
||||
}
|
||||
barSeries.thingBarSetMap = map
|
||||
|
||||
chartView.animationOptions = ChartView.SeriesAnimations
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = entries[i]
|
||||
// print("Adding entry", JSON.stringify(entry))
|
||||
for (var j = 0; j < consumers.count; j++) {
|
||||
var consumer = consumers.get(j)
|
||||
barSeries.thingBarSetMap[consumer.id].append(entry[consumer.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,7 +228,7 @@ StatsBase {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Style.smallMargins
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Consumers statistics")
|
||||
text: qsTr("Consumers totals")
|
||||
|
||||
}
|
||||
|
||||
@ -155,13 +240,14 @@ StatsBase {
|
||||
currentIndex: 0
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
append({modelData: qsTr("Months"), config: "months" })
|
||||
append({modelData: qsTr("Weeks"), config: "weeks" })
|
||||
append({modelData: qsTr("Days"), config: "days" })
|
||||
append({modelData: qsTr("Hours"), config: "hours" })
|
||||
append({modelData: qsTr("Days"), config: "days" })
|
||||
append({modelData: qsTr("Weeks"), config: "weeks" })
|
||||
append({modelData: qsTr("Months"), config: "months" })
|
||||
append({modelData: qsTr("Years"), config: "years" })
|
||||
// append({modelData: qsTr("Minutes"), config: "minutes" })
|
||||
|
||||
selectionTabs.currentIndex = 2
|
||||
selectionTabs.currentIndex = 1
|
||||
}
|
||||
}
|
||||
onCurrentValueChanged: {
|
||||
@ -217,6 +303,16 @@ StatsBase {
|
||||
lineVisible: false
|
||||
titleVisible: false
|
||||
shadesVisible: false
|
||||
|
||||
categories: {
|
||||
var ret = []
|
||||
for (var i = 0; i < timestamps.length; i++) {
|
||||
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i]))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
property var timestamps: []
|
||||
}
|
||||
axisY: ValueAxis {
|
||||
id: valueAxis
|
||||
@ -238,6 +334,78 @@ StatsBase {
|
||||
|
||||
property var thingBarSetMap: ({})
|
||||
}
|
||||
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: chartView
|
||||
anchors.leftMargin: chartView.plotArea.x
|
||||
anchors.topMargin: chartView.plotArea.y
|
||||
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
||||
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
Item {
|
||||
id: toolTip
|
||||
property int idx: Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)
|
||||
visible: mouseArea.containsMouse
|
||||
|
||||
x: Math.min(idx * mouseArea.width / categoryAxis.count, mouseArea.width - width)
|
||||
property double setMaxValue: {
|
||||
var max = 0;
|
||||
for (var i = 0; i < consumers.count; i++) {
|
||||
var consumer = consumers.get(i)
|
||||
max = barSeries.thingBarSetMap.hasOwnProperty(consumer.id) ? Math.max(max, barSeries.thingBarSetMap[consumer.id].at(idx)) : 0
|
||||
}
|
||||
return max
|
||||
}
|
||||
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
|
||||
|
||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||
|
||||
Behavior on x { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on y { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on width { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on height { NumberAnimation { duration: Style.animationDuration } }
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Style.tileOverlayColor
|
||||
opacity: .8
|
||||
radius: Style.smallCornerRadius
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: tooltipLayout
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
margins: Style.smallMargins
|
||||
}
|
||||
Label {
|
||||
text: categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : ""
|
||||
font: Style.smallFont
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: consumers
|
||||
delegate: RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: root.colors[index % root.colors.length]
|
||||
}
|
||||
Label {
|
||||
text: barSeries.thingBarSetMap.hasOwnProperty(model.id) ? "%1: %2 kWh".arg(model.name).arg(barSeries.thingBarSetMap[model.id].at(toolTip.idx).toFixed(2)) : ""
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,7 +141,9 @@ ChartView {
|
||||
series.borderColor = series.color
|
||||
|
||||
// print("Adding thingId series", thing.id, thing.name)
|
||||
d.thingsSeriesMap[thing.id] = series
|
||||
var map = d.thingsSeriesMap
|
||||
map[thing.id] = series
|
||||
d.thingsSeriesMap = map
|
||||
consumerThingIds.push(thing.id)
|
||||
}
|
||||
thingPowerLogs.thingIds = consumerThingIds;
|
||||
@ -235,4 +237,102 @@ ChartView {
|
||||
consumptionUpperSeries.append(entry.timestamp.getTime(), entry.consumption)
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.plotArea.x
|
||||
anchors.topMargin: root.plotArea.y
|
||||
anchors.rightMargin: root.width - root.plotArea.width - root.plotArea.x
|
||||
anchors.bottomMargin: root.height - root.plotArea.height - root.plotArea.y
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
Rectangle {
|
||||
height: parent.height
|
||||
width: 1
|
||||
color: Style.foregroundColor
|
||||
x: mouseArea.mouseX
|
||||
visible: mouseArea.containsMouse
|
||||
}
|
||||
|
||||
Item {
|
||||
id: toolTip
|
||||
visible: mouseArea.containsMouse
|
||||
|
||||
property int idx: consumptionUpperSeries.count - Math.floor(mouseArea.mouseX * consumptionUpperSeries.count / mouseArea.width)
|
||||
property int seriesIndex: consumptionUpperSeries.count - idx
|
||||
|
||||
property int xOnRight: mouseArea.mouseX + Style.smallMargins
|
||||
property int xOnLeft: mouseArea.mouseX - Style.smallMargins - width
|
||||
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
||||
property double maxValue: consumptionUpperSeries.at(seriesIndex).y
|
||||
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
||||
|
||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||
|
||||
property date timestamp: new Date(consumptionUpperSeries.at(seriesIndex).x)
|
||||
|
||||
Behavior on x { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on y { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on width { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on height { NumberAnimation { duration: Style.animationDuration } }
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Style.tileOverlayColor
|
||||
opacity: .8
|
||||
radius: Style.smallCornerRadius
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: tooltipLayout
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
margins: Style.smallMargins
|
||||
}
|
||||
Label {
|
||||
text: toolTip.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
font: Style.smallFont
|
||||
}
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: consumptionSeries.color
|
||||
}
|
||||
Label {
|
||||
property double rawValue: consumptionUpperSeries.at(toolTip.seriesIndex).y
|
||||
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
|
||||
property string unit: rawValue >= 1000 ? "kW" : "W"
|
||||
text: "%1: %2 %3".arg(qsTr("Total")).arg(displayValue.toFixed(2)).arg(unit)
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: consumers
|
||||
delegate: RowLayout {
|
||||
id: consumerToolTipDelegate
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: root.colors[index % root.colors.length]
|
||||
}
|
||||
|
||||
Label {
|
||||
property ThingPowerLogEntry entry: thingPowerLogs.find(model.id, toolTip.timestamp)
|
||||
property double rawValue: entry ? entry.currentPower : 0
|
||||
property double displayValue: rawValue >= 1000 ? rawValue / 1000 : rawValue
|
||||
property string unit: rawValue >= 1000 ? "kW" : "W"
|
||||
text: "%1: %2 %3".arg(model.name).arg(displayValue.toFixed(2)).arg(unit)
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
156
nymea-app/ui/mainviews/energy/ConsumersPieChart.qml
Normal file
156
nymea-app/ui/mainviews/energy/ConsumersPieChart.qml
Normal file
@ -0,0 +1,156 @@
|
||||
import QtQuick 2.8
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Controls.Material 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtCharts 2.2
|
||||
import Nymea 1.0
|
||||
|
||||
ChartView {
|
||||
id: root
|
||||
backgroundColor: "transparent"
|
||||
animationOptions: ChartView.SeriesAnimations
|
||||
title: qsTr("Consumers balance")
|
||||
titleColor: Style.foregroundColor
|
||||
legend.visible: false
|
||||
|
||||
property EnergyManager energyManager: null
|
||||
property ThingsProxy consumers: null
|
||||
property var colors: null
|
||||
|
||||
Connections {
|
||||
target: engine.thingManager
|
||||
onFetchingDataChanged: {
|
||||
if (!engine.thingManager.fetchingData) {
|
||||
updateConsumers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.consumers
|
||||
onCountChanged: {
|
||||
if (!engine.thingManager.fetchingData) {
|
||||
updateConsumers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: energyManager
|
||||
onPowerBalanceChanged: {
|
||||
var consumption = energyManager.currentPowerConsumption
|
||||
for (var i = 0; i < consumers.count; i++) {
|
||||
consumption -= consumers.get(i).stateByName("currentPower").value
|
||||
}
|
||||
d.unknownSlice.value = consumption
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
property var thingsColorMap: ({})
|
||||
property PieSlice unknownSlice: null
|
||||
}
|
||||
|
||||
function updateConsumers() {
|
||||
consumersBalanceSeries.clear();
|
||||
|
||||
var unknownConsumption = energyManager.currentPowerConsumption
|
||||
|
||||
var colorMap = {}
|
||||
for (var i = 0; i < consumers.count; i++) {
|
||||
var consumer = consumers.get(i)
|
||||
colorMap[consumer] = root.colors[i % root.colors.length]
|
||||
let currentPowerState = consumer.stateByName("currentPower")
|
||||
let slice = consumersBalanceSeries.append(consumer.name, currentPowerState.value)
|
||||
slice.color = root.colors[i % root.colors.length]
|
||||
currentPowerState.valueChanged.connect(function() {
|
||||
slice.value = currentPowerState.value
|
||||
})
|
||||
unknownConsumption -= currentPowerState.value
|
||||
}
|
||||
|
||||
d.unknownSlice = consumersBalanceSeries.append(qsTr("Unknown"), unknownConsumption)
|
||||
d.unknownSlice.color = Style.gray
|
||||
|
||||
d.thingsColorMap = colorMap
|
||||
}
|
||||
|
||||
PieSeries {
|
||||
id: consumersBalanceSeries
|
||||
size: 0.9
|
||||
holeSize: 0.7
|
||||
}
|
||||
|
||||
|
||||
ColumnLayout {
|
||||
id: centerLayout
|
||||
x: root.plotArea.x + (root.plotArea.width - width) / 2
|
||||
y: root.plotArea.y + (root.plotArea.height - height) / 2
|
||||
width: root.plotArea.width * 0.65
|
||||
// height: root.plotArea.height * 0.65
|
||||
spacing: Style.smallMargins
|
||||
property int maximumHeight: root.plotArea.height * 0.65
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
Label {
|
||||
text: qsTr("Total")
|
||||
font: Style.smallFont
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "%1 %2"
|
||||
.arg((energyManager.currentPowerConsumption / (energyManager.currentPowerConsumption > 1000 ? 1000 : 1)).toFixed(1))
|
||||
.arg(energyManager.currentPowerConsumption > 1000 ? "kW" : "W")
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font: Style.bigFont
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: count * (Style.smallMargins + Style.extraSmallFont.pixelSize + Style.smallFont.pixelSize)
|
||||
Layout.maximumHeight: centerLayout.maximumHeight - y
|
||||
clip: true
|
||||
spacing: Style.smallMargins
|
||||
|
||||
model: ThingsProxy {
|
||||
id: sortedConsumers
|
||||
engine: _engine
|
||||
parentProxy: root.consumers
|
||||
sortStateName: "currentPower"
|
||||
sortOrder: Qt.DescendingOrder
|
||||
}
|
||||
delegate: ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: 0
|
||||
Label {
|
||||
text: model.name
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
Label {
|
||||
property Thing consumer: sortedConsumers.get(index)
|
||||
property State currentPowerState: consumer ? consumer.stateByName("currentPower") : null
|
||||
property double value: currentPowerState ? currentPowerState.value : 0
|
||||
|
||||
color: d.thingsColorMap[consumer]
|
||||
text: "%1 %2"
|
||||
.arg((value / (value > 1000 ? 1000 : 1)).toFixed(1))
|
||||
.arg(value > 1000 ? "kWh" : "W")
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font: Style.smallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@ ChartView {
|
||||
id: consumptionPieChart
|
||||
backgroundColor: "transparent"
|
||||
animationOptions: ChartView.SeriesAnimations
|
||||
title: qsTr("Current power consumption balance")
|
||||
title: qsTr("My energy mix")
|
||||
titleColor: Style.foregroundColor
|
||||
legend.visible: false
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ ChartView {
|
||||
id: productionPieChart
|
||||
backgroundColor: "transparent"
|
||||
animationOptions: ChartView.SeriesAnimations
|
||||
title: qsTr("Current power production balance")
|
||||
title: qsTr("My energy production")
|
||||
titleColor: Style.foregroundColor
|
||||
legend.visible: false
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ StatsBase {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Style.smallMargins
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Energy consumption statistics")
|
||||
text: qsTr("Totals")
|
||||
|
||||
}
|
||||
|
||||
@ -36,54 +36,56 @@ StatsBase {
|
||||
Layout.rightMargin: Style.smallMargins
|
||||
model: ListModel {
|
||||
Component.onCompleted: {
|
||||
append({modelData: qsTr("Months"), config: "months" })
|
||||
append({modelData: qsTr("Weeks"), config: "weeks" })
|
||||
append({modelData: qsTr("Days"), config: "days" })
|
||||
append({modelData: qsTr("Hours"), config: "hours" })
|
||||
append({modelData: qsTr("Days"), config: "days" })
|
||||
append({modelData: qsTr("Weeks"), config: "weeks" })
|
||||
append({modelData: qsTr("Months"), config: "months" })
|
||||
append({modelData: qsTr("Years"), config: "years" })
|
||||
// append({modelData: qsTr("Minutes"), config: "minutes" })
|
||||
|
||||
selectionTabs.currentIndex = 2
|
||||
selectionTabs.currentIndex = 1
|
||||
}
|
||||
}
|
||||
onCurrentValueChanged: {
|
||||
var config = root.configs[currentValue.config]
|
||||
print("config:", config.startTime(), config.sampleList(), config.sampleListNames())
|
||||
print("config:", config.startTime(), config.sampleRate)
|
||||
|
||||
powerBalanceLogs.loadingInhibited = true
|
||||
powerBalanceLogs.sampleRate = config.sampleRate
|
||||
powerBalanceLogs.startTime = config.startTime()
|
||||
powerBalanceLogs.sampleList = config.sampleList()
|
||||
powerBalanceLogs.startTime = new Date(config.startTime().getTime() - config.sampleRate * 60000)
|
||||
powerBalanceLogs.loadingInhibited = false
|
||||
|
||||
barSeries.clear();
|
||||
|
||||
d.consumptionSet = barSeries.append(qsTr("Consumed"), [])
|
||||
d.consumptionSet.color = Style.blue
|
||||
d.consumptionSet.borderColor = d.consumptionSet.color
|
||||
d.consumptionSet.borderWidth = 0
|
||||
d.productionSet = barSeries.append(qsTr("Produced"), [])
|
||||
d.productionSet.color = Style.green
|
||||
d.productionSet.borderColor = d.productionSet.color
|
||||
d.productionSet.borderWidth = 0
|
||||
d.acquisitionSet = barSeries.append(qsTr("From grid"), [])
|
||||
d.acquisitionSet.color = Style.red
|
||||
d.acquisitionSet.borderColor = d.acquisitionSet.color
|
||||
d.acquisitionSet.borderWidth = 0
|
||||
d.returnSet = barSeries.append(qsTr("To grid"), [])
|
||||
d.returnSet.color = Style.orange
|
||||
d.returnSet.borderColor = d.returnSet.color
|
||||
d.returnSet.borderWidth = 0
|
||||
|
||||
|
||||
valueAxis.max = 0
|
||||
categoryAxis.categories = config.sampleListNames()
|
||||
|
||||
chartView.animationOptions = ChartView.SeriesAnimations
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: energyManager
|
||||
onPowerBalanceChanged: {
|
||||
var start = powerBalanceLogs.get(powerBalanceLogs.count - 1)
|
||||
// print("balance changed:", d.consumptionSet, powerBalanceLogs, powerBalanceLogs.count)
|
||||
// print("updating", start.timestamp, root.energyManager.totalConsumption - (start ? start.totalConsumption : 0))
|
||||
var start = powerBalanceLogs.get(powerBalanceLogs.count - 1 )
|
||||
// print("balance changed:", d.consumptionSet, powerBalanceLogs, powerBalanceLogs.count)
|
||||
// print("updating", start.timestamp, start.totalConsumption, root.energyManager.totalConsumption, root.energyManager.totalConsumption - (start ? start.totalConsumption : 0))
|
||||
d.consumptionSet.replace(d.consumptionSet.count - 1, root.energyManager.totalConsumption - (start ? start.totalConsumption : 0))
|
||||
d.productionSet.replace(d.productionSet.count - 1, root.energyManager.totalProduction - (start ? start.totalProduction : 0))
|
||||
d.acquisitionSet.replace(d.acquisitionSet.count - 1, root.energyManager.totalAcquisition - (start ? start.totalAcquisition : 0))
|
||||
@ -96,48 +98,102 @@ StatsBase {
|
||||
engine: _engine
|
||||
loadingInhibited: true
|
||||
|
||||
property var sampleList: minutesList
|
||||
|
||||
onFetchingDataChanged: {
|
||||
if (!fetchingData) {
|
||||
if (powerBalanceLogs.count == 0) {
|
||||
valueAxis.adjustMax(root.energyManager.totalConsumption)
|
||||
valueAxis.adjustMax(root.energyManager.totalAcquisition)
|
||||
valueAxis.adjustMax(root.energyManager.totalProduction)
|
||||
valueAxis.adjustMax(root.energyManager.totalReturn)
|
||||
chartView.animationOptions = ChartView.NoAnimation
|
||||
|
||||
for (var i = 0; i < sampleList.length; i++) {
|
||||
d.consumptionSet.append(i == sampleList.length - 1 ? root.energyManager.totalConsumption : 0)
|
||||
d.productionSet.append(i == sampleList.length - 1 ? root.energyManager.totalProduction : 0)
|
||||
d.acquisitionSet.append(i == sampleList.length - 1 ? root.energyManager.totalAcquisition : 0)
|
||||
d.returnSet.append(i == sampleList.length - 1 ? root.energyManager.totalReturn : 0)
|
||||
print("Logs fetched")
|
||||
var config = root.configs[selectionTabs.currentValue.config]
|
||||
|
||||
var labels = []
|
||||
var entries = []
|
||||
|
||||
var newestLogTimestamp = powerBalanceLogs.count > 0 ? powerBalanceLogs.get(powerBalanceLogs.count - 1).timestamp : new Date();
|
||||
for (var i = 0; i < config.count; i++) {
|
||||
var entry = powerBalanceLogs.get(powerBalanceLogs.count - i - 1)
|
||||
|
||||
// if it's the first, let's add a generated entry which shows the total from the newest log to the current live value
|
||||
if (i == 0) {
|
||||
var liveEntry = {
|
||||
consumption: energyManager.totalConsumption,
|
||||
production: energyManager.totalProduction,
|
||||
acquisition: energyManager.totalAcquisition,
|
||||
returned: energyManager.totalReturn
|
||||
}
|
||||
if (entry) {
|
||||
liveEntry.consumption -= entry.totalConsumption
|
||||
liveEntry.production -= entry.totalProduction
|
||||
liveEntry.acquisition -= entry.totalAcquisition
|
||||
liveEntry.returned -= entry.totalReturn
|
||||
}
|
||||
print("Adding live entry:", liveEntry.consumption, root.energyManager.totalConsumption, entry ? entry.totalConsumption : 0)
|
||||
entries.unshift(liveEntry)
|
||||
valueAxis.adjustMax(liveEntry.consumption)
|
||||
valueAxis.adjustMax(liveEntry.production)
|
||||
valueAxis.adjustMax(liveEntry.acquisition)
|
||||
valueAxis.adjustMax(liveEntry.returned)
|
||||
}
|
||||
return;
|
||||
|
||||
// Add the actual entry
|
||||
var graphEntry = {
|
||||
consumption: 0,
|
||||
production: 0,
|
||||
acquisition: 0,
|
||||
returned: 0
|
||||
}
|
||||
var labelTime = new Date();
|
||||
if (entry) {
|
||||
// print("Have entry:", entry.timestamp, config.toLabel(entry.timestamp))
|
||||
var previous = powerBalanceLogs.get(powerBalanceLogs.count - i - 2)
|
||||
if (previous) {
|
||||
graphEntry.consumption = entry.totalConsumption - previous.totalConsumption
|
||||
graphEntry.production = entry.totalProduction - previous.totalProduction
|
||||
graphEntry.acquisition = entry.totalAcquisition - previous.totalAcquisition
|
||||
graphEntry.returned = entry.totalReturn - previous.totalReturn
|
||||
} else {
|
||||
graphEntry.consumption = entry.totalConsumption
|
||||
graphEntry.production = entry.totalProduction
|
||||
graphEntry.acquisition = entry.totalAcquisition
|
||||
graphEntry.returned = entry.totalReturn
|
||||
}
|
||||
labelTime = entry.timestamp
|
||||
} else {
|
||||
labelTime = new Date(newestLogTimestamp.getTime() - config.sampleRate * i * 60000)
|
||||
}
|
||||
|
||||
// print("Adding entry:", labelTime, graphEntry.consumption, config.toLabel(labelTime))
|
||||
entries.unshift(graphEntry)
|
||||
labels.unshift(labelTime)
|
||||
|
||||
// Given we've added 2 entries for the first run but only one label, we'll add the missing label
|
||||
// at the end. This will shift the labels by one entries but that's ok because the logs timestamp
|
||||
// is when the sample was created, but for the user it's better to show the the consumption values
|
||||
// *during* that sample, not *before* the sample
|
||||
if (i == config.count - 1) {
|
||||
labelTime = new Date(labelTime.getTime() - config.sampleRate * 60000)
|
||||
// print("Adding oldest entry label", labelTime, config.sampleRate, config.toLabel(labelTime))
|
||||
labels.unshift(labelTime)
|
||||
}
|
||||
|
||||
valueAxis.adjustMax(graphEntry.consumption)
|
||||
valueAxis.adjustMax(graphEntry.production)
|
||||
valueAxis.adjustMax(graphEntry.acquisition)
|
||||
valueAxis.adjustMax(graphEntry.returned)
|
||||
}
|
||||
|
||||
for (var i = 0; i < sampleList.length; i++) {
|
||||
var start = powerBalanceLogs.find(new Date(sampleList[i]))
|
||||
var end = null;
|
||||
if (i+1 < sampleList.length) {
|
||||
end = powerBalanceLogs.find(new Date(sampleList[i+1]))
|
||||
}
|
||||
// print("** stats for:", new Date(sampleList[i]), /*start, end, */"start:", start ? start.totalConsumption : 0, "end:", end ? end.totalConsumption : root.energyManager.totalConsumption)
|
||||
var consumptionValue = (end != null ? end.totalConsumption : root.energyManager.totalConsumption) - (start ? start.totalConsumption : 0)
|
||||
var productionValue = (end != null ? end.totalProduction : root.energyManager.totalProduction) - (start ? start.totalProduction : 0)
|
||||
var acquisitionValue = (end != null ? end.totalAcquisition : root.energyManager.totalAcquisition) - (start ? start.totalAcquisition : 0)
|
||||
var returnValue = (end != null ? end.totalReturn : root.energyManager.totalReturn) - (start ? start.totalReturn : 0)
|
||||
// print("assigning categories:", labels)
|
||||
categoryAxis.timestamps = labels
|
||||
|
||||
valueAxis.adjustMax(consumptionValue)
|
||||
valueAxis.adjustMax(productionValue)
|
||||
valueAxis.adjustMax(acquisitionValue)
|
||||
valueAxis.adjustMax(returnValue)
|
||||
|
||||
d.consumptionSet.append(consumptionValue)
|
||||
d.productionSet.append(productionValue)
|
||||
d.acquisitionSet.append(acquisitionValue)
|
||||
d.returnSet.append(returnValue)
|
||||
chartView.animationOptions = ChartView.SeriesAnimations
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
// print("Appending to set", JSON.stringify(entries[i]))
|
||||
d.consumptionSet.append(entries[i].consumption)
|
||||
d.productionSet.append(entries[i].production)
|
||||
d.acquisitionSet.append(entries[i].acquisition)
|
||||
d.returnSet.append(entries[i].returned)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,14 +202,23 @@ StatsBase {
|
||||
return
|
||||
}
|
||||
|
||||
var config = root.configs[selectionTabs.currentValue.config]
|
||||
|
||||
|
||||
var start = entry
|
||||
var consumptionValue = root.energyManager.totalConsumption - (start ? start.totalConsumption : 0)
|
||||
var productionValue = root.energyManager.totalProduction - (start ? start.totalProduction : 0)
|
||||
var acquisitionValue = root.energyManager.totalAcquisition - (start ? start.totalAcquisition : 0)
|
||||
var returnValue = root.energyManager.totalReturn - (start ? start.totalReturn : 0)
|
||||
// print("Entry added:", entry.timestamp, entry.totalConsumption, consumptionValue)
|
||||
|
||||
chartView.animationOptions = ChartView.NoAnimation
|
||||
categoryAxis.categories = configs[selectionTabs.currentValue.config].sampleListNames()
|
||||
|
||||
var timestamps = categoryAxis.timestamps;
|
||||
timestamps.splice(0, 1)
|
||||
timestamps.push(entry.timestamp)
|
||||
categoryAxis.timestamps = timestamps
|
||||
|
||||
d.consumptionSet.append(consumptionValue)
|
||||
d.productionSet.append(productionValue)
|
||||
d.acquisitionSet.append(acquisitionValue)
|
||||
@ -171,7 +236,7 @@ StatsBase {
|
||||
id: chartView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
animationOptions: ChartView.NoAnimations
|
||||
animationOptions: ChartView.NoAnimation
|
||||
|
||||
backgroundColor: "transparent"
|
||||
legend.alignment: Qt.AlignBottom
|
||||
@ -214,6 +279,17 @@ StatsBase {
|
||||
lineVisible: false
|
||||
titleVisible: false
|
||||
shadesVisible: false
|
||||
|
||||
categories: {
|
||||
var ret = []
|
||||
for (var i = 0; i < timestamps.length; i++) {
|
||||
ret.push(root.configs[selectionTabs.currentValue.config].toLabel(timestamps[i]))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
property var timestamps: []
|
||||
|
||||
}
|
||||
axisY: ValueAxis {
|
||||
id: valueAxis
|
||||
@ -233,6 +309,106 @@ StatsBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: chartView
|
||||
anchors.leftMargin: chartView.plotArea.x
|
||||
anchors.topMargin: chartView.plotArea.y
|
||||
anchors.rightMargin: chartView.width - chartView.plotArea.width - chartView.plotArea.x
|
||||
anchors.bottomMargin: chartView.height - chartView.plotArea.height - chartView.plotArea.y
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
Item {
|
||||
id: toolTip
|
||||
property int idx: Math.floor(mouseArea.mouseX * categoryAxis.count / mouseArea.width)
|
||||
visible: mouseArea.containsMouse
|
||||
|
||||
x: Math.min(idx * mouseArea.width / categoryAxis.count, mouseArea.width - width)
|
||||
property double setMaxValue: d.consumptionSet && d.productionSet && d.acquisitionSet && d.returnSet ?
|
||||
Math.max(d.consumptionSet.at(idx), Math.max(d.productionSet.at(idx), Math.max(d.acquisitionSet.at(idx), d.returnSet.at(idx))))
|
||||
: 0
|
||||
y: Math.min(Math.max(mouseArea.height - (setMaxValue * mouseArea.height / valueAxis.max) - height - Style.smallMargins, 0), mouseArea.height - height)
|
||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||
|
||||
Behavior on x { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on y { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on width { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on height { NumberAnimation { duration: Style.animationDuration } }
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Style.tileOverlayColor
|
||||
opacity: .8
|
||||
radius: Style.smallCornerRadius
|
||||
}
|
||||
|
||||
|
||||
ColumnLayout {
|
||||
id: tooltipLayout
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
margins: Style.smallMargins
|
||||
}
|
||||
// Label {
|
||||
// text: powerBalanceLogs.count + ":" + categoryAxis.count + ":" + toolTip.idx
|
||||
// }
|
||||
|
||||
Label {
|
||||
text: categoryAxis.timestamps.length > toolTip.idx ? root.configs[selectionTabs.currentValue.config].toLongLabel(categoryAxis.timestamps[toolTip.idx]) : ""
|
||||
font: Style.smallFont
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.blue
|
||||
}
|
||||
Label {
|
||||
text: d.consumptionSet ? qsTr("Consumed: %1 kWh").arg(d.consumptionSet.at(toolTip.idx).toFixed(2)) : ""
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.green
|
||||
}
|
||||
Label {
|
||||
text: d.productionSet ? qsTr("Produced: %1 kWh").arg(d.productionSet.at(toolTip.idx).toFixed(2)) : ""
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.red
|
||||
}
|
||||
Label {
|
||||
text: d.acquisitionSet ? qsTr("From grid: %1 kWh").arg(d.acquisitionSet.at(toolTip.idx).toFixed(2)) : ""
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.orange
|
||||
}
|
||||
Label {
|
||||
text: d.returnSet ? qsTr("To grid: %1 kWh").arg(d.returnSet.at(toolTip.idx).toFixed(2)) : ""
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ ChartView {
|
||||
margins.bottom: 0
|
||||
margins.top: 0
|
||||
|
||||
title: qsTr("Power consumption balance history")
|
||||
title: qsTr("My consumption history")
|
||||
titleColor: Style.foregroundColor
|
||||
|
||||
legend.alignment: Qt.AlignBottom
|
||||
@ -229,4 +229,101 @@ ChartView {
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.plotArea.x
|
||||
anchors.topMargin: root.plotArea.y
|
||||
anchors.rightMargin: root.width - root.plotArea.width - root.plotArea.x
|
||||
anchors.bottomMargin: root.height - root.plotArea.height - root.plotArea.y
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
Rectangle {
|
||||
height: parent.height
|
||||
width: 1
|
||||
color: Style.foregroundColor
|
||||
x: mouseArea.mouseX
|
||||
visible: mouseArea.containsMouse
|
||||
}
|
||||
|
||||
Item {
|
||||
id: toolTip
|
||||
visible: mouseArea.containsMouse
|
||||
|
||||
property int idx: consumptionUpperSeries.count - (Math.floor(mouseArea.mouseX * consumptionUpperSeries.count / mouseArea.width))
|
||||
property int seriesIndex: consumptionUpperSeries.count - idx
|
||||
|
||||
property int xOnRight: mouseArea.mouseX + Style.smallMargins
|
||||
property int xOnLeft: mouseArea.mouseX - Style.smallMargins - width
|
||||
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
||||
property double maxValue: consumptionUpperSeries.at(seriesIndex).y
|
||||
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
||||
|
||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||
|
||||
Behavior on x { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on y { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on width { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on height { NumberAnimation { duration: Style.animationDuration } }
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Style.tileOverlayColor
|
||||
opacity: .8
|
||||
radius: Style.smallCornerRadius
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: tooltipLayout
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
margins: Style.smallMargins
|
||||
}
|
||||
Label {
|
||||
text: new Date(consumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
font: Style.smallFont
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.green
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Self production: %1 kW").arg(selfProductionUpperSeries.at(toolTip.seriesIndex).y.toFixed(2))
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.orange
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("From battery: %1 kW").arg(storageUpperSeries.at(toolTip.seriesIndex).y.toFixed(2))
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.red
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("From grid: %1 kW").arg(acquisitionUpperSeries.at(toolTip.seriesIndex).y.toFixed(2))
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ ChartView {
|
||||
margins.bottom: 0
|
||||
margins.top: 0
|
||||
|
||||
title: qsTr("Power production balance history")
|
||||
title: qsTr("My production history")
|
||||
titleColor: Style.foregroundColor
|
||||
|
||||
legend.alignment: Qt.AlignBottom
|
||||
@ -222,4 +222,101 @@ ChartView {
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.plotArea.x
|
||||
anchors.topMargin: root.plotArea.y
|
||||
anchors.rightMargin: root.width - root.plotArea.width - root.plotArea.x
|
||||
anchors.bottomMargin: root.height - root.plotArea.height - root.plotArea.y
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
Rectangle {
|
||||
height: parent.height
|
||||
width: 1
|
||||
color: Style.foregroundColor
|
||||
x: mouseArea.mouseX
|
||||
visible: mouseArea.containsMouse
|
||||
}
|
||||
|
||||
Item {
|
||||
id: toolTip
|
||||
visible: mouseArea.containsMouse
|
||||
|
||||
property int idx: productionUpperSeries.count - Math.floor(mouseArea.mouseX * productionUpperSeries.count / mouseArea.width)
|
||||
property int seriesIndex: productionUpperSeries.count - idx
|
||||
|
||||
property int xOnRight: mouseArea.mouseX + Style.smallMargins
|
||||
property int xOnLeft: mouseArea.mouseX - Style.smallMargins - width
|
||||
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
||||
property double maxValue: productionUpperSeries.at(seriesIndex).y
|
||||
y: Math.min(Math.max(mouseArea.height - (maxValue * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
||||
|
||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||
|
||||
Behavior on x { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on y { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on width { NumberAnimation { duration: Style.animationDuration } }
|
||||
Behavior on height { NumberAnimation { duration: Style.animationDuration } }
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Style.tileOverlayColor
|
||||
opacity: .8
|
||||
radius: Style.smallCornerRadius
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: tooltipLayout
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
margins: Style.smallMargins
|
||||
}
|
||||
Label {
|
||||
text: new Date(selfConsumptionUpperSeries.at(toolTip.seriesIndex).x).toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
font: Style.smallFont
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.green
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("consumed: %1 kW").arg(selfConsumptionUpperSeries.at(toolTip.seriesIndex).y.toFixed(2))
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.orange
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("To battery: %1 kW").arg(storageUpperSeries.at(toolTip.seriesIndex).y.toFixed(2))
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
width: Style.extraSmallFont.pixelSize
|
||||
height: width
|
||||
color: Style.red
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("To grid: %1 kW").arg(acquisitionUpperSeries.at(toolTip.seriesIndex).y.toFixed(2))
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,42 +4,55 @@ import Nymea 1.0
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int minutesCount: 10
|
||||
property int hoursCount: 12
|
||||
property int daysCount: 7
|
||||
property int minutesCount: 9
|
||||
property int hoursCount: 11
|
||||
property int daysCount: 6
|
||||
property int weeksCount: 12
|
||||
property int monthsCount: 12
|
||||
property int monthsCount: 11
|
||||
property int yearsCount: 5
|
||||
|
||||
property var configs: ({
|
||||
minutes: {
|
||||
count: minutesCount,
|
||||
startTime: minutesStart,
|
||||
sampleRate: EnergyLogs.SampleRate1Min,
|
||||
sampleList: minutesList,
|
||||
sampleListNames: minutesListNames
|
||||
toLabel: minuteLabel,
|
||||
toLongLabel: minuteLongLabel
|
||||
},
|
||||
hours: {
|
||||
count: hoursCount,
|
||||
startTime: hoursStart,
|
||||
sampleRate: EnergyLogs.SampleRate1Hour,
|
||||
sampleList: hoursList,
|
||||
sampleListNames: hoursListNames
|
||||
toLabel: hourLabel,
|
||||
toLongLabel: hourLongLabel
|
||||
},
|
||||
days: {
|
||||
count: daysCount,
|
||||
startTime: daysStart,
|
||||
sampleRate: EnergyLogs.SampleRate1Day,
|
||||
sampleList: daysList,
|
||||
sampleListNames: daysListNames
|
||||
toLabel: dayLabel,
|
||||
toLongLabel: dayLongLabel
|
||||
},
|
||||
weeks: {
|
||||
count: weeksCount,
|
||||
startTime: weeksStart,
|
||||
sampleRate: EnergyLogs.SampleRate1Week,
|
||||
sampleList: weeksList,
|
||||
sampleListNames: weeksListNames
|
||||
toLabel: weekLabel,
|
||||
toLongLabel: weekLongLabel
|
||||
},
|
||||
months: {
|
||||
count: monthsCount,
|
||||
startTime: monthsStart,
|
||||
sampleRate: EnergyLogs.SampleRate1Month,
|
||||
sampleList: monthsList,
|
||||
sampleListNames: monthsListNames
|
||||
toLabel: monthLabel,
|
||||
toLongLabel: monthLongLabel
|
||||
},
|
||||
years: {
|
||||
count: yearsCount,
|
||||
startTime: yearStart,
|
||||
sampleRate: EnergyLogs.SampleRate1Year,
|
||||
toLabel: yearLabel,
|
||||
toLongLabel: yearLabel
|
||||
}
|
||||
})
|
||||
|
||||
@ -48,45 +61,24 @@ Item {
|
||||
d.setMinutes(d.getMinutes() - minutesCount + 1, 0, 0)
|
||||
return d;
|
||||
}
|
||||
function minutesList() {
|
||||
var ret = []
|
||||
var startTime = minutesStart();
|
||||
for (var i = 0; i < minutesCount; i++) {
|
||||
var last = new Date(startTime)
|
||||
ret.push(last.setTime(last.getTime() + i * 60 * 1000))
|
||||
}
|
||||
return ret;
|
||||
function minuteLabel(date) {
|
||||
return date.toLocaleString(Qt.locale(), "hh:mm")
|
||||
}
|
||||
function minutesListNames() {
|
||||
var ret = []
|
||||
var list = minutesList()
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
ret.push(new Date(list[i]).toLocaleString(Qt.locale(), "hh:mm"))
|
||||
}
|
||||
return ret;
|
||||
function minuteLongLabel(date) {
|
||||
return date.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
}
|
||||
|
||||
|
||||
function hoursStart() {
|
||||
var d = new Date();
|
||||
d.setHours(d.getHours() - hoursCount + 1, 0, 0, 0)
|
||||
return d;
|
||||
}
|
||||
function hoursList() {
|
||||
var ret = []
|
||||
var startTime = hoursStart();
|
||||
for (var i = 0; i < hoursCount; i++) {
|
||||
var last = new Date(startTime)
|
||||
ret.push(last.setTime(last.getTime() + i * 60 * 60 * 1000))
|
||||
}
|
||||
return ret;
|
||||
function hourLabel(date) {
|
||||
return date.toLocaleString(Qt.locale(), "hh")
|
||||
}
|
||||
function hoursListNames() {
|
||||
var ret = [];
|
||||
var list = hoursList();
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
ret.push(new Date(list[i]).toLocaleString(Qt.locale(), "hh"));
|
||||
}
|
||||
return ret;
|
||||
function hourLongLabel(date) {
|
||||
return date.toLocaleString(Qt.locale(), Locale.ShortFormat)
|
||||
}
|
||||
|
||||
function daysStart() {
|
||||
@ -95,24 +87,11 @@ Item {
|
||||
d.setDate(d.getDate() - daysCount + 1);
|
||||
return d;
|
||||
}
|
||||
|
||||
function daysList() {
|
||||
var ret = []
|
||||
var startTime = daysStart();
|
||||
for (var i = 0; i < daysCount; i++) {
|
||||
var last = new Date(startTime)
|
||||
ret.push(last.setDate(last.getDate() + i))
|
||||
}
|
||||
return ret;
|
||||
function dayLabel(date) {
|
||||
return date.toLocaleString(Qt.locale(), "ddd")
|
||||
}
|
||||
|
||||
function daysListNames() {
|
||||
var ret = []
|
||||
var list = daysList();
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
ret.push(new Date(list[i]).toLocaleString(Qt.locale(), "ddd"))
|
||||
}
|
||||
return ret;
|
||||
function dayLongLabel(date) {
|
||||
return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat)
|
||||
}
|
||||
|
||||
function weeksStart() {
|
||||
@ -121,57 +100,41 @@ Item {
|
||||
d.setDate(d.getDate() - d.getDay() - weeksCount * 7);
|
||||
return d
|
||||
}
|
||||
function weeksList() {
|
||||
var ret = []
|
||||
var startTime = weeksStart();
|
||||
for (var i = 0; i < weeksCount; i++) {
|
||||
var last = new Date(startTime)
|
||||
ret.push(last.setDate(last.getDate() + i * 7))
|
||||
}
|
||||
return ret;
|
||||
function weekLabel(date) {
|
||||
var yearStart = new Date();
|
||||
yearStart.setHours(0,0,0,0);
|
||||
yearStart.setDate(1);
|
||||
yearStart.setMonth(0);
|
||||
return Math.ceil((((date - yearStart) / 86400000) + 1)/7)
|
||||
}
|
||||
function weeksListNames() {
|
||||
var ret = []
|
||||
var list = weeksList();
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var d = new Date(list[i])
|
||||
var dayNum = d.getDay() || 7;
|
||||
d.setDate(d.getDate() + 4 - dayNum);
|
||||
ret.push(Math.ceil((((d - yearStart()) / 86400000) + 1)/7))
|
||||
}
|
||||
return ret;
|
||||
function weekLongLabel(date) {
|
||||
var endDate = new Date(date)
|
||||
endDate.setDate(endDate.getDate() + 6)
|
||||
return date.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + " - " + endDate.toLocaleDateString(Qt.locale(), Locale.ShortFormat)
|
||||
}
|
||||
|
||||
|
||||
function monthsStart() {
|
||||
var d = new Date();
|
||||
d.setHours(0,0,0,0);
|
||||
d.setMonth(d.getMonth() - monthsCount + 1, 1);
|
||||
return d;
|
||||
}
|
||||
function monthsList() {
|
||||
var ret = []
|
||||
var startTime = monthsStart();
|
||||
for (var i = 0; i < monthsCount; i++) {
|
||||
var last = new Date(startTime)
|
||||
ret.push(last.setMonth(last.getMonth() + i))
|
||||
}
|
||||
return ret;
|
||||
function monthLabel(date) {
|
||||
return date.toLocaleString(Qt.locale(), "MMM")
|
||||
}
|
||||
function monthsListNames() {
|
||||
var ret = []
|
||||
var list = monthsList();
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
ret.push(new Date(list[i]).toLocaleString(Qt.locale(), "MMM"))
|
||||
}
|
||||
return ret;
|
||||
function monthLongLabel(date) {
|
||||
return date.toLocaleString(Qt.locale(), "MMMM yyyy")
|
||||
}
|
||||
|
||||
function yearStart() {
|
||||
var d = new Date();
|
||||
d.setHours(0,0,0,0);
|
||||
d.setDate(1);
|
||||
d.setMonth(0);
|
||||
d.setFullYear(d.getFullYear() - yearsCount + 1, 0, 1)
|
||||
return d;
|
||||
}
|
||||
function yearLabel(date) {
|
||||
return date.toLocaleString(Qt.locale(), "yyyy")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user