Add support for the new log engine (protocol version 8.0)
This commit is contained in:
parent
2e4f306c70
commit
6845d7d2ae
@ -691,7 +691,7 @@ void JsonRpcClient::helloReply(int /*commandId*/, const QVariantMap ¶ms)
|
||||
m_connection->currentHost()->setName(name);
|
||||
|
||||
QVersionNumber minimumRequiredVersion = QVersionNumber(5, 0);
|
||||
QVersionNumber maximumMajorVersion = QVersionNumber(7);
|
||||
QVersionNumber maximumMajorVersion = QVersionNumber(8);
|
||||
if (m_jsonRpcVersion < minimumRequiredVersion) {
|
||||
qCWarning(dcJsonRpc()) << "Nymea core doesn't support minimum required version. Required:" << minimumRequiredVersion << "Found:" << m_jsonRpcVersion;
|
||||
emit invalidMinimumVersion(m_jsonRpcVersion.toString(), minimumRequiredVersion.toString());
|
||||
|
||||
@ -68,6 +68,7 @@
|
||||
#include "models/barseriesadapter.h"
|
||||
#include "models/xyseriesadapter.h"
|
||||
#include "models/boolseriesadapter.h"
|
||||
#include "models/newlogsmodel.h"
|
||||
#include "models/interfacesproxy.h"
|
||||
#include "configuration/nymeaconfiguration.h"
|
||||
#include "configuration/serverconfiguration.h"
|
||||
@ -278,6 +279,9 @@ void registerQmlTypes() {
|
||||
qmlRegisterType<XYSeriesAdapter>(uri, 1, 0, "XYSeriesAdapter");
|
||||
qmlRegisterType<BoolSeriesAdapter>(uri, 1, 0, "BoolSeriesAdapter");
|
||||
|
||||
qmlRegisterType<NewLogsModel>(uri, 1, 0, "NewLogsModel");
|
||||
qmlRegisterUncreatableType<NewLogEntry>(uri, 1, 0, "NewLogEntry", "Get them from NewLogsModel");
|
||||
|
||||
qmlRegisterUncreatableType<TagsManager>(uri, 1, 0, "TagsManager", "Get it from Engine");
|
||||
qmlRegisterUncreatableType<Tags>(uri, 1, 0, "Tags", "Get it from TagsManager");
|
||||
qmlRegisterUncreatableType<Tag>(uri, 1, 0, "Tag", "Get it from Tags");
|
||||
|
||||
@ -28,6 +28,8 @@ SOURCES += \
|
||||
$$PWD/energy/thingpowerlogs.cpp \
|
||||
$$PWD/connection/tunnelproxytransport.cpp \
|
||||
$$PWD/models/boolseriesadapter.cpp \
|
||||
$$PWD/models/newlogentry.cpp \
|
||||
$$PWD/models/newlogsmodel.cpp \
|
||||
$$PWD/models/scriptsproxymodel.cpp \
|
||||
$$PWD/pluginconfigmanager.cpp \
|
||||
$$PWD/tagwatcher.cpp \
|
||||
@ -192,6 +194,8 @@ HEADERS += \
|
||||
$$PWD/energy/thingpowerlogs.h \
|
||||
$$PWD/connection/tunnelproxytransport.h \
|
||||
$$PWD/models/boolseriesadapter.h \
|
||||
$$PWD/models/newlogentry.h \
|
||||
$$PWD/models/newlogsmodel.h \
|
||||
$$PWD/models/scriptsproxymodel.h \
|
||||
$$PWD/pluginconfigmanager.h \
|
||||
$$PWD/tagwatcher.h \
|
||||
|
||||
25
libnymea-app/models/newlogentry.cpp
Normal file
25
libnymea-app/models/newlogentry.cpp
Normal file
@ -0,0 +1,25 @@
|
||||
#include "newlogentry.h"
|
||||
|
||||
NewLogEntry::NewLogEntry(const QString &source, const QDateTime ×tamp, const QVariantMap &values, QObject *parent)
|
||||
: QObject{parent},
|
||||
m_source(source),
|
||||
m_timestamp(timestamp),
|
||||
m_values(values)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QString NewLogEntry::source() const
|
||||
{
|
||||
return m_source;
|
||||
}
|
||||
|
||||
QDateTime NewLogEntry::timestamp() const
|
||||
{
|
||||
return m_timestamp;
|
||||
}
|
||||
|
||||
QVariantMap NewLogEntry::values() const
|
||||
{
|
||||
return m_values;
|
||||
}
|
||||
28
libnymea-app/models/newlogentry.h
Normal file
28
libnymea-app/models/newlogentry.h
Normal file
@ -0,0 +1,28 @@
|
||||
#ifndef NEWLOGENTRY_H
|
||||
#define NEWLOGENTRY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QVariant>
|
||||
|
||||
class NewLogEntry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QString source READ source CONSTANT)
|
||||
Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT)
|
||||
Q_PROPERTY(QVariantMap values READ values CONSTANT)
|
||||
|
||||
public:
|
||||
explicit NewLogEntry(const QString &source, const QDateTime ×tamp, const QVariantMap &values, QObject *parent = nullptr);
|
||||
|
||||
QString source() const;
|
||||
QDateTime timestamp() const;
|
||||
QVariantMap values() const;
|
||||
|
||||
private:
|
||||
QString m_source;
|
||||
QDateTime m_timestamp;
|
||||
QVariantMap m_values;
|
||||
};
|
||||
|
||||
#endif // NEWLOGENTRY_H
|
||||
406
libnymea-app/models/newlogsmodel.cpp
Normal file
406
libnymea-app/models/newlogsmodel.cpp
Normal file
@ -0,0 +1,406 @@
|
||||
#include "newlogsmodel.h"
|
||||
|
||||
#include "engine.h"
|
||||
|
||||
#include "logging.h"
|
||||
//NYMEA_LOGGING_CATEGORY(dcLogEngine, "LogEngine")
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcLogEngine)
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QMetaEnum>
|
||||
|
||||
NewLogsModel::NewLogsModel(QObject *parent)
|
||||
: QAbstractListModel{parent}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
int NewLogsModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return 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)
|
||||
return m_canFetchMore;
|
||||
}
|
||||
|
||||
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) {
|
||||
m_engine = engine;
|
||||
emit engineChanged();
|
||||
|
||||
// if (m_completed && m_canFetchMore) {
|
||||
// fetchMore();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
bool NewLogsModel::busy() const
|
||||
{
|
||||
return m_busy;
|
||||
}
|
||||
|
||||
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 = m_list.count() / 2;
|
||||
int jump = 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 (entry->timestamp() > timestamp) {
|
||||
// qCDebug(dcLogEngine()) << "entry is newer than searched:" << entry->timestamp().toString() << timestamp.toString();
|
||||
if (idx == m_list.count() - 1) {
|
||||
// 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 == 0) {
|
||||
// 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;
|
||||
}
|
||||
jump = qMax(1, jump / 2);
|
||||
};
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void NewLogsModel::clear()
|
||||
{
|
||||
int count = m_list.count();
|
||||
beginResetModel();
|
||||
qDeleteAll(m_list);
|
||||
m_list.clear();
|
||||
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_startTime.isNull() && !m_endTime.isNull()) {
|
||||
QDateTime startTime;
|
||||
QDateTime endTime;
|
||||
|
||||
QDateTime oldestExisting = m_list.count() > 0 ? m_list.last()->timestamp() : QDateTime();
|
||||
QDateTime newestExisting = m_list.count() > 0 ? m_list.first()->timestamp() : QDateTime();
|
||||
qCDebug(dcLogEngine()) << "request timeframe: " << m_startTime.toString() << " - " << m_endTime.toString();
|
||||
qCDebug(dcLogEngine()) << "existing timeframe:" << oldestExisting.toString() << "- " << newestExisting.toString();
|
||||
|
||||
if (oldestExisting.isNull() || newestExisting.isNull()) {
|
||||
startTime = m_startTime;
|
||||
endTime = qMin(QDateTime::currentDateTime(), m_endTime);
|
||||
} else {
|
||||
|
||||
if (m_startTime < oldestExisting) {
|
||||
startTime = m_startTime;
|
||||
endTime = qMin(QDateTime::currentDateTime(), qMin(m_endTime, oldestExisting));
|
||||
} else if (newestExisting < m_endTime) {
|
||||
startTime = qMax(m_startTime, newestExisting);
|
||||
endTime = qMin(QDateTime::currentDateTime(), m_endTime);
|
||||
} else {
|
||||
// Nothing to do...
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(dcLogEngine()) << "Actual request:" << startTime.toString() << " - " << endTime.toString();
|
||||
params.insert("startTime", startTime.toMSecsSinceEpoch());
|
||||
params.insert("endTime", endTime.toMSecsSinceEpoch());
|
||||
QMetaEnum sampleRateEnum = QMetaEnum::fromType<SampleRate>();
|
||||
params.insert("sampleRate", sampleRateEnum.valueToKey(m_sampleRate));
|
||||
} else {
|
||||
params.insert("limit", m_blockSize);
|
||||
if (m_list.count() > 0) {
|
||||
params.insert("offset", m_list.count() - 1); // -1 because we'll fetch the last existing one again as the receiving logic checks if timestamps line up for proper insertion. It will be removed again there
|
||||
params.insert("endTime", m_list.first()->timestamp().toMSecsSinceEpoch());
|
||||
}
|
||||
}
|
||||
|
||||
// if (!m_startTime.isNull()) {
|
||||
// params.insert("startTime", m_startTime.toMSecsSinceEpoch());
|
||||
// }
|
||||
// if (!m_endTime.isNull()) {
|
||||
// params.insert("endTime", m_endTime.toMSecsSinceEpoch());
|
||||
// }
|
||||
qCDebug(dcLogEngine()) << "Fetching logs:" << QJsonDocument::fromVariant(params).toJson();
|
||||
m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "logsReply");
|
||||
}
|
||||
|
||||
void NewLogsModel::logsReply(int commandId, const QVariantMap &data)
|
||||
{
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
m_canFetchMore = entries.count() >= m_blockSize;
|
||||
qCDebug(dcLogEngine()) << "Logs received:" << entries.count() << "Requested:" << m_blockSize;
|
||||
|
||||
if (!entries.isEmpty()) {
|
||||
qCDebug(dcLogEngine()) << "Logs received:" << entries.first()->timestamp().toString() << " - " << entries.last()->timestamp().toString();
|
||||
if (m_list.isEmpty()) {
|
||||
qCDebug(dcLogEngine()) << "Inserting into emptry model";
|
||||
beginInsertRows(QModelIndex(), 0, entries.count() - 1);
|
||||
m_list.append(entries);
|
||||
endInsertRows();
|
||||
emit entriesAdded(0, entries);
|
||||
|
||||
} else if (entries.last()->timestamp() == m_list.last()->timestamp()) {
|
||||
qCDebug(dcLogEngine()) << "First item of new list already existing... no new data...";
|
||||
qDeleteAll(entries);
|
||||
|
||||
} else if (entries.last()->timestamp() < m_list.last()->timestamp()) {
|
||||
if (entries.first()->timestamp() == m_list.last()->timestamp()) {
|
||||
qCDebug(dcLogEngine()) << "Appending received items";
|
||||
beginRemoveRows(QModelIndex(), m_list.count() - 1, m_list.count() - 1);
|
||||
m_list.takeLast()->deleteLater();
|
||||
endRemoveRows();
|
||||
emit entriesRemoved(m_list.count(), 1);
|
||||
|
||||
int insertIdx = m_list.count();
|
||||
beginInsertRows(QModelIndex(), insertIdx, insertIdx + entries.count() - 1);
|
||||
m_list = m_list + entries;
|
||||
endInsertRows();
|
||||
emit entriesAdded(insertIdx, entries);
|
||||
} else {
|
||||
// Start of fetched entries does not line up with end of existing entries. Discarding existing entries
|
||||
qCDebug(dcLogEngine()) << "Start of fetched entries does not line up with end of existing entries. Discarding existing entries" << entries.first()->timestamp().toString() << " - " << m_list.last()->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_endTime && entries.last()->timestamp() >= m_endTime)
|
||||
|| (entries.first()->timestamp() <= m_startTime && entries.last()->timestamp() <= m_endTime)) {
|
||||
clear();
|
||||
beginInsertRows(QModelIndex(), 0, entries.count() - 1);
|
||||
m_list.append(entries);
|
||||
endInsertRows();
|
||||
emit entriesAdded(0, entries);
|
||||
} else {
|
||||
clear();
|
||||
fetchLogs();
|
||||
}
|
||||
}
|
||||
|
||||
} else if (entries.last()->timestamp() == m_list.first()->timestamp()) {
|
||||
beginRemoveRows(QModelIndex(), 0, 0);
|
||||
m_list.takeAt(0)->deleteLater();
|
||||
endRemoveRows();
|
||||
emit entriesRemoved(0, 1);
|
||||
qCDebug(dcLogEngine()) << "Prepending received items";
|
||||
beginInsertRows(QModelIndex(), 0, entries.count() - 1);
|
||||
m_list = entries + m_list;
|
||||
endInsertRows();
|
||||
emit entriesAdded(0, entries);
|
||||
|
||||
} else {
|
||||
// End of fetched entries does not line up with start of existing entries. Discarding existing entries
|
||||
qCDebug(dcLogEngine()) << "End of fetched entries does not line up with start of existing entries" << m_list.last()->timestamp().toString() << " - " << m_list.first()->timestamp().toString();
|
||||
clear();
|
||||
beginInsertRows(QModelIndex(), 0, entries.count() - 1);
|
||||
m_list.append(entries);
|
||||
endInsertRows();
|
||||
emit entriesAdded(0, entries);
|
||||
}
|
||||
}
|
||||
|
||||
emit countChanged();
|
||||
}
|
||||
128
libnymea-app/models/newlogsmodel.h
Normal file
128
libnymea-app/models/newlogsmodel.h
Normal file
@ -0,0 +1,128 @@
|
||||
#ifndef NEWLOGSMODEL_H
|
||||
#define NEWLOGSMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QQmlParserStatus>
|
||||
#include "newlogentry.h"
|
||||
|
||||
class Engine;
|
||||
|
||||
class NewLogsModel : public QAbstractListModel, public QQmlParserStatus
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_INTERFACES(QQmlParserStatus)
|
||||
Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged)
|
||||
Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourcesChanged)
|
||||
Q_PROPERTY(QStringList sources READ sources WRITE setSources NOTIFY sourcesChanged)
|
||||
Q_PROPERTY(QStringList columns READ columns WRITE setColumns NOTIFY columnsChanged)
|
||||
Q_PROPERTY(QVariantMap filter READ filter WRITE setFilter NOTIFY filterChanged)
|
||||
Q_PROPERTY(QDateTime startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged)
|
||||
Q_PROPERTY(QDateTime endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged)
|
||||
Q_PROPERTY(SampleRate sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged)
|
||||
|
||||
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||
// Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged)
|
||||
|
||||
public:
|
||||
enum Role {
|
||||
RoleSource,
|
||||
RoleTimestamp,
|
||||
RoleValues
|
||||
};
|
||||
Q_ENUM(Role)
|
||||
|
||||
enum SampleRate {
|
||||
SampleRateAny = 0,
|
||||
SampleRate1Min = 1,
|
||||
SampleRate15Mins = 15,
|
||||
SampleRate1Hour = 60,
|
||||
SampleRate3Hours = 180,
|
||||
SampleRate1Day = 1440,
|
||||
SampleRate1Week = 10080,
|
||||
SampleRate1Month = 43200,
|
||||
SampleRate1Year = 525600
|
||||
};
|
||||
Q_ENUM(SampleRate)
|
||||
|
||||
explicit NewLogsModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
void classBegin() override;
|
||||
void componentComplete() override;
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
void fetchMore(const QModelIndex &parent = QModelIndex()) override;
|
||||
|
||||
Engine *engine() const;
|
||||
void setEngine(Engine *engine);
|
||||
|
||||
QString source() const;
|
||||
void setSource(const QString &source);
|
||||
|
||||
QStringList sources() const;
|
||||
void setSources(const QStringList &sources);
|
||||
|
||||
QStringList columns() const;
|
||||
void setColumns(const QStringList &columns);
|
||||
|
||||
QVariantMap filter() const;
|
||||
void setFilter(const QVariantMap &filter);
|
||||
|
||||
QDateTime startTime() const;
|
||||
void setStartTime(const QDateTime &startTime);
|
||||
|
||||
QDateTime endTime() const;
|
||||
void setEndTime(const QDateTime &endTime);
|
||||
|
||||
SampleRate sampleRate() const;
|
||||
void setSampleRate(SampleRate sampleRate);
|
||||
|
||||
bool busy() const;
|
||||
|
||||
Q_INVOKABLE NewLogEntry *get(int index) const;
|
||||
Q_INVOKABLE NewLogEntry *find(const QDateTime ×tamp) const;
|
||||
|
||||
// bool live() const;
|
||||
// void setLive(bool live);
|
||||
|
||||
public slots:
|
||||
void clear();
|
||||
void fetchLogs();
|
||||
|
||||
signals:
|
||||
void engineChanged();
|
||||
void sourcesChanged();
|
||||
void columnsChanged();
|
||||
void filterChanged();
|
||||
void busyChanged();
|
||||
void countChanged();
|
||||
void startTimeChanged();
|
||||
void endTimeChanged();
|
||||
void sampleRateChanged();
|
||||
|
||||
void entriesAdded(int index, const QList<NewLogEntry*> &entries);
|
||||
void entriesRemoved(int index, int count);
|
||||
|
||||
private slots:
|
||||
void logsReply(int commandId, const QVariantMap &data);
|
||||
|
||||
private:
|
||||
Engine *m_engine = nullptr;
|
||||
QStringList m_sources;
|
||||
QStringList m_columns;
|
||||
QVariantMap m_filter;
|
||||
QDateTime m_startTime;
|
||||
QDateTime m_endTime;
|
||||
SampleRate m_sampleRate = SampleRateAny;
|
||||
|
||||
bool m_completed = false;
|
||||
bool m_canFetchMore = true;
|
||||
bool m_busy = false;
|
||||
int m_blockSize = 30;
|
||||
|
||||
QList<NewLogEntry*> m_list;
|
||||
};
|
||||
|
||||
#endif // NEWLOGSMODEL_H
|
||||
@ -48,6 +48,20 @@ QVariant ThingModel::data(const QModelIndex &index, int role) const
|
||||
if (role == RoleId) {
|
||||
return m_list.at(index.row());
|
||||
}
|
||||
if (role == RoleName) {
|
||||
StateType* stateType = m_device->thingClass()->stateTypes()->getStateType(m_list.at(index.row()));
|
||||
if (stateType) {
|
||||
return stateType->name();
|
||||
}
|
||||
ActionType* actionType = m_device->thingClass()->actionTypes()->getActionType(m_list.at(index.row()));
|
||||
if (actionType) {
|
||||
return actionType->name();
|
||||
}
|
||||
EventType* eventType = m_device->thingClass()->eventTypes()->getEventType(m_list.at(index.row()));
|
||||
if (eventType) {
|
||||
return eventType->name();
|
||||
}
|
||||
}
|
||||
if (role == RoleType) {
|
||||
StateType* stateType = m_device->thingClass()->stateTypes()->getStateType(m_list.at(index.row()));
|
||||
if (stateType) {
|
||||
@ -87,6 +101,7 @@ QHash<int, QByteArray> ThingModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles.insert(RoleId, "id");
|
||||
roles.insert(RoleName, "name");
|
||||
roles.insert(RoleType, "type");
|
||||
roles.insert(RoleDisplayName, "displayName");
|
||||
roles.insert(RoleWritable, "writable");
|
||||
|
||||
@ -52,6 +52,7 @@ public:
|
||||
enum Roles {
|
||||
RoleId,
|
||||
RoleType,
|
||||
RoleName,
|
||||
RoleDisplayName,
|
||||
RoleWritable
|
||||
};
|
||||
|
||||
@ -951,6 +951,17 @@ Thing* ThingManager::unpackThing(ThingManager *thingManager, const QVariantMap &
|
||||
}
|
||||
thing->setStates(states);
|
||||
|
||||
|
||||
QList<QUuid> loggedStateTypeIds;
|
||||
foreach (const QVariant &uuid, thingMap.value("loggedStateTypeIds").toList()) {
|
||||
loggedStateTypeIds.append(uuid.toUuid());
|
||||
}
|
||||
thing->setLoggedStateTypeIds(loggedStateTypeIds);
|
||||
QList<QUuid> loggedEventTypeIds;
|
||||
foreach (const QVariant &uuid, thingMap.value("loggedEventTypeIds").toList()) {
|
||||
loggedEventTypeIds.append(uuid.toUuid());
|
||||
}
|
||||
thing->setLoggedEventTypeIds(loggedEventTypeIds);
|
||||
return thing;
|
||||
}
|
||||
|
||||
|
||||
@ -182,4 +182,6 @@ private:
|
||||
QDateTime m_connectionBenchmark;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(QList<QUuid>)
|
||||
|
||||
#endif // THINGMANAGER_H
|
||||
|
||||
@ -218,6 +218,32 @@ void Thing::setStateValue(const QUuid &stateTypeId, const QVariant &value)
|
||||
}
|
||||
}
|
||||
|
||||
QList<QUuid> Thing::loggedStateTypeIds() const
|
||||
{
|
||||
return m_loggedStateTypeIds;
|
||||
}
|
||||
|
||||
void Thing::setLoggedStateTypeIds(const QList<QUuid> &loggedStateTypeIds)
|
||||
{
|
||||
if (m_loggedStateTypeIds != loggedStateTypeIds) {
|
||||
m_loggedStateTypeIds = loggedStateTypeIds;
|
||||
emit loggedStateTypeIdsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QList<QUuid> Thing::loggedEventTypeIds() const
|
||||
{
|
||||
return m_loggedEventTypeIds;
|
||||
}
|
||||
|
||||
void Thing::setLoggedEventTypeIds(const QList<QUuid> &loggedEventTypeIds)
|
||||
{
|
||||
if (m_loggedEventTypeIds != loggedEventTypeIds) {
|
||||
m_loggedEventTypeIds = loggedEventTypeIds;
|
||||
emit loggedEventTypeIdsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
int Thing::executeAction(const QString &actionName, const QVariantList ¶ms)
|
||||
{
|
||||
ActionType *actionType = m_thingClass->actionTypes()->findByName(actionName);
|
||||
|
||||
@ -55,6 +55,8 @@ class Thing : public QObject
|
||||
Q_PROPERTY(Params *settings READ settings NOTIFY settingsChanged)
|
||||
Q_PROPERTY(States *states READ states NOTIFY statesChanged)
|
||||
Q_PROPERTY(ThingClass *thingClass READ thingClass CONSTANT)
|
||||
Q_PROPERTY(QList<QUuid> loggedStateTypeIds READ loggedStateTypeIds NOTIFY loggedStateTypeIdsChanged)
|
||||
Q_PROPERTY(QList<QUuid> loggedEventTypeIds READ loggedEventTypeIds NOTIFY loggedEventTypeIdsChanged)
|
||||
|
||||
public:
|
||||
enum ThingSetupStatus {
|
||||
@ -122,6 +124,12 @@ public:
|
||||
void setStates(States *states);
|
||||
void setStateValue(const QUuid &stateTypeId, const QVariant &value);
|
||||
|
||||
QList<QUuid> loggedStateTypeIds() const;
|
||||
void setLoggedStateTypeIds(const QList<QUuid> &loggedStateTypeIds);
|
||||
|
||||
QList<QUuid> loggedEventTypeIds() const;
|
||||
void setLoggedEventTypeIds(const QList<QUuid> &loggedEventTypeIds);
|
||||
|
||||
ThingClass *thingClass() const;
|
||||
|
||||
Q_INVOKABLE bool hasState(const QUuid &stateTypeId) const;
|
||||
@ -140,6 +148,8 @@ signals:
|
||||
void paramsChanged();
|
||||
void settingsChanged();
|
||||
void statesChanged();
|
||||
void loggedStateTypeIdsChanged();
|
||||
void loggedEventTypeIdsChanged();
|
||||
void eventTriggered(const QUuid &eventTypeId, const QVariantList ¶ms);
|
||||
|
||||
signals:
|
||||
@ -156,6 +166,8 @@ protected:
|
||||
Params *m_settings = nullptr;
|
||||
States *m_states = nullptr;
|
||||
ThingClass *m_thingClass = nullptr;
|
||||
QList<QUuid> m_loggedStateTypeIds;
|
||||
QList<QUuid> m_loggedEventTypeIds;
|
||||
|
||||
QList<int> m_pendingActions;
|
||||
};
|
||||
|
||||
@ -305,5 +305,7 @@
|
||||
<file>ui/images/infinity.svg</file>
|
||||
<file>ui/images/edit-paste.svg</file>
|
||||
<file>ui/images/list-move.svg</file>
|
||||
<file>ui/images/system-log-out.svg</file>
|
||||
<file>ui/images/system-restart.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
<file>ui/customviews/WeatherView.qml</file>
|
||||
<file>ui/devicepages/MediaThingPage.qml</file>
|
||||
<file>ui/devicepages/ButtonThingPage.qml</file>
|
||||
<file>ui/devicepages/GenericDevicePage.qml</file>
|
||||
<file>ui/devicepages/GenericThingPage.qml</file>
|
||||
<file>ui/devicepages/WeatherDevicePage.qml</file>
|
||||
<file>ui/devicepages/SensorDevicePage.qml</file>
|
||||
<file>ui/devicepages/ThingPageBase.qml</file>
|
||||
@ -90,6 +90,7 @@
|
||||
<file>ui/delegates/ThingDelegate.qml</file>
|
||||
<file>ui/delegates/InterfaceTile.qml</file>
|
||||
<file>ui/system/LogViewerPage.qml</file>
|
||||
<file>ui/system/LogViewerPagePre18.qml</file>
|
||||
<file>ui/system/PluginsPage.qml</file>
|
||||
<file>ui/system/PluginParamsPage.qml</file>
|
||||
<file>ui/system/AboutNymeaPage.qml</file>
|
||||
@ -309,5 +310,7 @@
|
||||
<file>ui/mainviews/airconditioning/EditZonePage.qml</file>
|
||||
<file>ui/mainviews/airconditioning/EditZoneThingsPage.qml</file>
|
||||
<file>ui/mainviews/airconditioning/LegendDelegate.qml</file>
|
||||
<file>ui/customviews/StateChart.qml</file>
|
||||
<file>ui/devicepages/ThingLogPage.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -174,7 +174,13 @@ Page {
|
||||
text: qsTr("Log viewer")
|
||||
subText: qsTr("View system log")
|
||||
visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin)
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("system/LogViewerPage.qml"))
|
||||
onClicked: {
|
||||
if (engine.jsonRpcClient.ensureServerVersion("8.0")) {
|
||||
pageStack.push(Qt.resolvedUrl("system/LogViewerPage.qml"))
|
||||
} else {
|
||||
pageStack.push(Qt.resolvedUrl("system/LogViewerPagePre18.qml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsTile {
|
||||
|
||||
@ -98,7 +98,7 @@ Item {
|
||||
|
||||
// Icon/graph colors for various interfaces
|
||||
property var interfaceColors: {
|
||||
"temperaturesensor": red,
|
||||
"temperaturesensor": orange,
|
||||
"humiditysensor": lightBlue,
|
||||
"moisturesensor": blue,
|
||||
"lightsensor": yellow,
|
||||
|
||||
@ -140,7 +140,7 @@ ItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
text: root.subText
|
||||
font.pixelSize: root.prominentSubText ? Style.smallFont : app.extraSmallFont
|
||||
font.pixelSize: root.prominentSubText ? Style.smallFont.pixelSize : Style.extraSmallFont.pixelSize
|
||||
color: root.prominentSubText ? Material.foreground : Material.color(Material.Grey)
|
||||
wrapMode: root.wrapTexts ? Text.WordWrap : Text.NoWrap
|
||||
maximumLineCount: root.wrapTexts ? 2 : 1
|
||||
|
||||
@ -55,7 +55,7 @@ AutoSizeMenu {
|
||||
pageStack.push(Qt.resolvedUrl("../magic/ThingRulesPage.qml"), {thing: root.thing})
|
||||
}
|
||||
function openGenericThingPage() {
|
||||
pageStack.push(Qt.resolvedUrl("../devicepages/GenericDevicePage.qml"), {thing: root.thing})
|
||||
pageStack.push(Qt.resolvedUrl("../devicepages/GenericThingPage.qml"), {thing: root.thing})
|
||||
}
|
||||
function toggleFavorite() {
|
||||
if (favoritesProxy.count === 0) {
|
||||
@ -74,7 +74,11 @@ AutoSizeMenu {
|
||||
}
|
||||
|
||||
function openThingLogPage() {
|
||||
pageStack.push(Qt.resolvedUrl("../devicepages/DeviceLogPage.qml"), {thing: root.thing });
|
||||
if (engine.jsonRpcClient.ensureServerVersion("8.0")) {
|
||||
pageStack.push(Qt.resolvedUrl("../devicepages/ThingLogPage.qml"), {thing: root.thing });
|
||||
} else {
|
||||
pageStack.push(Qt.resolvedUrl("../devicepages/DeviceLogPage.qml"), {thing: root.thing });
|
||||
}
|
||||
}
|
||||
|
||||
function writeNfcTag() {
|
||||
|
||||
519
nymea-app/ui/customviews/StateChart.qml
Normal file
519
nymea-app/ui/customviews/StateChart.qml
Normal file
@ -0,0 +1,519 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Controls.Material 2.2
|
||||
import QtQuick.Layouts 1.1
|
||||
import Nymea 1.0
|
||||
import NymeaApp.Utils 1.0
|
||||
import "../components"
|
||||
import "../customviews"
|
||||
import QtCharts 2.2
|
||||
|
||||
Item {
|
||||
id: root
|
||||
implicitHeight: width * .6
|
||||
implicitWidth: 400
|
||||
|
||||
property Thing thing: null
|
||||
property StateType stateType: null
|
||||
property int roundTo: 2
|
||||
property color color: Style.accentColor
|
||||
property string iconSource: ""
|
||||
property alias title: titleLabel.text
|
||||
property bool titleVisible: true
|
||||
|
||||
readonly property State valueState: thing && stateType ? thing.states.getState(stateType.id) : null
|
||||
readonly property StateType connectedStateType: hasConnectable ? thing.thingClass.stateTypes.findByName("connected") : null
|
||||
readonly property bool hasConnectable: connectedStateType != null
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
property date now: new Date()
|
||||
|
||||
readonly property int range: selectionTabs.currentValue.range
|
||||
readonly property int sampleRate: root.stateType == null || root.stateType.type.toLowerCase() == "bool" ? NewLogsModel.SampleRateAny : selectionTabs.currentValue.sampleRate
|
||||
|
||||
readonly property int visibleValues: range / sampleRate
|
||||
|
||||
readonly property var startTime: {
|
||||
var date = new Date(fixTime(now));
|
||||
date.setTime(date.getTime() - range * 60000 + 2000);
|
||||
return date;
|
||||
}
|
||||
|
||||
readonly property var endTime: {
|
||||
var date = new Date(fixTime(now));
|
||||
date.setTime(date.getTime() + 2000)
|
||||
return date;
|
||||
}
|
||||
|
||||
function fixTime(timestamp) {
|
||||
return timestamp
|
||||
}
|
||||
}
|
||||
|
||||
NewLogsModel {
|
||||
id: logsModel
|
||||
engine: root.thing && root.stateType ? _engine : null
|
||||
source: root.thing ? "state-" + thing.id + "-" + root.stateType.name : ""
|
||||
// columns: [root.stateType.name]
|
||||
// filter: root.stateType ? ({state: root.stateType.name}) : ({})
|
||||
startTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||
endTime: new Date(d.endTime.getTime() + d.range * 60000)
|
||||
sampleRate: d.sampleRate
|
||||
|
||||
property double minValue
|
||||
property double maxValue
|
||||
|
||||
onEntriesAdded: {
|
||||
print("**** entries added", index, entries.length, "entries in series:", valueSeries.count, "in model", logsModel.count)
|
||||
if (valueSeries.count == 0) {
|
||||
print("adding zero item", new Date())
|
||||
valueSeries.insert(0, new Date(), 0)
|
||||
zeroSeries.ensureValue(new Date())
|
||||
}
|
||||
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = entries[i]
|
||||
// print("entry", entry.timestamp, entry.source, JSON.stringify(entry.values))
|
||||
zeroSeries.ensureValue(entry.timestamp)
|
||||
|
||||
if (root.stateType.type.toLowerCase() == "bool") {
|
||||
var value = entry.values[root.stateType.name]
|
||||
if (value == null) {
|
||||
value = false;
|
||||
}
|
||||
// for booleans, we'll insert the opposite value right before the new one so the position is doubled
|
||||
// +1 because the is the "new" value at the beginning
|
||||
var insertIdx = (index + i) * 2 + 1
|
||||
valueSeries.insert(insertIdx, entry.timestamp, value)
|
||||
valueSeries.insert(insertIdx + 1, entry.timestamp.getTime() - 500, !value)
|
||||
|
||||
if (insertIdx == 1) {
|
||||
// first index, we'll have to update the "now" value
|
||||
valueSeries.removePoints(0, 1);
|
||||
valueSeries.insert(0, entry.timestamp.getTime() + 2000, value)
|
||||
zeroSeries.ensureValue(new Date(entry.timestamp.getTime() + 2000))
|
||||
}
|
||||
|
||||
} else {
|
||||
var value = entry.values[root.stateType.name]
|
||||
if (value == null) {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
minValue = minValue == undefined ? value : Math.min(minValue, value)
|
||||
maxValue = maxValue == undefined ? value : Math.max(maxValue, value)
|
||||
|
||||
var insertIdx = (index + i) + 1
|
||||
valueSeries.insert(insertIdx, entry.timestamp, value)
|
||||
|
||||
if (insertIdx == 1) {
|
||||
// first index, we'll have to update the "now" value
|
||||
valueSeries.removePoints(0, 1);
|
||||
valueSeries.insert(0, entry.timestamp.getTime() + 2000, value)
|
||||
zeroSeries.ensureValue(new Date(entry.timestamp.getTime() + 2000))
|
||||
}
|
||||
}
|
||||
}
|
||||
print("added entries. now in series:", valueSeries.count)
|
||||
}
|
||||
onEntriesRemoved: {
|
||||
print("removing:", index, count, valueSeries.count)
|
||||
if (root.stateType.type.toLowerCase() == "bool") {
|
||||
valueSeries.removePoints((index * 2) + 1, count * 2)
|
||||
} else {
|
||||
valueSeries.removePoints(index + 1, count)
|
||||
}
|
||||
|
||||
zeroSeries.shrink()
|
||||
}
|
||||
|
||||
onEngineChanged: fetchLogs()
|
||||
Component.onCompleted: fetchLogs()
|
||||
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Label {
|
||||
id: titleLabel
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Style.smallMargins
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.stateType.displayName
|
||||
visible: root.titleVisible
|
||||
// MouseArea {
|
||||
// anchors.fill: parent
|
||||
// onClicked: {
|
||||
// pageStack.push(Qt.resolvedUrl("PowerBalanceHistoryPage.qml"))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
SelectionTabs {
|
||||
id: selectionTabs
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.smallMargins
|
||||
Layout.rightMargin: Style.smallMargins
|
||||
currentIndex: 1
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
modelData: qsTr("Hours")
|
||||
sampleRate: NewLogsModel.SampleRate1Min
|
||||
range: 180 // 3 Hours: 3 * 60
|
||||
}
|
||||
ListElement {
|
||||
modelData: qsTr("Days")
|
||||
sampleRate: NewLogsModel.SampleRate15Mins
|
||||
range: 1440 // 1 Day: 24 * 60
|
||||
}
|
||||
ListElement {
|
||||
modelData: qsTr("Weeks")
|
||||
sampleRate: NewLogsModel.SampleRate1Hour
|
||||
range: 10080 // 7 Days: 7 * 24 * 60
|
||||
}
|
||||
ListElement {
|
||||
modelData: qsTr("Months")
|
||||
sampleRate: NewLogsModel.SampleRate3Hours
|
||||
range: 43200 // 30 Days: 30 * 24 * 60
|
||||
}
|
||||
}
|
||||
onTabSelected: {
|
||||
d.now = new Date()
|
||||
logsModel.clear()
|
||||
logsModel.fetchLogs()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ChartView {
|
||||
id: chartView
|
||||
anchors.fill: parent
|
||||
// backgroundColor: "transparent"
|
||||
margins.left: 0
|
||||
margins.right: 0
|
||||
margins.bottom: Style.smallMargins //Style.smallIconSize + Style.margins
|
||||
margins.top: 0
|
||||
|
||||
backgroundColor: Style.tileBackgroundColor
|
||||
backgroundRoundness: Style.cornerRadius
|
||||
|
||||
legend.alignment: Qt.AlignBottom
|
||||
legend.labelColor: Style.foregroundColor
|
||||
legend.font: Style.extraSmallFont
|
||||
legend.visible: false
|
||||
|
||||
ValueAxis {
|
||||
id: valueAxis
|
||||
min: logsModel.minValue == undefined || logsModel.minValue == 0
|
||||
? 0
|
||||
: logsModel.minValue - 5
|
||||
max: logsModel.maxValue == undefined || logsModel.maxValue == 0
|
||||
? 0
|
||||
: logsModel.maxValue + 5
|
||||
|
||||
labelFormat: ""
|
||||
gridLineColor: Style.tileOverlayColor
|
||||
labelsVisible: false
|
||||
lineVisible: false
|
||||
titleVisible: false
|
||||
shadesVisible: false
|
||||
|
||||
}
|
||||
Item {
|
||||
id: labelsLayout
|
||||
x: Style.smallMargins
|
||||
y: chartView.plotArea.y
|
||||
height: chartView.plotArea.height
|
||||
width: chartView.plotArea.x - x
|
||||
visible: root.stateType.type.toLowerCase() != "bool"
|
||||
Repeater {
|
||||
model: valueAxis.tickCount
|
||||
delegate: Label {
|
||||
y: parent.height / (valueAxis.tickCount - 1) * index - font.pixelSize / 2
|
||||
width: parent.width - Style.smallMargins
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: root.stateType ? Types.toUiValue(((valueAxis.max - (index * valueAxis.max / (valueAxis.tickCount - 1)))), root.stateType.unit).toFixed(0) + Types.toUiUnit(root.stateType.unit) : ""
|
||||
verticalAlignment: Text.AlignTop
|
||||
font: Style.extraSmallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DateTimeAxis {
|
||||
id: dateTimeAxis
|
||||
|
||||
min: d.startTime
|
||||
max: d.endTime
|
||||
format: {
|
||||
switch (selectionTabs.currentValue.sampleRate) {
|
||||
case NewLogsModel.SampleRate1Min:
|
||||
case NewLogsModel.SampleRate15Mins:
|
||||
return "hh:mm"
|
||||
case NewLogsModel.SampleRate1Hour:
|
||||
case NewLogsModel.SampleRate3Hours:
|
||||
case NewLogsModel.SampleRate1Day:
|
||||
return "dd.MM."
|
||||
}
|
||||
}
|
||||
tickCount: {
|
||||
switch (selectionTabs.currentValue.sampleRate) {
|
||||
case NewLogsModel.SampleRate1Min:
|
||||
case NewLogsModel.SampleRate15Mins:
|
||||
return root.width > 500 ? 13 : 7
|
||||
case NewLogsModel.SampleRate1Hour:
|
||||
return 7
|
||||
case NewLogsModel.SampleRate3Hours:
|
||||
case NewLogsModel.SampleRate1Day:
|
||||
return root.width > 500 ? 12 : 6
|
||||
}
|
||||
}
|
||||
labelsFont: Style.extraSmallFont
|
||||
gridVisible: false
|
||||
minorGridVisible: false
|
||||
lineVisible: false
|
||||
shadesVisible: false
|
||||
labelsColor: Style.foregroundColor
|
||||
}
|
||||
|
||||
AreaSeries {
|
||||
id: mainSeries
|
||||
axisX: dateTimeAxis
|
||||
axisY: valueAxis
|
||||
name: root.stateType ? root.stateType.displayName : ""
|
||||
color: Qt.rgba(root.color.r, root.color.g, root.color.b, .5)
|
||||
borderColor: root.color
|
||||
borderWidth: 2
|
||||
lowerSeries: LineSeries {
|
||||
id: zeroSeries
|
||||
XYPoint { x: dateTimeAxis.max.getTime(); y: 0 }
|
||||
XYPoint { x: dateTimeAxis.min.getTime(); y: 0 }
|
||||
function ensureValue(timestamp) {
|
||||
if (count == 0) {
|
||||
append(timestamp, 0)
|
||||
} else if (count == 1) {
|
||||
if (timestamp.getTime() < at(0).x) {
|
||||
append(timestamp, 0)
|
||||
} else {
|
||||
insert(0, timestamp, 0)
|
||||
}
|
||||
} else {
|
||||
if (timestamp.getTime() < at(1).x) {
|
||||
remove(1)
|
||||
append(timestamp, 0)
|
||||
} else if (timestamp.getTime() > at(0).x) {
|
||||
remove(0)
|
||||
insert(0, timestamp, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
function shrink() {
|
||||
clear();
|
||||
if (logsModel.count > 0) {
|
||||
ensureValue(logsModel.get(0).timestamp)
|
||||
ensureValue(logsModel.get(logsModel.count-1).timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upperSeries: LineSeries {
|
||||
id: valueSeries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RowLayout {
|
||||
id: legend
|
||||
anchors { left: parent.left; bottom: parent.bottom; right: parent.right }
|
||||
anchors.leftMargin: chartView.plotArea.x
|
||||
height: Style.smallIconSize
|
||||
anchors.margins: Style.margins
|
||||
visible: false
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
// opacity: selfProductionConsumptionSeries.opacity
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: -Style.smallMargins
|
||||
anchors.bottomMargin: -Style.smallMargins
|
||||
// onClicked: d.selectSeries(selfProductionConsumptionSeries)
|
||||
}
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.smallMargins
|
||||
ColorIcon {
|
||||
name: "weathericons/weather-clear-day"
|
||||
size: Style.smallIconSize
|
||||
color: Style.green
|
||||
}
|
||||
Label {
|
||||
width: parent.parent.width - x
|
||||
elide: Text.ElideRight
|
||||
visible: legend.width > 500
|
||||
text: qsTr("Produced")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font: Style.smallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
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
|
||||
preventStealing: tooltipping || dragging
|
||||
propagateComposedEvents: true
|
||||
|
||||
property int startMouseX: 0
|
||||
property bool dragging: false
|
||||
property bool tooltipping: false
|
||||
|
||||
property var startDatetime: null
|
||||
|
||||
Timer {
|
||||
interval: 300
|
||||
running: mouseArea.pressed
|
||||
onTriggered: {
|
||||
if (!mouseArea.dragging) {
|
||||
mouseArea.tooltipping = true
|
||||
}
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
if (mouseArea.dragging) {
|
||||
logsModel.fetchLogs()
|
||||
mouseArea.dragging = false;
|
||||
}
|
||||
|
||||
mouseArea.tooltipping = false;
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
startMouseX = mouseX
|
||||
startDatetime = d.now
|
||||
}
|
||||
|
||||
onDoubleClicked: {
|
||||
if (selectionTabs.currentIndex == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = Math.ceil(mouseArea.mouseX * d.visibleValues / mouseArea.width)
|
||||
var timestamp = new Date(d.startTime.getTime() + (idx * d.sampleRate * 60000))
|
||||
selectionTabs.currentIndex--
|
||||
d.now = new Date(Math.min(new Date().getTime(), timestamp.getTime() + (d.visibleValues / 2) * d.sampleRate * 60000))
|
||||
powerBalanceLogs.fetchLogs()
|
||||
}
|
||||
|
||||
onMouseXChanged: {
|
||||
if (!pressed || mouseArea.tooltipping) {
|
||||
return;
|
||||
}
|
||||
if (Math.abs(startMouseX - mouseX) < 10) {
|
||||
return;
|
||||
}
|
||||
dragging = true
|
||||
|
||||
var dragDelta = startMouseX - mouseX
|
||||
var totalTime = d.endTime.getTime() - d.startTime.getTime()
|
||||
// dragDelta : timeDelta = width : totalTime
|
||||
var timeDelta = dragDelta * totalTime / mouseArea.width
|
||||
// print("dragging", dragDelta, totalTime, mouseArea.width)
|
||||
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() + timeDelta)))
|
||||
}
|
||||
|
||||
onWheel: {
|
||||
startDatetime = d.now
|
||||
var totalTime = d.endTime.getTime() - d.startTime.getTime()
|
||||
// pixelDelta : timeDelta = width : totalTime
|
||||
var timeDelta = wheel.pixelDelta.x * totalTime / mouseArea.width
|
||||
// print("wheeling", wheel.pixelDelta.x, totalTime, mouseArea.width)
|
||||
d.now = new Date(Math.min(new Date(), new Date(startDatetime.getTime() - timeDelta)))
|
||||
wheelStopTimer.restart()
|
||||
}
|
||||
Timer {
|
||||
id: wheelStopTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: logsModel.fetchLogs()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: parent.height
|
||||
width: 1
|
||||
color: Style.foregroundColor
|
||||
x: Math.min(mouseArea.width, Math.max(0, toolTip.entryX))
|
||||
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||
}
|
||||
|
||||
NymeaToolTip {
|
||||
id: toolTip
|
||||
visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging
|
||||
|
||||
backgroundItem: chartView
|
||||
backgroundRect: Qt.rect(mouseArea.x + toolTip.x, mouseArea.y + toolTip.y, toolTip.width, toolTip.height)
|
||||
|
||||
property var timestamp: new Date(d.startTime.getTime() + (mouseArea.mouseX * (d.endTime.getTime() - d.startTime.getTime()) / mouseArea.width) )
|
||||
property NewLogEntry entry: logsModel.find(timestamp)
|
||||
|
||||
// eX : eT = w : duration
|
||||
property int entryX: entry ? (entry.timestamp.getTime() - d.startTime.getTime()) * mouseArea.width / (d.endTime.getTime() - d.startTime.getTime()) : 0
|
||||
property int xOnRight: Math.max(0, entryX) + Style.smallMargins
|
||||
property int xOnLeft: Math.min(entryX, mouseArea.width) - Style.smallMargins - width
|
||||
x: xOnRight + width < mouseArea.width ? xOnRight : xOnLeft
|
||||
property double value: toolTip.entry ? entry.values[root.stateType.name] : 0
|
||||
y: Math.min(Math.max(mouseArea.height - (value * mouseArea.height / valueAxis.max) - height - Style.margins, 0), mouseArea.height - height)
|
||||
|
||||
width: tooltipLayout.implicitWidth + Style.smallMargins * 2
|
||||
height: tooltipLayout.implicitHeight + Style.smallMargins * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: tooltipLayout
|
||||
width: parent.width
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
margins: Style.smallMargins
|
||||
}
|
||||
Label {
|
||||
text: toolTip.entry ? toolTip.entry.timestamp.toLocaleString(Qt.locale(), Locale.ShortFormat) : ""
|
||||
font: Style.smallFont
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
property double value: toolTip.entry
|
||||
? (toolTip.entry.acquisition >= 0 ? toolTip.entry.consumption : Math.max(0, -toolTip.entry.production))
|
||||
: 0
|
||||
property bool translate: value >= 1000
|
||||
property double translatedValue: value / (translate ? 1000 : 1)
|
||||
text: toolTip.entry == null
|
||||
? ""
|
||||
: root.stateType.type.toLowerCase() == "bool"
|
||||
? root.stateType.displayName + ": " + (toolTip.value ? qsTr("Yes") : qsTr("No"))
|
||||
: Types.toUiValue(toolTip.entry.values[root.stateType.name], root.stateType.unit).toFixed(root.roundTo) + Types.toUiUnit(root.stateType.unit)
|
||||
font: Style.smallFont
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -57,7 +57,7 @@ MainPageTile {
|
||||
// Only one item? Go streight to the thing page
|
||||
if (thingsProxy.count === 1) {
|
||||
if (!iface) {
|
||||
page = "GenericDevicePage.qml";
|
||||
page = "GenericThingPage.qml";
|
||||
} else {
|
||||
page = NymeaUtils.interfaceListToDevicePage([iface.name]);
|
||||
}
|
||||
|
||||
@ -36,6 +36,8 @@ import Nymea 1.0
|
||||
import "../components"
|
||||
import "../customviews"
|
||||
|
||||
// Legacy for jsonrpc < 8.0
|
||||
|
||||
Page {
|
||||
id: root
|
||||
|
||||
|
||||
@ -128,7 +128,12 @@ ThingPageBase {
|
||||
}
|
||||
onClicked: {
|
||||
swipe.close();
|
||||
pageStack.push(Qt.resolvedUrl("DeviceLogPage.qml"), {thing: root.thing, filterTypeIds: [model.id]})
|
||||
print("opening logs for", delegate.stateType)
|
||||
if (engine.jsonRpcClient.ensureServerVersion("8.0")) {
|
||||
pageStack.push(Qt.resolvedUrl("StateLogPage.qml"), {thing: root.thing, stateType: delegate.stateType})
|
||||
} else {
|
||||
pageStack.push(Qt.resolvedUrl("DeviceLogPage.qml"), {thing: root.thing, filterTypeIds: [model.id]})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -269,16 +274,14 @@ ThingPageBase {
|
||||
when: !stateDelegate.valueCacheDirty && stateDelegate.pendingActionId === -1
|
||||
}
|
||||
Binding {
|
||||
target: stateDelegateLoader.item
|
||||
target: stateDelegateLoader.item.hasOwnProperty("from") ? stateDelegateLoader.item : null
|
||||
property: "from"
|
||||
value: stateDelegate.thingState.minValue
|
||||
when: stateDelegateLoader.item.hasOwnProperty("from")
|
||||
}
|
||||
Binding {
|
||||
target: stateDelegateLoader.item
|
||||
target: stateDelegateLoader.item.hasOwnProperty("to") ? stateDelegateLoader.item : null
|
||||
property: "to"
|
||||
value: stateDelegate.thingState.maxValue
|
||||
when: stateDelegateLoader.item.hasOwnProperty("to")
|
||||
}
|
||||
Binding {
|
||||
target: stateDelegateLoader.item.hasOwnProperty("unit") ? stateDelegateLoader.item : null
|
||||
@ -142,13 +142,23 @@ ThingPageBase {
|
||||
property State state: root.thing.stateByName(interfaceStateMap[modelData])
|
||||
property string interfaceName: modelData
|
||||
|
||||
sourceComponent: graphComponent
|
||||
sourceComponent: engine.jsonRpcClient.ensureServerVersion("8.0") ? stateChartComponent : graphComponent
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: stateChartComponent
|
||||
StateChart {
|
||||
thing: root.thing
|
||||
stateType: parent.stateType
|
||||
color: app.interfaceToColor(interfaceName)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: graphComponent
|
||||
|
||||
|
||||
@ -42,6 +42,8 @@ Page {
|
||||
property Thing thing: null
|
||||
property StateType stateType: null
|
||||
|
||||
readonly property bool isLogged: thing.loggedStateTypeIds.indexOf(stateType.id) >= 0
|
||||
|
||||
readonly property bool canShowGraph: {
|
||||
switch (root.stateType.type) {
|
||||
case "Int":
|
||||
@ -58,71 +60,115 @@ Page {
|
||||
onBackPressed: pageStack.pop()
|
||||
}
|
||||
|
||||
LogsModelNg {
|
||||
id: logsModelNg
|
||||
NewLogsModel {
|
||||
id: logsModel
|
||||
engine: _engine
|
||||
thingId: root.thing.id
|
||||
typeIds: [root.stateType.id]
|
||||
live: true
|
||||
columns: [root.stateType.name]
|
||||
source: "states-" + root.thing.id
|
||||
filter: ({state: root.stateType.name})
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Component.onCompleted: {
|
||||
print("loaded statelogpage for", root.stateType)
|
||||
}
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
GridLayout {
|
||||
anchors.fill: parent
|
||||
columns: app.landscape ? 2 : 1
|
||||
|
||||
StateChart {
|
||||
Layout.fillWidth: true
|
||||
visible: root.canShowGraph
|
||||
TabButton {
|
||||
text: qsTr("Log")
|
||||
}
|
||||
TabButton {
|
||||
text: qsTr("Graph")
|
||||
}
|
||||
thing: root.thing
|
||||
stateType: root.stateType
|
||||
}
|
||||
|
||||
SwipeView {
|
||||
id: swipeView
|
||||
ListView {
|
||||
id: listView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
currentIndex: tabBar.currentIndex
|
||||
interactive: false
|
||||
implicitWidth: 400
|
||||
model: logsModel
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
GenericTypeLogView {
|
||||
id: logView
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
|
||||
logsModel: logsModelNg
|
||||
|
||||
onAddRuleClicked: {
|
||||
var value = logView.logsModel.get(index).value
|
||||
var typeId = logView.logsModel.get(index).typeId
|
||||
var rule = engine.ruleManager.createNewRule();
|
||||
var stateEvaluator = rule.createStateEvaluator();
|
||||
stateEvaluator.stateDescriptor.thingId = thing.id;
|
||||
stateEvaluator.stateDescriptor.stateTypeId = typeId;
|
||||
stateEvaluator.stateDescriptor.value = value;
|
||||
stateEvaluator.stateDescriptor.valueOperator = StateDescriptor.ValueOperatorEquals;
|
||||
rule.setStateEvaluator(stateEvaluator);
|
||||
rule.name = root.thing.name + " - " + stateType.displayName + " = " + value;
|
||||
|
||||
var rulePage = pageStack.push(Qt.resolvedUrl("../magic/ThingRulesPage.qml"), {thing: root.thing});
|
||||
rulePage.addRule(rule);
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: graphLoader
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
Component.onCompleted: {
|
||||
var source;
|
||||
source = Qt.resolvedUrl("../customviews/GenericTypeGraph.qml");
|
||||
setSource(source, {thing: root.thing, stateType: root.stateType})
|
||||
}
|
||||
delegate: NymeaItemDelegate {
|
||||
width: listView.width
|
||||
property NewLogEntry entry: logsModel.get(index)
|
||||
text: entry.values[root.stateType.name]
|
||||
subText: entry.timestamp.toLocaleString(Qt.locale())
|
||||
progressive: false
|
||||
Component.onCompleted: print("delegate:", JSON.stringify(entry.values), root.stateType.name, entry.values[root.stateType.name])
|
||||
}
|
||||
}
|
||||
|
||||
// TabBar {
|
||||
// id: tabBar
|
||||
// Layout.fillWidth: true
|
||||
// visible: root.canShowGraph
|
||||
// TabButton {
|
||||
// text: qsTr("Log")
|
||||
// }
|
||||
// TabButton {
|
||||
// text: qsTr("Graph")
|
||||
// }
|
||||
// }
|
||||
|
||||
// SwipeView {
|
||||
// id: swipeView
|
||||
// Layout.fillWidth: true
|
||||
// Layout.fillHeight: true
|
||||
// currentIndex: tabBar.currentIndex
|
||||
// interactive: false
|
||||
|
||||
// GenericTypeLogView {
|
||||
// id: logView
|
||||
// width: swipeView.width
|
||||
// height: swipeView.height
|
||||
|
||||
// logsModel: logsModelNg
|
||||
|
||||
// onAddRuleClicked: {
|
||||
// var value = logView.logsModel.get(index).value
|
||||
// var typeId = logView.logsModel.get(index).typeId
|
||||
// var rule = engine.ruleManager.createNewRule();
|
||||
// var stateEvaluator = rule.createStateEvaluator();
|
||||
// stateEvaluator.stateDescriptor.thingId = thing.id;
|
||||
// stateEvaluator.stateDescriptor.stateTypeId = typeId;
|
||||
// stateEvaluator.stateDescriptor.value = value;
|
||||
// stateEvaluator.stateDescriptor.valueOperator = StateDescriptor.ValueOperatorEquals;
|
||||
// rule.setStateEvaluator(stateEvaluator);
|
||||
// rule.name = root.thing.name + " - " + stateType.displayName + " = " + value;
|
||||
|
||||
// var rulePage = pageStack.push(Qt.resolvedUrl("../magic/ThingRulesPage.qml"), {thing: root.thing});
|
||||
// rulePage.addRule(rule);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Loader {
|
||||
// id: graphLoader
|
||||
// width: swipeView.width
|
||||
// height: swipeView.height
|
||||
// Component.onCompleted: {
|
||||
// var source;
|
||||
// source = Qt.resolvedUrl("../customviews/GenericTypeGraph.qml");
|
||||
// setSource(source, {thing: root.thing, stateType: root.stateType})
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
EmptyViewPlaceholder {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - app.margins * 2
|
||||
title: qsTr("Logging not enabled")
|
||||
text: qsTr("This state is not being logged.")
|
||||
imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle)
|
||||
buttonText: qsTr("Enable logging")
|
||||
visible: !root.isLogged
|
||||
onButtonClicked: {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
397
nymea-app/ui/devicepages/ThingLogPage.qml
Normal file
397
nymea-app/ui/devicepages/ThingLogPage.qml
Normal file
@ -0,0 +1,397 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* Copyright 2013 - 2023, nymea GmbH
|
||||
* Contact: contact@nymea.io
|
||||
*
|
||||
* This file is part of nymea.
|
||||
* This project including source code and documentation is protected by
|
||||
* copyright law, and remains the property of nymea GmbH. All rights, including
|
||||
* reproduction, publication, editing and translation, are reserved. The use of
|
||||
* this project is subject to the terms of a license agreement to be concluded
|
||||
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
|
||||
* under https://nymea.io/license
|
||||
*
|
||||
* GNU General Public License Usage
|
||||
* Alternatively, this project may be redistributed and/or modified under the
|
||||
* terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, GNU version 3. This project 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 General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this project. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* For any further details and any questions please contact us under
|
||||
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||
* https://nymea.io/license/faq
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Controls.Material 2.2
|
||||
import QtQuick.Layouts 1.1
|
||||
import Nymea 1.0
|
||||
import "../components"
|
||||
import "../customviews"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
|
||||
property Thing thing: null
|
||||
|
||||
header: NymeaHeader {
|
||||
text: qsTr("History for %1").arg(root.thing.name)
|
||||
onBackPressed: pageStack.pop()
|
||||
|
||||
HeaderButton {
|
||||
imageSource: "../images/filters.svg"
|
||||
color: logsModelNg.filterEnabled ? Style.accentColor : Style.iconColor
|
||||
onClicked: logsModelNg.filterEnabled = !logsModelNg.filterEnabled
|
||||
visible: root.filterTypeIds.length === 0
|
||||
}
|
||||
}
|
||||
|
||||
NewLogsModel {
|
||||
id: logsModelNg
|
||||
engine: _engine
|
||||
columns: [root.stateType.name]
|
||||
sources: ["states-" + root.thing.id, "events-" + root.thing.id, "actions-" + root.thing.id]
|
||||
filter: {
|
||||
if (!filterEnabled) {
|
||||
return ({})
|
||||
}
|
||||
print("*** filter updated", isStateFilter, isEventFilter, isActionFilter, filterTypeName, thing.thingClass.stateTypes.findByName(filterTypeName))
|
||||
if (isStateFilter) {
|
||||
return ({state: filterTypeName})
|
||||
}
|
||||
if (isEventFilter) {
|
||||
return ({event: filterTypeName})
|
||||
}
|
||||
if (isActionFilter) {
|
||||
return ({action: filterTypeName})
|
||||
}
|
||||
return ({})
|
||||
}
|
||||
property string filterTypeName: filterDeviceModel.getData(filterComboBox.currentIndex, ThingModel.RoleName)
|
||||
property bool isStateFilter: thing.thingClass.stateTypes.findByName(filterTypeName) !== null
|
||||
property bool isEventFilter: thing.thingClass.eventTypes.findByName(filterTypeName) !== null
|
||||
property bool isActionFilter: thing.thingClass.actionTypes.findByName(filterTypeName) !== null
|
||||
|
||||
onFilterChanged: {
|
||||
logsModelNg.clear()
|
||||
logsModelNg.fetchLogs()
|
||||
}
|
||||
|
||||
// thingId: root.thing.id
|
||||
// typeIds: root.filterTypeIds.length > 0
|
||||
// ? root.filterTypeIds
|
||||
// : filterEnabled
|
||||
// ? [filterDeviceModel.getData(filterComboBox.currentIndex, ThingModel.RoleId)]
|
||||
// : []
|
||||
// live: true
|
||||
|
||||
onEntriesAdded: {
|
||||
console.log("entries added", JSON.stringify(entries))
|
||||
}
|
||||
|
||||
property bool filterEnabled: false
|
||||
}
|
||||
|
||||
ThingModel {
|
||||
id: filterDeviceModel
|
||||
thing: root.thing
|
||||
}
|
||||
|
||||
Pane {
|
||||
id: filterPane
|
||||
anchors { left: parent.left; top: parent.top; right: parent.right }
|
||||
Behavior on height { NumberAnimation { duration: 120; easing.type: Easing.InOutQuad } }
|
||||
|
||||
height: logsModelNg.filterEnabled ? implicitHeight + app.margins * 2 : 0
|
||||
Material.elevation: 1
|
||||
|
||||
leftPadding: 0; rightPadding: 0; topPadding: 0; bottomPadding: 0
|
||||
contentItem: Item {
|
||||
clip: true
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: app.margins
|
||||
spacing: app.margins
|
||||
Label {
|
||||
text: qsTr("Filter by")
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: filterComboBox
|
||||
Layout.fillWidth: true
|
||||
textRole: "displayName"
|
||||
model: filterDeviceModel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: graphLoader
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: filterPane.bottom
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
readonly property StateType stateType: root.thing.thingClass.stateTypes.getStateType(root.filterTypeIds[0])
|
||||
|
||||
readonly property bool canShowGraph: {
|
||||
if (stateType === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (stateType.unit === Types.UnitUnixTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (stateType.type.toLowerCase()) {
|
||||
case "uint":
|
||||
case "int":
|
||||
case "double":
|
||||
case "bool":
|
||||
return true;
|
||||
}
|
||||
print("not showing graph for", stateType.type)
|
||||
return false;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (root.filterTypeIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (!canShowGraph) {
|
||||
return;
|
||||
}
|
||||
|
||||
var source = Qt.resolvedUrl("../customviews/GenericTypeGraph.qml");
|
||||
setSource(source, {thing: root.thing, stateType: stateType})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ListView {
|
||||
anchors { left: parent.left; top: graphLoader.bottom; right: parent.right; bottom: parent.bottom }
|
||||
clip: true
|
||||
model: logsModelNg
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
visible: logsModelNg.busy
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: entryDelegate
|
||||
width: parent.width
|
||||
property NewLogEntry entry: logsModelNg.get(index)
|
||||
|
||||
property StateType stateType: entry && entry.values.hasOwnProperty("state") ? root.thing.thingClass.stateTypes.findByName(entry.values.state) : null
|
||||
property EventType eventType: entry && entry.values.hasOwnProperty("event") ? root.thing.thingClass.eventTypes.findByName(entry.values.event) : null
|
||||
property ActionType actionType: entry && entry.values.hasOwnProperty("action") ? root.thing.thingClass.actionTypes.findByName(entry.values.action) : null
|
||||
|
||||
contentItem: RowLayout {
|
||||
ColorIcon {
|
||||
Layout.preferredWidth: Style.iconSize
|
||||
Layout.preferredHeight: width
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: Style.accentColor
|
||||
name: {
|
||||
if (entryDelegate.stateType) {
|
||||
return "../images/state.svg"
|
||||
}
|
||||
if (entryDelegate.eventType) {
|
||||
return "../images/event.svg"
|
||||
}
|
||||
if (entryDelegate.actionType) {
|
||||
return "../images/action.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Label {
|
||||
text: {
|
||||
if (entryDelegate.stateType) {
|
||||
return entryDelegate.stateType.displayName
|
||||
}
|
||||
if (entryDelegate.eventType) {
|
||||
return entryDelegate.eventType.displayName
|
||||
}
|
||||
if (entryDelegate.actionType) {
|
||||
return entryDelegate.actionType.displayName
|
||||
}
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font: Style.smallFont
|
||||
}
|
||||
Label {
|
||||
text: Qt.formatDateTime(model.timestamp,"dd.MM.yy hh:mm:ss")
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: app.smallFont
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Loader {
|
||||
id: valueLoader
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: {
|
||||
if (entryDelegate.stateType) {
|
||||
switch (entryDelegate.stateType.type.toLowerCase()) {
|
||||
case "bool":
|
||||
return boolComponent;
|
||||
case "color":
|
||||
return colorComponent
|
||||
case "double":
|
||||
return floatLabelComponent;
|
||||
default:
|
||||
if (entryDelegate.stateType.unit == Types.UnitUnixTime) {
|
||||
return dateTimeComponent
|
||||
}
|
||||
|
||||
return labelComponent
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// switch (model.source) {
|
||||
// case LogEntry.LoggingSourceStates:
|
||||
// case LogEntry.LoggingSourceActions:
|
||||
// return labelComponent;
|
||||
// case LogEntry.LoggingSourceEvents:
|
||||
|
||||
// break;
|
||||
// }
|
||||
|
||||
return labelComponent
|
||||
}
|
||||
Binding {
|
||||
when: entryDelegate.stateType != null
|
||||
target: valueLoader.item;
|
||||
property: "value";
|
||||
value: entryDelegate.stateType ? Types.toUiValue(entry.values[entry.values.state], entryDelegate.stateType.unit) : ""
|
||||
}
|
||||
Binding {
|
||||
when: entryDelegate.stateType != null
|
||||
target: entryDelegate.stateType && valueLoader.item.hasOwnProperty("unitString") ? valueLoader.item : null;
|
||||
property: "unitString"
|
||||
value: entryDelegate.stateType ? Types.toUiUnit(entryDelegate.stateType.unit) : ""
|
||||
}
|
||||
Binding {
|
||||
when: entryDelegate.actionType != null
|
||||
target: valueLoader.item;
|
||||
property: "value";
|
||||
value: {
|
||||
if (entryDelegate.actionType == null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
var ret = []
|
||||
var values = JSON.parse(model.values.params)
|
||||
for (var i = 0; i < entryDelegate.actionType.paramTypes.count; i++) {
|
||||
var paramType = entryDelegate.actionType.paramTypes.get(i)
|
||||
ret.push(paramType.displayName + ": " + Types.toUiValue(values[paramType.name], paramType.unit) + " " + Types.toUiUnit(paramType.unit))
|
||||
}
|
||||
return ret.join(", ")
|
||||
}
|
||||
}
|
||||
Binding {
|
||||
when: entryDelegate.eventType != null
|
||||
target: valueLoader.item;
|
||||
property: "value";
|
||||
value: {
|
||||
if (entryDelegate.eventType == null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
var ret = []
|
||||
var values = JSON.parse(entry.values.params)
|
||||
for (var i = 0; i < entryDelegate.eventType.paramTypes.count; i++) {
|
||||
var paramType = entryDelegate.eventType.paramTypes.get(i)
|
||||
ret.push(paramType.displayName + ": " + Types.toUiValue(values[paramType.name], paramType.unit) + " " + Types.toUiUnit(paramType.unit))
|
||||
}
|
||||
return ret.join(", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: labelComponent
|
||||
Label {
|
||||
property var value
|
||||
property string unitString
|
||||
text: value + " " + unitString
|
||||
font: Style.smallFont
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: floatLabelComponent
|
||||
Label {
|
||||
property double value
|
||||
property string unitString
|
||||
text: value.toFixed(value > 1000 ? 0 : 2) + " " + unitString
|
||||
font: Style.smallFont
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: dateTimeComponent
|
||||
Label {
|
||||
property var value
|
||||
font: Style.smallFont
|
||||
text: Qt.formatDateTime(new Date(value * 1000), Qt.DefaultLocaleShortDate)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: boolComponent
|
||||
RowLayout {
|
||||
id: boolLed
|
||||
property var value
|
||||
Led {
|
||||
implicitHeight: app.smallFont
|
||||
state: boolLed.value === "true" ? "on" : "off"
|
||||
}
|
||||
Label {
|
||||
font: Style.smallFont
|
||||
text: boolLed.value === "true" ? qsTr("Yes") : qsTr("No")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: colorComponent
|
||||
Item {
|
||||
property var value
|
||||
implicitHeight: app.smallFont
|
||||
Rectangle {
|
||||
height: parent.height
|
||||
width: height * 2
|
||||
color: parent.value
|
||||
// radius: width / 2
|
||||
border.color: Style.foregroundColor
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,33 +57,125 @@ ThingPageBase {
|
||||
Layout.fillWidth: true
|
||||
columns: Math.min(width / 300, 4)
|
||||
|
||||
GenericTypeGraph {
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("temperature")
|
||||
iconSource: app.interfaceToIcon("temperaturesensor")
|
||||
color: app.interfaceToColor("temperaturesensor")
|
||||
sourceComponent: {
|
||||
if (engine.jsonRpcClient.ensureServerVersion("8.0")) {
|
||||
return tempComponent
|
||||
}
|
||||
return tempComponentPre18
|
||||
}
|
||||
}
|
||||
GenericTypeGraph {
|
||||
Layout.fillWidth: true
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("humidity")
|
||||
iconSource: app.interfaceToIcon("humiditysensor")
|
||||
color: app.interfaceToColor("humiditysensor")
|
||||
|
||||
Component {
|
||||
id: tempComponent
|
||||
StateChart {
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("temperature")
|
||||
color: app.interfaceToColor("temperaturesensor")
|
||||
}
|
||||
}
|
||||
GenericTypeGraph {
|
||||
Layout.fillWidth: true
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("pressure")
|
||||
iconSource: app.interfaceToIcon("pressuresensor")
|
||||
color: app.interfaceToColor("pressuresensor")
|
||||
|
||||
Component {
|
||||
id: tempComponentPre18
|
||||
|
||||
GenericTypeGraph {
|
||||
Layout.fillWidth: true
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("temperature")
|
||||
iconSource: app.interfaceToIcon("temperaturesensor")
|
||||
color: app.interfaceToColor("temperaturesensor")
|
||||
}
|
||||
}
|
||||
GenericTypeGraph {
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("windSpeed")
|
||||
iconSource: app.interfaceToIcon("windspeedsensor")
|
||||
color: app.interfaceToColor("windspeedsensor")
|
||||
sourceComponent: {
|
||||
if (engine.jsonRpcClient.ensureServerVersion("8.0")) {
|
||||
return humidityComponent
|
||||
}
|
||||
return humidityComponentPre18
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: humidityComponent
|
||||
StateChart {
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("humidity")
|
||||
color: app.interfaceToColor("humiditysensor")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: humidityComponentPre18
|
||||
GenericTypeGraph {
|
||||
Layout.fillWidth: true
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("humidity")
|
||||
iconSource: app.interfaceToIcon("humiditysensor")
|
||||
color: app.interfaceToColor("humiditysensor")
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: {
|
||||
if (engine.jsonRpcClient.ensureServerVersion("8.0")) {
|
||||
return pressureComponent
|
||||
}
|
||||
return pressureComponentPre18
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pressureComponent
|
||||
StateChart {
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("pressure")
|
||||
color: app.interfaceToColor("pressuresensor")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pressureComponentPre18
|
||||
GenericTypeGraph {
|
||||
Layout.fillWidth: true
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("pressure")
|
||||
iconSource: app.interfaceToIcon("pressuresensor")
|
||||
color: app.interfaceToColor("pressuresensor")
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: {
|
||||
if (engine.jsonRpcClient.ensureServerVersion("8.0")) {
|
||||
return windSpeedComponent
|
||||
}
|
||||
return windSpeedComponentPre18
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: windSpeedComponent
|
||||
StateChart {
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("windSpeed")
|
||||
color: app.interfaceToColor("windspeedsensor")
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: windSpeedComponentPre18
|
||||
GenericTypeGraph {
|
||||
Layout.fillWidth: true
|
||||
thing: root.thing
|
||||
stateType: root.thingClass.stateTypes.findByName("windSpeed")
|
||||
iconSource: app.interfaceToIcon("windspeedsensor")
|
||||
color: app.interfaceToColor("windspeedsensor")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
167
nymea-app/ui/images/system-log-out.svg
Normal file
167
nymea-app/ui/images/system-log-out.svg
Normal file
@ -0,0 +1,167 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="96"
|
||||
height="96"
|
||||
id="svg4874"
|
||||
version="1.1"
|
||||
inkscape:version="0.91+devel r"
|
||||
viewBox="0 0 96 96.000001"
|
||||
sodipodi:docname="system-logout.svg">
|
||||
<defs
|
||||
id="defs4876" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="7.024999"
|
||||
inkscape:cx="109.81075"
|
||||
inkscape:cy="31.025706"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
showborder="true"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:snap-bbox-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:object-nodes="true"
|
||||
inkscape:snap-smooth-nodes="true"
|
||||
inkscape:snap-midpoints="true"
|
||||
inkscape:snap-object-midpoints="true"
|
||||
inkscape:snap-center="true"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:snap-global="true">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid5451"
|
||||
empspacing="8" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="8,-8.0000001"
|
||||
id="guide4063"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="4,-8.0000001"
|
||||
id="guide4065"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="-8,88.000001"
|
||||
id="guide4067"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="-8,92.000001"
|
||||
id="guide4069"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="104,4"
|
||||
id="guide4071"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="-5,8.0000001"
|
||||
id="guide4073"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="88,-8.0000001"
|
||||
id="guide4077"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="-8,84.000001"
|
||||
id="guide4074"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="12,-8.0000001"
|
||||
id="guide4076"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="84,-8.0000001"
|
||||
id="guide4080"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="48,-8.0000001"
|
||||
orientation="1,0"
|
||||
id="guide4170"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="-8,48"
|
||||
orientation="0,1"
|
||||
id="guide4172"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="92,-8.0000001"
|
||||
orientation="1,0"
|
||||
id="guide4760"
|
||||
inkscape:locked="false" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata4879">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(67.857146,-78.50504)">
|
||||
<rect
|
||||
transform="rotate(90)"
|
||||
y="-28.142855"
|
||||
x="78.505043"
|
||||
height="96"
|
||||
width="96"
|
||||
id="rect4782-637"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:3.99999976;marker:none;enable-background:accumulate" />
|
||||
<path
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="rect4747-4"
|
||||
d="m 20.142883,124.50503 1.000025,3.99998 h -32.999999 v -3.99998 z"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;stroke:none;stroke-width:8.99999905;marker:none;enable-background:accumulate"
|
||||
inkscape:transform-center-x="-26.500014" />
|
||||
<path
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.99999976;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
d="M 5.6230724,101.05661 C -4.6712646,90.767855 -20.167327,87.685805 -33.61715,93.253885 c -13.44979,5.56807 -22.23047,18.700765 -22.23047,33.251945 0,14.55118 8.78068,27.68194 22.23047,33.25002 13.449823,5.56804 28.9458854,2.48602 39.2402224,-7.80276 l -2.828107,-2.82811 c -9.158173,9.15322 -22.9166364,11.88745 -34.8828054,6.93358 -11.96618,-4.95383 -19.75979,-16.60879 -19.75979,-29.55273 0,-12.94393 7.79361,-24.60083 19.75979,-29.554705 11.966169,-4.95382 25.7246324,-2.2196 34.8828054,6.933635 z"
|
||||
id="path4145"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path5837"
|
||||
d="m 7.1430854,114.50443 0.0076,24 c 3.6500406,-1.66873 7.3659206,-3.53593 11.1491896,-5.59872 3.747742,-2.06778 7.362822,-4.2005 10.84286,-6.40026 -3.480038,-2.15576 -7.095118,-4.26806 -10.84286,-6.33584 -3.785348,-2.06393 -7.503345,-3.95188 -11.1553126,-5.66518 z"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.99940658;marker:none;enable-background:accumulate" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
18
nymea-app/ui/images/system-restart.svg
Normal file
18
nymea-app/ui/images/system-restart.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg id="svg4874" width="96" height="96" version="1.1" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<metadata id="metadata4879">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g id="layer1" transform="translate(67.857 -78.505)">
|
||||
<rect id="rect4782-30" transform="rotate(90)" x="78.505" y="-28.143" width="96" height="96" style="color:#000000;fill:none"/>
|
||||
<path id="path4116-17" d="m-21.223 90.588c-3.8018 0.13947-7.624 0.88592-11.326 2.2793-14.809 5.5735-24.24 20.2-23.205 35.988 1.0349 15.789 12.293 29.059 27.703 32.652 15.41 3.5929 31.378-3.3305 39.289-17.033l-3.4648-2c-7.0391 12.192-21.206 18.333-34.916 15.137-13.71-3.1967-23.698-14.97-24.619-29.018-0.92073-14.048 7.4453-27.024 20.621-31.982 13.176-4.9589 28.024-0.7194 36.594 10.449l3.1719-2.4356c-7.2243-9.4147-18.442-14.456-29.848-14.037z" style="color-rendering:auto;color:#000000;fill:#808080;font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:none;font-variant-numeric:normal;font-variant-position:normal;image-rendering:auto;isolation:auto;mix-blend-mode:normal;shape-padding:0;shape-rendering:auto;solid-color:#000000;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
||||
<path id="path5839-5" d="m10.449 92.17-16.965 16.976c3.7609 1.401 7.7088 2.7082 11.843 3.9248 4.1122 1.1879 8.1765 2.2361 12.193 3.1414-0.93638-3.9851-1.999-8.035-3.1869-12.147-1.2172-4.1361-2.5113-8.1001-3.8821-11.894z" style="color:#000000;fill:#808080"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@ -31,6 +31,7 @@ Page {
|
||||
id: d
|
||||
|
||||
property date now: new Date()
|
||||
property int sampleRate: NewLogsModel.SampleRate15Mins
|
||||
|
||||
property int range: 60 * 24
|
||||
|
||||
@ -203,28 +204,29 @@ Page {
|
||||
readonly property Thing thing: zoneWrapper.thermostats.get(index)
|
||||
property XYSeries series: null
|
||||
|
||||
readonly property LogsModel logsModel: LogsModel {
|
||||
readonly property NewLogsModel logsModel: NewLogsModel {
|
||||
objectName: "temp: " + thing.name
|
||||
engine: typeIds.length > 0 ? _engine : null
|
||||
thingId: thing.id
|
||||
live: true
|
||||
sourceFilter: LogsModel.SourceStates
|
||||
// graphSeries: series
|
||||
viewStartTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||
engine: _engine
|
||||
source: "states-" + thing.id
|
||||
filter: ({state: "temperature"})
|
||||
startTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||
endTime: new Date(d.endTime.getTime() + d.range * 60000)
|
||||
sampleRate: d.sampleRate
|
||||
|
||||
fetchBlockSize: 500
|
||||
|
||||
typeIds: {
|
||||
var ret = [];
|
||||
ret.push(thing.thingClass.stateTypes.findByName("temperature").id)
|
||||
return ret;
|
||||
onEntriesAdded: {
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = entries[i]
|
||||
var value = entry.values["temperature"]
|
||||
if (value == null) {
|
||||
value = 0;
|
||||
}
|
||||
series.insert(index + i, entry.timestamp, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XYSeriesAdapter {
|
||||
logsModel: thermostatDelegate.logsModel
|
||||
xySeries: series
|
||||
sampleRate: XYSeriesAdapter.SampleRate10Minutes
|
||||
onEntriesRemoved: {
|
||||
series.removePoints(index, count)
|
||||
}
|
||||
Component.onCompleted: fetchLogs()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@ -253,28 +255,29 @@ Page {
|
||||
readonly property Thing thing: zoneWrapper.indoorTempSensors.get(index)
|
||||
property XYSeries series: null
|
||||
|
||||
readonly property LogsModel logsModel: LogsModel {
|
||||
readonly property NewLogsModel logsModel: NewLogsModel {
|
||||
objectName: "temp: " + thing.name
|
||||
engine: typeIds.length > 0 ? _engine : null
|
||||
thingId: thing.id
|
||||
sourceFilter: LogsModel.SourceStates
|
||||
live: true
|
||||
// graphSeries: series
|
||||
viewStartTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||
|
||||
fetchBlockSize: 500
|
||||
|
||||
typeIds: {
|
||||
var ret = [];
|
||||
ret.push(thing.thingClass.stateTypes.findByName("temperature").id)
|
||||
return ret;
|
||||
engine: _engine
|
||||
source: "states-" + thing.id
|
||||
filter: ({state: "temperature"})
|
||||
startTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||
endTime: new Date(d.endTime.getTime() + d.range * 60000)
|
||||
sampleRate: d.sampleRate
|
||||
onEntriesAdded: {
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = entries[i]
|
||||
var value = entry.values["temperature"]
|
||||
if (value == null) {
|
||||
value = 0;
|
||||
}
|
||||
series.insert(index + i, entry.timestamp, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
onEntriesRemoved: {
|
||||
series.removePoints(index, count)
|
||||
}
|
||||
Component.onCompleted: fetchLogs()
|
||||
|
||||
XYSeriesAdapter {
|
||||
logsModel: tempDelegate.logsModel
|
||||
xySeries: series
|
||||
sampleRate: XYSeriesAdapter.SampleRate10Minutes
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@ -303,27 +306,29 @@ Page {
|
||||
readonly property Thing thing: zoneWrapper.indoorHumiditySensors.get(index)
|
||||
property XYSeries series: null
|
||||
|
||||
readonly property LogsModel logsModel: LogsModel {
|
||||
readonly property NewLogsModel logsModel: NewLogsModel {
|
||||
objectName: "hum: " + thing.name
|
||||
engine: typeIds.length > 0 ? _engine : null
|
||||
thingId: thing.id
|
||||
sourceFilter: LogsModel.SourceStates
|
||||
live: true
|
||||
// graphSeries: series
|
||||
viewStartTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||
fetchBlockSize: 500
|
||||
engine: _engine
|
||||
source: "states-" + thing.id
|
||||
filter: ({state: "humidity"})
|
||||
startTime: new Date(d.startTime.getTime() - d.range * 60000)
|
||||
endTime: new Date(d.endTime.getTime() + d.range * 60000)
|
||||
sampleRate: d.sampleRate
|
||||
|
||||
typeIds: {
|
||||
var ret = [];
|
||||
ret.push(thing.thingClass.stateTypes.findByName("humidity").id)
|
||||
return ret;
|
||||
onEntriesAdded: {
|
||||
for (var i = 0; i < entries.length; i++) {
|
||||
var entry = entries[i]
|
||||
var value = entry.values["humidity"]
|
||||
if (value == null) {
|
||||
value = 0;
|
||||
}
|
||||
series.insert(index + i, entry.timestamp, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XYSeriesAdapter {
|
||||
logsModel: humidityDelegate.logsModel
|
||||
xySeries: series
|
||||
sampleRate: XYSeriesAdapter.SampleRate10Minutes
|
||||
onEntriesRemoved: {
|
||||
series.removePoints(index, count)
|
||||
}
|
||||
Component.onCompleted: fetchLogs()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@ -345,7 +350,7 @@ Page {
|
||||
|
||||
Repeater {
|
||||
id: vocRepeater
|
||||
model: zoneWrapper.indoorVocSensors
|
||||
// model: zoneWrapper.indoorVocSensors
|
||||
delegate: Item {
|
||||
id: vocDelegate
|
||||
readonly property Thing thing: zoneWrapper.indoorVocSensors.get(index)
|
||||
@ -391,7 +396,7 @@ Page {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: zoneWrapper.windowSensors
|
||||
// model: zoneWrapper.windowSensors
|
||||
delegate: Item {
|
||||
id: closableDelegate
|
||||
readonly property Thing thing: zoneWrapper.windowSensors.get(index)
|
||||
@ -446,7 +451,7 @@ Page {
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: zoneWrapper.thermostats.count
|
||||
// model: zoneWrapper.thermostats.count
|
||||
delegate: Item {
|
||||
id: heatingDelegate
|
||||
readonly property Thing thing: zoneWrapper.thermostats.get(index)
|
||||
|
||||
@ -45,19 +45,35 @@ DashboardDelegateBase {
|
||||
readonly property StateType stateType: thing ? thing.thingClass.stateTypes.getStateType(item.stateTypeId) : null
|
||||
readonly property State state: thing ? thing.states.getState(item.stateTypeId) : null
|
||||
|
||||
contentItem: GenericTypeGraph {
|
||||
contentItem: StateChart {
|
||||
id: graph
|
||||
width: root.width
|
||||
height: root.height
|
||||
title: root.state && root.stateType ? root.thing.name + " " + Types.toUiValue(root.state.value, root.stateType.unit) + Types.toUiUnit(root.stateType.unit) : ""
|
||||
title: root.state && root.stateType ? root.thing.name + ", " + root.stateType.displayName + ": " + Types.toUiValue(root.state.value, root.stateType.unit).toFixed(0) + Types.toUiUnit(root.stateType.unit) : ""
|
||||
|
||||
thing: root.thing
|
||||
color: "blue"//app.interfaceToColor(interfaceName)
|
||||
iconSource: ""// app.interfaceToIcon(interfaceName)
|
||||
color: root.thing ? app.interfaceToColor(root.thing.thingClass.interfaces[0]) : Style.accentColor
|
||||
// iconSource: ""// app.interfaceToIcon(interfaceName)
|
||||
implicitHeight: width * .6
|
||||
// property string interfaceName: parent.interfaceName
|
||||
stateType: root.stateType
|
||||
property State state: root.state
|
||||
// property State state: root.state
|
||||
|
||||
}
|
||||
|
||||
// contentItem: GenericTypeGraph {
|
||||
// id: graph
|
||||
// width: root.width
|
||||
// height: root.height
|
||||
// title: root.state && root.stateType ? root.thing.name + " " + Types.toUiValue(root.state.value, root.stateType.unit) + Types.toUiUnit(root.stateType.unit) : ""
|
||||
|
||||
// thing: root.thing
|
||||
// color: "blue"//app.interfaceToColor(interfaceName)
|
||||
// iconSource: ""// app.interfaceToIcon(interfaceName)
|
||||
// implicitHeight: width * .6
|
||||
//// property string interfaceName: parent.interfaceName
|
||||
// stateType: root.stateType
|
||||
// property State state: root.state
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@ -55,10 +55,17 @@ Page {
|
||||
|
||||
LogsModel {
|
||||
id: logsModel
|
||||
engine: _engine
|
||||
//engine: _engine
|
||||
live: true
|
||||
}
|
||||
|
||||
NewLogsModel {
|
||||
id: newLogsModel
|
||||
engine: _engine
|
||||
// sources: ["core", "rules", "scripts"]
|
||||
source: "core"
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: listView
|
||||
visible: logsModel.busy
|
||||
@ -66,7 +73,7 @@ Page {
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
model: logsModel
|
||||
model: newLogsModel
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
headerPositioning: ListView.OverlayHeader
|
||||
@ -84,14 +91,28 @@ Page {
|
||||
visible: listView.model.busy
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
delegate: NymeaItemDelegate {
|
||||
id: delegate
|
||||
width: parent.width
|
||||
property Thing thing: engine.thingManager.things.getThing(model.thingId)
|
||||
width: listView.width
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
property NewLogEntry entry: newLogsModel.get(index)
|
||||
property string event: entry.values.event
|
||||
property string shutdownReason: {
|
||||
switch (entry.values.shutdownReason) {
|
||||
case "ShutdownReasonTerm":
|
||||
return qsTr("Terminated by system")
|
||||
case "ShutdownReasonQuit":
|
||||
return qsTr("Application quit")
|
||||
case "ShutdownReasonFailure":
|
||||
return qsTr("Application error")
|
||||
default:
|
||||
return qsTr("Unknown reason")
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
id: contentColumn
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins / 2 }
|
||||
@ -99,32 +120,24 @@ Page {
|
||||
Layout.preferredWidth: Style.iconSize
|
||||
Layout.preferredHeight: width
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: {
|
||||
switch (model.source) {
|
||||
case LogEntry.LoggingSourceStates:
|
||||
case LogEntry.LoggingSourceSystem:
|
||||
case LogEntry.LoggingSourceActions:
|
||||
case LogEntry.LoggingSourceEvents:
|
||||
return Style.accentColor
|
||||
case LogEntry.LoggingSourceRules:
|
||||
if (model.loggingEventType === LogEntry.LoggingEventTypeActiveChange) {
|
||||
return model.value === true ? "green" : Style.iconColor
|
||||
}
|
||||
return Style.accentColor
|
||||
}
|
||||
}
|
||||
color: delegate.event == "started"
|
||||
? Style.accentColor
|
||||
: delegate.entry.values.shutdownReason === "ShutdownReasonFailure"
|
||||
? Style.red
|
||||
: Style.iconColor
|
||||
name: {
|
||||
switch (model.source) {
|
||||
case LogEntry.LoggingSourceStates:
|
||||
return "../images/state.svg"
|
||||
case LogEntry.LoggingSourceSystem:
|
||||
return "../images/system-shutdown.svg"
|
||||
case LogEntry.LoggingSourceActions:
|
||||
return "../images/action.svg"
|
||||
case LogEntry.LoggingSourceEvents:
|
||||
return "../images/event.svg"
|
||||
case LogEntry.LoggingSourceRules:
|
||||
return "../images/magic.svg"
|
||||
switch (delegate.event) {
|
||||
case "started":
|
||||
return "system-restart"
|
||||
case "stopped":
|
||||
switch (delegate.entry.values.shutdownReason) {
|
||||
case "ShutdownReasonQuit":
|
||||
return "system-logout"
|
||||
case "ShutdownReasonTerm":
|
||||
return "system-shutdown"
|
||||
case "ShutdownReasonFailure":
|
||||
return "dialog-error-symbolic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,54 +145,30 @@ Page {
|
||||
RowLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: model.source === LogEntry.LoggingSourceSystem ?
|
||||
qsTr("%1 Server").arg(Configuration.systemName)
|
||||
: model.source === LogEntry.LoggingSourceRules ?
|
||||
engine.ruleManager.rules.getRule(model.typeId).name
|
||||
: delegate.thing.name
|
||||
text: {
|
||||
switch (delegate.event) {
|
||||
case "started":
|
||||
return qsTr("Started")
|
||||
case "stopped":
|
||||
return qsTr("Stopped")
|
||||
default:
|
||||
console.warn("LogViewer: Unhand event", delegate.event)
|
||||
return qsTr(delegate.event)
|
||||
}
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Label {
|
||||
text: Qt.formatDateTime(model.timestamp,"dd.MM.yy - hh:mm:ss")
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: app.smallFont
|
||||
font: Style.smallFont
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text : {
|
||||
switch (model.source) {
|
||||
case LogEntry.LoggingSourceStates:
|
||||
var stateType = delegate.thing.thingClass.stateTypes.getStateType(model.typeId);
|
||||
return "%1 -> %2 %3".arg(stateType.displayName).arg(Types.toUiValue(model.value, stateType.unit)).arg(Types.toUiUnit(stateType.unit));
|
||||
case LogEntry.LoggingSourceSystem:
|
||||
return model.loggingEventType === LogEntry.LoggingEventTypeActiveChange ? qsTr("System started") : "N/A"
|
||||
case LogEntry.LoggingSourceActions:
|
||||
return "%1 (%2)".arg(delegate.thing.thingClass.actionTypes.getActionType(model.typeId).displayName).arg(model.value);
|
||||
case LogEntry.LoggingSourceEvents:
|
||||
return "%1 (%2)".arg(delegate.thing.thingClass.eventTypes.getEventType(model.typeId).displayName).arg(model.value);
|
||||
case LogEntry.LoggingSourceRules:
|
||||
switch (model.loggingEventType) {
|
||||
case LogEntry.LoggingEventTypeTrigger:
|
||||
return qsTr("Rule triggered");
|
||||
case LogEntry.LoggingEventTypeActionsExecuted:
|
||||
return qsTr("Actions executed");
|
||||
case LogEntry.LoggingEventTypeActiveChange:
|
||||
return model.value === true ? qsTr("Rule active") : qsTr("Rule inactive")
|
||||
case LogEntry.LoggingEventTypeExitActionsExecuted:
|
||||
return qsTr("Exit actions executed");
|
||||
case LogEntry.LoggingEventTypeEnabledChange:
|
||||
return qsTr("Enabled changed");
|
||||
default:
|
||||
print("Unhandled logging event type", model.loggingEventType)
|
||||
}
|
||||
return "N/A"
|
||||
default:
|
||||
print("unhandled logging source:", model.source)
|
||||
}
|
||||
return "N/A";
|
||||
}
|
||||
text: delegate.shutdownReason
|
||||
visible: delegate.event == "stopped"
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: app.smallFont
|
||||
font: Style.smallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
188
nymea-app/ui/system/LogViewerPagePre18.qml
Normal file
188
nymea-app/ui/system/LogViewerPagePre18.qml
Normal file
@ -0,0 +1,188 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* Copyright 2013 - 2020, nymea GmbH
|
||||
* Contact: contact@nymea.io
|
||||
*
|
||||
* This file is part of nymea.
|
||||
* This project including source code and documentation is protected by
|
||||
* copyright law, and remains the property of nymea GmbH. All rights, including
|
||||
* reproduction, publication, editing and translation, are reserved. The use of
|
||||
* this project is subject to the terms of a license agreement to be concluded
|
||||
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
|
||||
* under https://nymea.io/license
|
||||
*
|
||||
* GNU General Public License Usage
|
||||
* Alternatively, this project may be redistributed and/or modified under the
|
||||
* terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, GNU version 3. This project 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 General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this project. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* For any further details and any questions please contact us under
|
||||
* contact@nymea.io or see our FAQ/Licensing Information on
|
||||
* https://nymea.io/license/faq
|
||||
*
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
import QtQuick 2.8
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Controls.Material 2.1
|
||||
import Nymea 1.0
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: root
|
||||
header: NymeaHeader {
|
||||
text: qsTr("Log viewer")
|
||||
onBackPressed: pageStack.pop()
|
||||
|
||||
HeaderButton {
|
||||
imageSource: "../images/down.svg"
|
||||
color: root.autoScroll ? Style.accentColor : Style.iconColor
|
||||
onClicked: {
|
||||
listView.positionViewAtEnd();
|
||||
root.autoScroll = !root.autoScroll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool autoScroll: true
|
||||
|
||||
LogsModel {
|
||||
id: logsModel
|
||||
engine: _engine
|
||||
live: true
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: listView
|
||||
visible: logsModel.busy
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
model: logsModel
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
headerPositioning: ListView.OverlayHeader
|
||||
|
||||
onDraggingChanged: {
|
||||
if (dragging) {
|
||||
root.autoScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
visible: listView.model.busy
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: delegate
|
||||
width: parent.width
|
||||
property Thing thing: engine.thingManager.things.getThing(model.thingId)
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
contentItem: RowLayout {
|
||||
id: contentColumn
|
||||
anchors { left: parent.left; right: parent.right; margins: app.margins / 2 }
|
||||
ColorIcon {
|
||||
Layout.preferredWidth: Style.iconSize
|
||||
Layout.preferredHeight: width
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: {
|
||||
switch (model.source) {
|
||||
case LogEntry.LoggingSourceStates:
|
||||
case LogEntry.LoggingSourceSystem:
|
||||
case LogEntry.LoggingSourceActions:
|
||||
case LogEntry.LoggingSourceEvents:
|
||||
return Style.accentColor
|
||||
case LogEntry.LoggingSourceRules:
|
||||
if (model.loggingEventType === LogEntry.LoggingEventTypeActiveChange) {
|
||||
return model.value === true ? "green" : Style.iconColor
|
||||
}
|
||||
return Style.accentColor
|
||||
}
|
||||
}
|
||||
name: {
|
||||
switch (model.source) {
|
||||
case LogEntry.LoggingSourceStates:
|
||||
return "../images/state.svg"
|
||||
case LogEntry.LoggingSourceSystem:
|
||||
return "../images/system-shutdown.svg"
|
||||
case LogEntry.LoggingSourceActions:
|
||||
return "../images/action.svg"
|
||||
case LogEntry.LoggingSourceEvents:
|
||||
return "../images/event.svg"
|
||||
case LogEntry.LoggingSourceRules:
|
||||
return "../images/magic.svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: model.source === LogEntry.LoggingSourceSystem ?
|
||||
qsTr("%1 Server").arg(Configuration.systemName)
|
||||
: model.source === LogEntry.LoggingSourceRules ?
|
||||
engine.ruleManager.rules.getRule(model.typeId).name
|
||||
: delegate.thing.name
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Label {
|
||||
text: Qt.formatDateTime(model.timestamp,"dd.MM.yy - hh:mm:ss")
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: app.smallFont
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text : {
|
||||
switch (model.source) {
|
||||
case LogEntry.LoggingSourceStates:
|
||||
var stateType = delegate.thing.thingClass.stateTypes.getStateType(model.typeId);
|
||||
return "%1 -> %2 %3".arg(stateType.displayName).arg(Types.toUiValue(model.value, stateType.unit)).arg(Types.toUiUnit(stateType.unit));
|
||||
case LogEntry.LoggingSourceSystem:
|
||||
return model.loggingEventType === LogEntry.LoggingEventTypeActiveChange ? qsTr("System started") : "N/A"
|
||||
case LogEntry.LoggingSourceActions:
|
||||
return "%1 (%2)".arg(delegate.thing.thingClass.actionTypes.getActionType(model.typeId).displayName).arg(model.value);
|
||||
case LogEntry.LoggingSourceEvents:
|
||||
return "%1 (%2)".arg(delegate.thing.thingClass.eventTypes.getEventType(model.typeId).displayName).arg(model.value);
|
||||
case LogEntry.LoggingSourceRules:
|
||||
switch (model.loggingEventType) {
|
||||
case LogEntry.LoggingEventTypeTrigger:
|
||||
return qsTr("Rule triggered");
|
||||
case LogEntry.LoggingEventTypeActionsExecuted:
|
||||
return qsTr("Actions executed");
|
||||
case LogEntry.LoggingEventTypeActiveChange:
|
||||
return model.value === true ? qsTr("Rule active") : qsTr("Rule inactive")
|
||||
case LogEntry.LoggingEventTypeExitActionsExecuted:
|
||||
return qsTr("Exit actions executed");
|
||||
case LogEntry.LoggingEventTypeEnabledChange:
|
||||
return qsTr("Enabled changed");
|
||||
default:
|
||||
print("Unhandled logging event type", model.loggingEventType)
|
||||
}
|
||||
return "N/A"
|
||||
default:
|
||||
print("unhandled logging source:", model.source)
|
||||
}
|
||||
return "N/A";
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: app.smallFont
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -74,7 +74,7 @@ Item {
|
||||
} else if (interfaceList.indexOf("cleaningrobot") >= 0) {
|
||||
page = "CleaningRobotThingPage.qml";
|
||||
} else {
|
||||
page = "GenericDevicePage.qml";
|
||||
page = "GenericThingPage.qml";
|
||||
}
|
||||
print("Selecting page", page, "for interface list:", interfaceList)
|
||||
return page;
|
||||
|
||||
Reference in New Issue
Block a user