// 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 . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "energylogs.h" #include #include #include "logging.h" NYMEA_LOGGING_CATEGORY(dcEnergyLogs, "EnergyLogs") EnergyLogEntry::EnergyLogEntry(QObject *parent): QObject(parent) { } EnergyLogEntry::EnergyLogEntry(const QDateTime ×tamp, 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 argument connect(this, &EnergyLogs::entriesAdded, this, [this](int index, const QList &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(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 ×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(); return -1; } qCDebug(dcEnergyLogs()) << "finding:" << timestamp << index << first.toString() << m_list.at(index)->timestamp(); // 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; } if (diffToNext < diffToFound) { // qWarning() << "Correcting to next" << index << m_list.count() << found << next << diffToNext << diffToFound; return index + 1; } return index; } EnergyLogEntry *EnergyLogs::find(const QDateTime ×tamp) { int index = indexOf(timestamp); if (index < 0) { return nullptr; } return m_list.at(index); } QList EnergyLogs::entries() const { return m_list; } void EnergyLogs::appendEntry(EnergyLogEntry *entry, double minValue, double maxValue) { entry->setParent(this); int index = static_cast(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 &entries) { int index = static_cast(m_list.count()); beginInsertRows(QModelIndex(), index, index + static_cast(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 ¶ms) { Q_UNUSED(commandId) double minValue = 0, maxValue = 0; qCDebug(dcEnergyLogs()) << "Logs response:" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); QList 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(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(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(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(m_list.count()); beginInsertRows(QModelIndex(), index, index + static_cast(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(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(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(); 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"); }