diff --git a/libnymea-app/energy/energylogs.cpp b/libnymea-app/energy/energylogs.cpp index 745ae0a7..842687d9 100644 --- a/libnymea-app/energy/energylogs.cpp +++ b/libnymea-app/energy/energylogs.cpp @@ -216,38 +216,42 @@ int EnergyLogs::indexOf(const QDateTime ×tamp) if (m_list.isEmpty()) { return -1; } - QDateTime first = m_list.first()->timestamp(); - int index = qRound(1.0 * first.secsTo(timestamp) / (m_sampleRate * 60)); - if (index < 0 || index >= m_list.count()) { - qCDebug(dcEnergyLogs()) << "finding:" << timestamp << index << first.toString() << "NOT FOUND" << m_list.last()->timestamp() << m_list.count(); + const qint64 target = timestamp.toMSecsSinceEpoch(); + const qint64 firstTimestamp = m_list.first()->timestamp().toMSecsSinceEpoch(); + const qint64 lastTimestamp = m_list.last()->timestamp().toMSecsSinceEpoch(); + if (target < firstTimestamp || target > lastTimestamp) { return -1; } - qCDebug(dcEnergyLogs()) << "finding:" << timestamp << index << first.toString() << m_list.at(index)->timestamp(); + int low = 0; + int high = m_list.count() - 1; - // Normally, if the DB is in a consistent state, we can rely that the above finds the correct entry. - // However, if the user changes the timezone, during the lifetime, or other woes may appear like NTP - // changing time which may cause inconsistent entries like passing the same time twice, we could end up - // off by one. In order to compensate for that, we'll see if the next or previous entries may be closer - // In theory we could even be off by some more samples in very rare circumstances, but unlikely enough - // to not bother with that at this point. - QDateTime found = m_list.at(index)->timestamp(); - QDateTime previous = index > 0 ? m_list.at(index-1)->timestamp() : found; - QDateTime next = index < m_list.count() - 1 ? m_list.at(index+1)->timestamp() : found; - - int diffToFound = qAbs(timestamp.secsTo(found)); - int diffToPrevious = qAbs(timestamp.secsTo(previous)); - int diffToNext = qAbs(timestamp.secsTo(next)); - if (diffToPrevious < diffToFound && diffToPrevious < diffToNext) { -// qWarning() << "Correcting to previous" << index << m_list.count() << found << previous << diffToPrevious << diffToFound; - return index - 1; + // Use timestamp-based lookup instead of sample-rate math. This is robust against + // duplicate/missing rows (e.g. after resampling glitches or timezone jumps). + while (low <= high) { + const int mid = low + (high - low) / 2; + const qint64 midTimestamp = m_list.at(mid)->timestamp().toMSecsSinceEpoch(); + if (midTimestamp < target) { + low = mid + 1; + } else if (midTimestamp > target) { + high = mid - 1; + } else { + return mid; + } } - if (diffToNext < diffToFound) { -// qWarning() << "Correcting to next" << index << m_list.count() << found << next << diffToNext << diffToFound; - return index + 1; + + const int previousIndex = low - 1; + const int nextIndex = low; + const qint64 previousTimestamp = m_list.at(previousIndex)->timestamp().toMSecsSinceEpoch(); + const qint64 nextTimestamp = m_list.at(nextIndex)->timestamp().toMSecsSinceEpoch(); + const qint64 diffToPrevious = qAbs(target - previousTimestamp); + const qint64 diffToNext = qAbs(target - nextTimestamp); + + if (diffToPrevious <= diffToNext) { + return previousIndex; } - return index; + return nextIndex; } EnergyLogEntry *EnergyLogs::find(const QDateTime ×tamp) diff --git a/libnymea-app/energy/powerbalancelogs.cpp b/libnymea-app/energy/powerbalancelogs.cpp index b1640c83..bed78576 100644 --- a/libnymea-app/energy/powerbalancelogs.cpp +++ b/libnymea-app/energy/powerbalancelogs.cpp @@ -24,6 +24,7 @@ #include "powerbalancelogs.h" +#include #include PowerBalanceLogEntry::PowerBalanceLogEntry(QObject *parent): EnergyLogEntry(parent) @@ -98,8 +99,15 @@ QString PowerBalanceLogs::logsName() const QList PowerBalanceLogs::unpackEntries(const QVariantMap ¶ms, double *minValue, double *maxValue) { QList ret; + QMap deduplicatedEntries; foreach (const QVariant &variant, params.value("powerBalanceLogEntries").toList()) { QVariantMap map = variant.toMap(); + // Keep the last row for a timestamp if the backend returned duplicates. + deduplicatedEntries.insert(map.value("timestamp").toLongLong(), map); + } + + for (auto it = deduplicatedEntries.constBegin(); it != deduplicatedEntries.constEnd(); ++it) { + const QVariantMap &map = it.value(); QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong()); double consumption = map.value("consumption").toDouble(); double production = map.value("production").toDouble(); diff --git a/libnymea-app/energy/thingpowerlogs.cpp b/libnymea-app/energy/thingpowerlogs.cpp index d314d614..6e22d6d6 100644 --- a/libnymea-app/energy/thingpowerlogs.cpp +++ b/libnymea-app/energy/thingpowerlogs.cpp @@ -24,6 +24,8 @@ #include "thingpowerlogs.h" +#include +#include #include #include @@ -142,25 +144,37 @@ QVariantMap ThingPowerLogs::fetchParams() const QList ThingPowerLogs::unpackEntries(const QVariantMap ¶ms, double *minValue, double *maxValue) { + qint64 newestCurrentTimestamp = std::numeric_limits::min(); foreach (const QVariant &variant, params.value("currentEntries").toList()) { QVariantMap map = variant.toMap(); if (map.value("thingId").toUuid() != m_thingId) { continue; } + const qint64 timestamp = map.value("timestamp").toLongLong(); + if (timestamp < newestCurrentTimestamp) { + continue; + } + newestCurrentTimestamp = timestamp; if (m_liveEntry) { m_liveEntry->deleteLater(); } m_liveEntry = unpack(map); emit liveEntryChanged(m_liveEntry); - break; } QList ret; + QMap deduplicatedEntries; foreach (const QVariant &variant, params.value("thingPowerLogEntries").toList()) { QVariantMap map = variant.toMap(); if (map.value("thingId").toUuid() != m_thingId) { continue; } + // Keep the last row for a timestamp if the backend returned duplicates. + deduplicatedEntries.insert(map.value("timestamp").toLongLong(), map); + } + + for (auto it = deduplicatedEntries.constBegin(); it != deduplicatedEntries.constEnd(); ++it) { + const QVariantMap &map = it.value(); QDateTime timestamp = QDateTime::fromSecsSinceEpoch(map.value("timestamp").toLongLong()); QUuid thingId = map.value("thingId").toUuid(); double currentPower = map.value("currentPower").toDouble(); diff --git a/nymea-app/ui/mainviews/energy/ConsumerStats.qml b/nymea-app/ui/mainviews/energy/ConsumerStats.qml index e1d1a24f..804690fc 100644 --- a/nymea-app/ui/mainviews/energy/ConsumerStats.qml +++ b/nymea-app/ui/mainviews/energy/ConsumerStats.qml @@ -80,6 +80,14 @@ StatsBase { d.selectedThing = thing } } + + 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 + } } ThingPowerLogsLoader { @@ -133,22 +141,18 @@ StatsBase { // print("timestamp:", timestamp, "previous:", previousTimestamp) var entry = thingPowerLogs.find(timestamp) var previousEntry = thingPowerLogs.find(previousTimestamp); - if (timestamp < upcomingTimestamp && entry && (previousEntry || !d.loading)) { + var hasEntry = d.sampleMatches(entry, timestamp) + var hasPreviousEntry = d.sampleMatches(previousEntry, previousTimestamp) + if (timestamp < upcomingTimestamp && hasEntry && hasPreviousEntry) { // print("found entry:", entry.timestamp, previousEntry) - var consumption = entry.totalConsumption - if (previousEntry) { - consumption -= previousEntry.totalConsumption - } + var consumption = entry.totalConsumption - previousEntry.totalConsumption barSet.replace(i, consumption) valueAxis.adjustMax(consumption) - } else if (timestamp.getTime() == upcomingTimestamp.getTime() && (previousEntry || !d.loading) && thingPowerLogs.liveEntry()) { + } else if (timestamp.getTime() == upcomingTimestamp.getTime() && hasPreviousEntry && thingPowerLogs.liveEntry()) { var consumption = thingPowerLogs.liveEntry().totalConsumption // print("it's today for thing", thing.name, consumption, previousEntry) - if (previousEntry) { -// print("previous timestamp", previousEntry.timestamp, previousEntry.totalConsumption) - consumption -= previousEntry.totalConsumption - } + consumption -= previousEntry.totalConsumption barSet.replace(i, consumption) valueAxis.adjustMax(consumption) } else { diff --git a/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml b/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml index 17da03e5..1f442296 100644 --- a/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml +++ b/nymea-app/ui/mainviews/energy/PowerBalanceStats.qml @@ -79,6 +79,14 @@ StatsBase { } } + 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; @@ -90,25 +98,19 @@ StatsBase { 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 idx = powerBalanceLogs.indexOf(timestamp); - var entry = powerBalanceLogs.get(idx) -// print("timestamp:", timestamp, "previousTimestamp:", previousTimestamp) + var entry = powerBalanceLogs.find(timestamp) var previousEntry = powerBalanceLogs.find(previousTimestamp); - if (timestamp < upcomingTimestamp && entry && (previousEntry || !d.loading)) { -// print("found entry:", entry.timestamp, entry.totalConsumption) -// if (previousEntry) { -// print("found previous:", previousEntry.timestamp, previousEntry.totalConsumption) -// } + 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 - if (previousEntry) { - consumption -= previousEntry.totalConsumption - production -= previousEntry.totalProduction - acquisition -= previousEntry.totalAcquisition - returned -= previousEntry.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) @@ -117,18 +119,16 @@ StatsBase { valueAxis.adjustMax(production) valueAxis.adjustMax(acquisition) valueAxis.adjustMax(returned) - } else if (timestamp.getTime() == upcomingTimestamp.getTime() && (previousEntry || !d.loading)) { + } 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 - if (previousEntry) { - consumption -= previousEntry.totalConsumption - production -= previousEntry.totalProduction - acquisition -= previousEntry.totalAcquisition - returned -= previousEntry.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)