nymea-app/libnymea-app/energy/energylogs.cpp

495 lines
14 KiB
C++

// SPDX-License-Identifier: LGPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of libnymea-app.
*
* libnymea-app is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* libnymea-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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with libnymea-app. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "energylogs.h"
#include <QMetaEnum>
#include <QJsonDocument>
#include "logging.h"
NYMEA_LOGGING_CATEGORY(dcEnergyLogs, "EnergyLogs")
EnergyLogEntry::EnergyLogEntry(QObject *parent): QObject(parent)
{
}
EnergyLogEntry::EnergyLogEntry(const QDateTime &timestamp, QObject *parent):
QObject(parent),
m_timestamp(timestamp)
{
}
QDateTime EnergyLogEntry::timestamp() const
{
return m_timestamp;
}
EnergyLogs::EnergyLogs(QObject *parent) : QAbstractListModel(parent)
{
// Workaround for older Qt versions (5.12 and older) which can't deal with the QList<EnergyLogEntry*> argument
connect(this, &EnergyLogs::entriesAdded, this, [this](int index, const QList<EnergyLogEntry*> &entries){
emit entriesAddedIdx(index, entries.count());
});
}
EnergyLogs::~EnergyLogs()
{
if (m_engine) {
m_engine->jsonRpcClient()->unregisterNotificationHandler(this);
}
}
Engine *EnergyLogs::engine() const
{
return m_engine;
}
void EnergyLogs::setEngine(Engine *engine)
{
if (m_engine != engine) {
m_engine = engine;
emit engineChanged();
if (!m_engine) {
return;
}
connect(engine, &Engine::destroyed, this, [=](){
if (engine == m_engine) {
m_engine = nullptr;
emit engineChanged();
}
});
if (m_engine->jsonRpcClient()->experiences().value("Energy").toString() >= "1.0") {
m_engine->jsonRpcClient()->registerNotificationHandler(this, "Energy", "notificationReceivedInternal");
// if (m_ready && !m_loadingInhibited) {
// fetchLogs();
// }
}
}
}
EnergyLogs::SampleRate EnergyLogs::sampleRate() const
{
return m_sampleRate;
}
void EnergyLogs::setSampleRate(SampleRate sampleRate)
{
if (m_sampleRate != sampleRate) {
m_sampleRate = sampleRate;
emit sampleRateChanged();
clear();
}
}
QDateTime EnergyLogs::startTime() const
{
return m_startTime;
}
void EnergyLogs::setStartTime(const QDateTime &startTime)
{
if (m_startTime != startTime) {
m_startTime = startTime;
emit startTimeChanged();
}
}
QDateTime EnergyLogs::endTime() const
{
return m_endTime;
}
void EnergyLogs::setEndTime(const QDateTime &endTime)
{
if (m_endTime != endTime) {
m_endTime = endTime;
emit endTimeChanged();
}
}
bool EnergyLogs::live() const
{
return m_live;
}
void EnergyLogs::setLive(bool live)
{
if (m_live != live) {
m_live = live;
emit liveChanged();
}
}
bool EnergyLogs::fetchingData() const
{
return m_fetchingData;
}
bool EnergyLogs::loadingInhibited() const
{
return m_loadingInhibited;
}
void EnergyLogs::setLoadingInhibited(bool loadingInhibited)
{
if (m_loadingInhibited != loadingInhibited) {
m_loadingInhibited = loadingInhibited;
emit loadingInhibitedChanged();
// if (!m_loadingInhibited) {
// fetchLogs();
// }
}
}
void EnergyLogs::classBegin()
{
}
void EnergyLogs::componentComplete()
{
m_ready = true;
// fetchLogs();
}
int EnergyLogs::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return static_cast<int>(m_list.count());
}
QVariant EnergyLogs::data(const QModelIndex &index, int role) const
{
Q_UNUSED(index)
Q_UNUSED(role)
return QVariant();
}
double EnergyLogs::minValue() const
{
return m_minValue;
}
double EnergyLogs::maxValue() const
{
return m_maxValue;
}
EnergyLogEntry *EnergyLogs::get(int index) const
{
if (index < 0 || index >= m_list.count()) {
return nullptr;
}
return m_list.at(index);
}
int EnergyLogs::indexOf(const QDateTime &timestamp)
{
if (m_list.isEmpty()) {
return -1;
}
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;
}
int low = 0;
int high = m_list.count() - 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;
}
}
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 nextIndex;
}
EnergyLogEntry *EnergyLogs::find(const QDateTime &timestamp)
{
int index = indexOf(timestamp);
if (index < 0) {
return nullptr;
}
return m_list.at(index);
}
QList<EnergyLogEntry *> EnergyLogs::entries() const
{
return m_list;
}
void EnergyLogs::appendEntry(EnergyLogEntry *entry, double minValue, double maxValue)
{
entry->setParent(this);
int index = static_cast<int>(m_list.count());
beginInsertRows(QModelIndex(), index, index);
m_list.append(entry);
endInsertRows();
emit countChanged();
emit entryAdded(index, entry);
emit entriesAdded(index, {entry});
if (minValue < m_minValue) {
m_minValue = minValue;
emit minValueChanged();
}
if (maxValue > m_maxValue) {
m_maxValue = maxValue;
emit maxValueChanged();
}
}
void EnergyLogs::appendEntries(const QList<EnergyLogEntry *> &entries)
{
int index = static_cast<int>(m_list.count());
beginInsertRows(QModelIndex(), index, index + static_cast<int>(entries.count()));
for (int i = 0; i < entries.count(); i++) {
EnergyLogEntry* entry = entries.at(i);
entry->setParent(this);
m_list.append(entry);
emit entryAdded(index + i, entry);
}
endInsertRows();
emit entriesAdded(index, entries);
emit countChanged();
}
QVariantMap EnergyLogs::fetchParams() const
{
return QVariantMap();
}
void EnergyLogs::getLogsResponse(int commandId, const QVariantMap &params)
{
Q_UNUSED(commandId)
double minValue = 0, maxValue = 0;
qCDebug(dcEnergyLogs()) << "Logs response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
QList<EnergyLogEntry*> entries = unpackEntries(params, &minValue, &maxValue);
qCDebug(dcEnergyLogs()) << "Energy logs received" << entries.count();
if (!entries.isEmpty()) {
if (m_list.isEmpty()) {
// qCDebug(dcEnergyLogs()) << "Energy logs received" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
beginInsertRows(QModelIndex(), 0, static_cast<int>(entries.count()));
m_list.append(entries);
endInsertRows();
emit entriesAdded(0, entries);
m_minValue = minValue;
emit minValueChanged();
m_maxValue = maxValue;
emit maxValueChanged();
} else if (entries.first()->timestamp() < m_list.first()->timestamp()) {
if (entries.last()->timestamp().addSecs(m_sampleRate * 60) == m_list.first()->timestamp()) {
beginInsertRows(QModelIndex(), 0, static_cast<int>(entries.count()));
m_list = entries + m_list;
endInsertRows();
emit entriesAdded(0, entries);
if (minValue < m_minValue) {
m_minValue = minValue;
emit minValueChanged();
}
if (maxValue > m_maxValue) {
m_maxValue = maxValue;
emit maxValueChanged();
}
} else {
// End of fetched entries does not line up with start of existing entries. Discarding existing entries
qCDebug(dcEnergyLogs()) << "End of fetched entrie does not line up with start of existing entries. Discarding existing entries" << entries.last()->timestamp().addSecs(m_sampleRate * 60).toString() << " - " << m_list.first()->timestamp().toString();
clear();
// If the mismatch is in the visible area, we'll discard everything and fetch again
// Else if the mismatch is outside the visible area, we'll just discard the old data and work with what we received
if (entries.first()->timestamp() <= m_startTime && entries.last()->timestamp() >= m_endTime) {
beginInsertRows(QModelIndex(), 0, static_cast<int>(entries.count()));
m_list.append(entries);
endInsertRows();
emit entriesAdded(0, entries);
m_minValue = minValue;
emit minValueChanged();
m_maxValue = maxValue;
emit maxValueChanged();
} else {
qDeleteAll(entries);
fetchLogs();
}
}
} else if (entries.first()->timestamp().addSecs(-m_sampleRate * 60) == m_list.last()->timestamp()) {
int index = static_cast<int>(m_list.count());
beginInsertRows(QModelIndex(), index, index + static_cast<int>(entries.count()));
m_list.append(entries);
endInsertRows();
emit entriesAdded(index, entries);
if (minValue < m_minValue) {
m_minValue = minValue;
emit minValueChanged();
}
if (maxValue > m_maxValue) {
m_maxValue = maxValue;
emit maxValueChanged();
}
} else {
// Start of fetched entries does not line up with end of existing entries. Discarding existing entries
clear();
beginInsertRows(QModelIndex(), 0, static_cast<int>(entries.count()));
m_list.append(entries);
endInsertRows();
emit entriesAdded(0, entries);
m_minValue = minValue;
emit minValueChanged();
m_maxValue = maxValue;
emit maxValueChanged();
}
} else {
qCDebug(dcEnergyLogs()) << "Received empty log entries set.";
}
m_fetchingData = false;
if (m_fetchAgain) {
qCDebug(dcEnergyLogs()) << "Fetching again...";
m_fetchAgain = false;
fetchLogs();
} else {
emit fetchingDataChanged();
}
}
void EnergyLogs::notificationReceivedInternal(const QVariantMap &data)
{
if (!m_live) {
return;
}
if (!data.value("notification").toString().contains("Log")) {
return;
}
notificationReceived(data);
}
void EnergyLogs::clear()
{
int count = static_cast<int>(m_list.count());
beginResetModel();
foreach (EnergyLogEntry *entry, m_list)
entry->deleteLater();
m_list.clear();
endResetModel();
emit countChanged();
emit entriesRemoved(0, count);
m_minValue = 0;
emit minValueChanged();
m_maxValue = 0;
emit maxValueChanged();
}
void EnergyLogs::fetchLogs()
{
if (m_loadingInhibited || !m_ready || !m_engine || m_engine->jsonRpcClient()->experiences().value("Energy").toString() < "1.0") {
return;
}
if (m_fetchingData) {
qCDebug(dcEnergyLogs()) << "Already busy.. queing up call";
m_fetchAgain = true;
return;
}
QVariantMap params = fetchParams();
QMetaEnum metaEnum = QMetaEnum::fromType<SampleRate>();
params.insert("sampleRate", metaEnum.valueToKey(m_sampleRate));
if (!m_startTime.isNull() && !m_endTime.isNull()) {
QDateTime startTime;
QDateTime endTime;
QDateTime oldestExisting = m_list.count() > 0 ? m_list.first()->timestamp() : QDateTime();
QDateTime newestExisting = m_list.count() > 0 ? m_list.last()->timestamp() : QDateTime();
qCDebug(dcEnergyLogs()) << "request timeframe: " << m_startTime.toString() << " - " << m_endTime.toString();
qCDebug(dcEnergyLogs()) << "existing timeframe:" << oldestExisting.toString() << "- " << newestExisting.toString();
if (oldestExisting.isNull() || newestExisting.isNull()) {
startTime = m_startTime;
endTime = m_endTime;
} else {
if (m_startTime < oldestExisting) {
startTime = m_startTime;
endTime = qMin(m_endTime, oldestExisting.addSecs(-m_sampleRate * 60));
} else if (newestExisting < m_endTime) {
startTime = qMax(m_startTime, newestExisting.addSecs(m_sampleRate * 60));
endTime = m_endTime;
} else {
// Nothing to do...
return;
}
}
params.insert("from", startTime.toSecsSinceEpoch());
params.insert("to", endTime.toSecsSinceEpoch());
qCDebug(dcEnergyLogs()) << "Fetching from" << startTime.toString() << "to" << endTime.toString() << "with sample rate" << m_sampleRate;
}
m_fetchingData = true;
fetchingDataChanged();
qCDebug(dcEnergyLogs()) << "Fetching energy logs:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
m_engine->jsonRpcClient()->sendCommand("Energy.Get" + logsName(), params, this, "getLogsResponse");
}