503 lines
15 KiB
C++
503 lines
15 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 "newlogsmodel.h"
|
|
|
|
#include "engine.h"
|
|
#include "logmanager.h"
|
|
|
|
#include "logging.h"
|
|
//NYMEA_LOGGING_CATEGORY(dcLogEngine, "LogEngine")
|
|
Q_DECLARE_LOGGING_CATEGORY(dcLogEngine)
|
|
|
|
#include <QJsonDocument>
|
|
#include <QMetaEnum>
|
|
|
|
NewLogsModel::NewLogsModel(QObject *parent)
|
|
: QAbstractListModel{parent}
|
|
{
|
|
// Workaround for older Qt versions (5.12 and older) which can't deal with the QList<EnergyLogEntry*> argument
|
|
connect(this, &NewLogsModel::entriesAdded, this, [this](int index, const QList<NewLogEntry*> &entries){
|
|
emit entriesAddedIdx(index, entries.count());
|
|
});
|
|
|
|
}
|
|
|
|
int NewLogsModel::rowCount(const QModelIndex &parent) const
|
|
{
|
|
Q_UNUSED(parent)
|
|
return static_cast<int>(m_list.count());
|
|
}
|
|
|
|
QVariant NewLogsModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
switch (role) {
|
|
case RoleSource:
|
|
return m_list.at(index.row())->source();
|
|
case RoleTimestamp:
|
|
return m_list.at(index.row())->timestamp();
|
|
case RoleValues:
|
|
return m_list.at(index.row())->values();
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
QHash<int, QByteArray> NewLogsModel::roleNames() const
|
|
{
|
|
return {
|
|
{RoleSource, "source"},
|
|
{RoleTimestamp, "timestamp"},
|
|
{RoleValues, "values"}
|
|
};
|
|
}
|
|
|
|
void NewLogsModel::classBegin()
|
|
{
|
|
|
|
}
|
|
|
|
void NewLogsModel::componentComplete()
|
|
{
|
|
m_completed = true;
|
|
// fetchMore();
|
|
}
|
|
|
|
bool NewLogsModel::canFetchMore(const QModelIndex &parent) const
|
|
{
|
|
Q_UNUSED(parent)
|
|
// Cannot fetchMore when there are multiple sources as paging doesn't really work in that case
|
|
return m_canFetchMore && (m_sources.count() == 1 || m_list.isEmpty());
|
|
}
|
|
|
|
void NewLogsModel::fetchMore(const QModelIndex &parent)
|
|
{
|
|
Q_UNUSED(parent)
|
|
|
|
if (!m_engine) {
|
|
return;
|
|
}
|
|
if (!m_completed) {
|
|
return;
|
|
}
|
|
|
|
fetchLogs();
|
|
|
|
}
|
|
|
|
Engine *NewLogsModel::engine() const
|
|
{
|
|
return m_engine;
|
|
}
|
|
|
|
void NewLogsModel::setEngine(Engine *engine)
|
|
{
|
|
if (m_engine == engine) {
|
|
return;
|
|
}
|
|
|
|
if (m_engine) {
|
|
disconnect(m_engine->logManager(), &LogManager::logEntryReceived, this, &NewLogsModel::newLogEntryReceived);
|
|
}
|
|
|
|
m_engine = engine;
|
|
emit engineChanged();
|
|
|
|
if (m_engine) {
|
|
connect(m_engine->logManager(), &LogManager::logEntryReceived, this, &NewLogsModel::newLogEntryReceived);
|
|
}
|
|
}
|
|
|
|
QString NewLogsModel::source() const
|
|
{
|
|
return m_sources.count() > 0 ? m_sources.first() : "";
|
|
}
|
|
|
|
void NewLogsModel::setSource(const QString &source)
|
|
{
|
|
if (m_sources != QStringList(source)) {
|
|
m_sources = QStringList(source);
|
|
emit sourcesChanged();
|
|
}
|
|
}
|
|
|
|
QStringList NewLogsModel::sources() const
|
|
{
|
|
return m_sources;
|
|
}
|
|
|
|
void NewLogsModel::setSources(const QStringList &sources)
|
|
{
|
|
if (m_sources != sources) {
|
|
m_sources = sources;
|
|
emit sourcesChanged();
|
|
}
|
|
}
|
|
|
|
QStringList NewLogsModel::columns() const
|
|
{
|
|
return m_columns;
|
|
}
|
|
|
|
void NewLogsModel::setColumns(const QStringList &columns)
|
|
{
|
|
if (m_columns != columns) {
|
|
m_columns = columns;
|
|
emit columnsChanged();
|
|
}
|
|
}
|
|
|
|
QVariantMap NewLogsModel::filter() const
|
|
{
|
|
return m_filter;
|
|
}
|
|
|
|
void NewLogsModel::setFilter(const QVariantMap &filter)
|
|
{
|
|
if (m_filter != filter) {
|
|
m_filter = filter;
|
|
emit filterChanged();
|
|
}
|
|
}
|
|
|
|
QDateTime NewLogsModel::startTime() const
|
|
{
|
|
return m_startTime;
|
|
}
|
|
|
|
void NewLogsModel::setStartTime(const QDateTime &startTime)
|
|
{
|
|
if (m_startTime != startTime) {
|
|
m_startTime = startTime;
|
|
emit startTimeChanged();
|
|
}
|
|
}
|
|
|
|
QDateTime NewLogsModel::endTime() const
|
|
{
|
|
return m_endTime;
|
|
}
|
|
|
|
void NewLogsModel::setEndTime(const QDateTime &endTime)
|
|
{
|
|
if (m_endTime != endTime) {
|
|
m_endTime = endTime;
|
|
emit endTimeChanged();
|
|
}
|
|
}
|
|
|
|
NewLogsModel::SampleRate NewLogsModel::sampleRate() const
|
|
{
|
|
return m_sampleRate;
|
|
}
|
|
|
|
void NewLogsModel::setSampleRate(SampleRate sampleRate)
|
|
{
|
|
if (m_sampleRate != sampleRate) {
|
|
m_sampleRate = sampleRate;
|
|
emit sampleRateChanged();
|
|
clear();
|
|
}
|
|
}
|
|
|
|
Qt::SortOrder NewLogsModel::sortOrder() const
|
|
{
|
|
return m_sortOrder;
|
|
}
|
|
|
|
void NewLogsModel::setSortOrder(Qt::SortOrder sortOrder)
|
|
{
|
|
if (m_sortOrder != sortOrder) {
|
|
m_sortOrder = sortOrder;
|
|
emit sortOrderChanged();
|
|
}
|
|
}
|
|
|
|
bool NewLogsModel::busy() const
|
|
{
|
|
return m_busy;
|
|
}
|
|
|
|
bool NewLogsModel::live() const
|
|
{
|
|
return m_live;
|
|
}
|
|
|
|
void NewLogsModel::setLive(bool live)
|
|
{
|
|
if (m_live != live) {
|
|
m_live = live;
|
|
emit liveChanged();
|
|
}
|
|
}
|
|
|
|
int NewLogsModel::fetchBlockSize() const
|
|
{
|
|
return m_blockSize;
|
|
}
|
|
|
|
void NewLogsModel::setFetchBlockSize(int fetchBlockSize)
|
|
{
|
|
if (m_blockSize != fetchBlockSize) {
|
|
m_blockSize = fetchBlockSize;
|
|
emit fetchBlockSizeChanged();
|
|
}
|
|
}
|
|
|
|
NewLogEntry *NewLogsModel::get(int index) const
|
|
{
|
|
if (index < 0 || index >= m_list.count()) {
|
|
return nullptr;
|
|
}
|
|
return m_list.at(index);
|
|
}
|
|
|
|
NewLogEntry *NewLogsModel::find(const QDateTime ×tamp) const
|
|
{
|
|
// qCDebug(dcLogEngine()) << "finding:" << timestamp.toString();
|
|
if (m_list.isEmpty()) {
|
|
return nullptr;
|
|
}
|
|
int idx = static_cast<int>(m_list.count() / 2);
|
|
int jump = static_cast<int>(m_list.count() / 4);
|
|
int stopper = 10;
|
|
while (stopper-- > 0) {
|
|
// qCDebug(dcLogEngine()) << "idx:" << idx << "cnt:" << m_list.count() << "jmp" << jump;
|
|
NewLogEntry *entry = m_list.at(idx);
|
|
if (entry->timestamp() == timestamp) {
|
|
// qCDebug(dcLogEngine()) << "found exact";
|
|
return entry;
|
|
}
|
|
qint64 diff = timestamp.msecsTo(entry->timestamp());
|
|
if (m_sortOrder == Qt::AscendingOrder) {
|
|
if (entry->timestamp() > timestamp) {
|
|
// qCDebug(dcLogEngine()) << "entry is newer than searched:" << entry->timestamp().toString() << timestamp.toString();
|
|
if (idx == 0) {
|
|
// qCDebug(dcLogEngine()) << "Is oldest.";
|
|
return entry;
|
|
}
|
|
NewLogEntry *previousEntry = m_list.at(idx-1);
|
|
if (previousEntry->timestamp() < timestamp) {
|
|
qint64 previousDiff = timestamp.msecsTo(previousEntry->timestamp());
|
|
// qCDebug(dcLogEngine()) << "time between this and previous:" << entry->timestamp().toString() << previousEntry->timestamp().toString() << (qAbs(previousDiff) < qAbs(diff) ? "next" : "this");
|
|
return qAbs(previousDiff) < qAbs(diff) ? previousEntry : entry;
|
|
}
|
|
idx -= jump;
|
|
} else if (entry->timestamp() < timestamp) {
|
|
// qCDebug(dcLogEngine()) << "entry is older than searched:" << entry->timestamp().toString() << timestamp.toString();
|
|
if (idx == m_list.count() - 1) {
|
|
// qCDebug(dcLogEngine()) << "Is newest.";
|
|
return entry;
|
|
}
|
|
NewLogEntry *nextEntry = m_list.at(idx+1);
|
|
if (nextEntry->timestamp() > timestamp) {
|
|
qint64 nextDiff = timestamp.msecsTo(nextEntry->timestamp());
|
|
// qCDebug(dcLogEngine()) << "time between next and this:" << nextEntry->timestamp().toString() << "-" << entry->timestamp().toString() << (qAbs(nextDiff) > qAbs(diff) ? "prev" : "this");
|
|
return qAbs(nextDiff) < qAbs(diff) ? nextEntry : entry;
|
|
}
|
|
idx += jump;
|
|
}
|
|
} else {
|
|
if (entry->timestamp() > timestamp) {
|
|
// qCDebug(dcLogEngine()) << "entry is newer than searched:" << entry->timestamp().toString() << timestamp.toString();
|
|
if (idx == m_list.count() - 1) {
|
|
// qCDebug(dcLogEngine()) << "Is newest.";
|
|
return entry;
|
|
}
|
|
NewLogEntry *previousEntry = m_list.at(idx+1);
|
|
// qCDebug(dcLogEngine) << "previous:" << previousEntry->timestamp().toString();
|
|
if (previousEntry->timestamp() < timestamp) {
|
|
qint64 previousDiff = timestamp.msecsTo(previousEntry->timestamp());
|
|
// qCDebug(dcLogEngine()) << "time between previous and this:" << previousEntry->timestamp().toString() << entry->timestamp().toString() << previousDiff;
|
|
return qAbs(previousDiff) < qAbs(diff) ? previousEntry : entry;
|
|
}
|
|
idx += jump;
|
|
} else if (entry->timestamp() < timestamp) {
|
|
// qCDebug(dcLogEngine()) << "entry is older than searched:" << entry->timestamp().toString() << timestamp.toString();
|
|
if (idx == 0) {
|
|
// qCDebug(dcLogEngine()) << "Is oldest.";
|
|
return entry;
|
|
}
|
|
NewLogEntry *nextEntry = m_list.at(idx-1);
|
|
// qCDebug(dcLogEngine) << "next:" << nextEntry;
|
|
if (nextEntry->timestamp() > timestamp) {
|
|
qint64 nextDiff = timestamp.msecsTo(nextEntry->timestamp());
|
|
// qCDebug(dcLogEngine()) << "time between this and next:" << entry->timestamp().toString() << nextEntry->timestamp().toString() << nextDiff;
|
|
return qAbs(nextDiff) < qAbs(diff) ? nextEntry : entry;
|
|
}
|
|
idx -= jump;
|
|
}
|
|
}
|
|
jump = qMax(1, jump / 2);
|
|
};
|
|
return nullptr;
|
|
}
|
|
|
|
void NewLogsModel::clear()
|
|
{
|
|
int count = static_cast<int>(m_list.count());
|
|
beginResetModel();
|
|
foreach (NewLogEntry *entry, m_list)
|
|
entry->deleteLater();
|
|
|
|
m_list.clear();
|
|
m_currentNewest = QDateTime();
|
|
m_lastOffset = 0;
|
|
endResetModel();
|
|
emit countChanged();
|
|
emit entriesRemoved(0, count);
|
|
}
|
|
|
|
void NewLogsModel::fetchLogs()
|
|
{
|
|
if (!m_engine) {
|
|
return;
|
|
}
|
|
QVariantMap params {
|
|
{"sources", m_sources},
|
|
{"columns", m_columns},
|
|
{"filter", m_filter}
|
|
};
|
|
|
|
|
|
if (m_sampleRate == SampleRateAny) { // Discrete logs
|
|
|
|
if (!m_startTime.isNull() && !m_endTime.isNull()) { // Either specific time frame
|
|
params.insert("startTime", m_startTime.toMSecsSinceEpoch());
|
|
params.insert("endTime", m_endTime.toMSecsSinceEpoch());
|
|
|
|
} else {
|
|
params.insert("limit", m_blockSize);
|
|
if (m_list.count() > 0) {
|
|
if (m_currentNewest.isNull()) {
|
|
m_currentNewest = QDateTime::currentDateTime();
|
|
}
|
|
params.insert("offset", m_lastOffset);
|
|
params.insert("endTime", m_currentNewest.toMSecsSinceEpoch());
|
|
}
|
|
m_lastOffset += m_blockSize;
|
|
}
|
|
|
|
} else {
|
|
if (!m_startTime.isNull() && !m_endTime.isNull()) {
|
|
params.insert("startTime", m_startTime.toMSecsSinceEpoch());
|
|
params.insert("endTime", m_endTime.toMSecsSinceEpoch());
|
|
|
|
QMetaEnum sampleRateEnum = QMetaEnum::fromType<SampleRate>();
|
|
params.insert("sampleRate", sampleRateEnum.valueToKey(m_sampleRate));
|
|
} else {
|
|
qCWarning(dcLogEngine()) << "startTime and endTime is required when asking for resampling";
|
|
return;
|
|
}
|
|
}
|
|
|
|
QMetaEnum sortOrderEnum = QMetaEnum::fromType<Qt::SortOrder>();
|
|
params.insert("sortOrder", sortOrderEnum.valueToKey(m_sortOrder));
|
|
|
|
qCDebug(dcLogEngine()) << "Fetching logs:" << QJsonDocument::fromVariant(params).toJson();
|
|
m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "logsReply");
|
|
|
|
m_busy = true;
|
|
emit busyChanged();
|
|
}
|
|
|
|
void NewLogsModel::logsReply(int commandId, const QVariantMap &data)
|
|
{
|
|
Q_UNUSED(commandId)
|
|
|
|
m_busy = false;
|
|
emit busyChanged();
|
|
|
|
QList<NewLogEntry*> entries;
|
|
foreach (const QVariant &entryVariant, data.value("logEntries").toList()) {
|
|
QVariantMap map = entryVariant.toMap();
|
|
QString source = map.value("source").toString();
|
|
QDateTime timestamp = QDateTime::fromMSecsSinceEpoch(map.value("timestamp").toULongLong());
|
|
QVariantMap values = map.value("values").toMap();
|
|
NewLogEntry *entry = new NewLogEntry(source, timestamp, values, this);
|
|
entries.append(entry);
|
|
qCDebug(dcLogEngine()) << "Log entry:" << entry->timestamp() << entry->values();;
|
|
}
|
|
|
|
m_canFetchMore = entries.count() >= m_blockSize;
|
|
qCDebug(dcLogEngine()) << "Logs received:" << entries.count();
|
|
|
|
if (!m_startTime.isNull() && !m_endTime.isNull()) {
|
|
beginResetModel();
|
|
QList<NewLogEntry*> oldEntries = m_list;
|
|
m_list.clear();
|
|
endResetModel();
|
|
emit entriesRemoved(0, oldEntries.count());
|
|
|
|
foreach (NewLogEntry *entry, oldEntries)
|
|
entry->deleteLater();
|
|
|
|
if (!entries.isEmpty()) {
|
|
beginInsertRows(QModelIndex(), 0, static_cast<int>(entries.count()) - 1);
|
|
m_list = entries;
|
|
endInsertRows();
|
|
}
|
|
emit entriesAdded(0, entries);
|
|
emit countChanged();
|
|
|
|
} else {
|
|
if (!entries.isEmpty()) {
|
|
beginInsertRows(QModelIndex(), static_cast<int>(m_list.count()), static_cast<int>(m_list.count()) + static_cast<int>(entries.count()) - 1);
|
|
std::sort(entries.begin(), entries.end(), [](NewLogEntry *left, NewLogEntry *right){
|
|
return left->timestamp() > right->timestamp();
|
|
});
|
|
m_list.append(entries);
|
|
endInsertRows();
|
|
}
|
|
emit entriesAdded(m_list.count() - entries.count(), entries);
|
|
emit countChanged();
|
|
}
|
|
|
|
}
|
|
|
|
void NewLogsModel::newLogEntryReceived(const QVariantMap &map)
|
|
{
|
|
QString source = map.value("source").toString();
|
|
QDateTime timestamp = QDateTime::fromMSecsSinceEpoch(map.value("timestamp").toULongLong());
|
|
QVariantMap values = map.value("values").toMap();
|
|
|
|
|
|
if (m_sources.contains(source) && m_sampleRate == SampleRateAny) {
|
|
qCritical() << "New entry!" << m_sources << source << m_sampleRate;
|
|
NewLogEntry *entry = new NewLogEntry(source, timestamp, values, this);
|
|
if (m_sortOrder == Qt::DescendingOrder) {
|
|
beginInsertRows(QModelIndex(), 0, 0);
|
|
m_list.prepend(entry);
|
|
endInsertRows();
|
|
emit entriesAdded(0, {entry});
|
|
} else {
|
|
beginInsertRows(QModelIndex(), static_cast<int>(m_list.count()), static_cast<int>(m_list.count()));
|
|
m_list.append(entry);
|
|
endInsertRows();
|
|
emit entriesAdded(m_list.count() - 1, {entry});
|
|
}
|
|
emit countChanged();
|
|
}
|
|
}
|