Add support for the new log engine (protocol version 8.0)

This commit is contained in:
Michael Zanetti 2023-03-06 13:03:34 +01:00
parent 2e4f306c70
commit 6845d7d2ae
34 changed files with 2352 additions and 223 deletions

View File

@ -691,7 +691,7 @@ void JsonRpcClient::helloReply(int /*commandId*/, const QVariantMap &params)
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());

View File

@ -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");

View File

@ -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 \

View File

@ -0,0 +1,25 @@
#include "newlogentry.h"
NewLogEntry::NewLogEntry(const QString &source, const QDateTime &timestamp, 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;
}

View 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 &timestamp, 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

View 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 &timestamp) 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();
}

View 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 &timestamp) 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

View File

@ -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");

View File

@ -52,6 +52,7 @@ public:
enum Roles {
RoleId,
RoleType,
RoleName,
RoleDisplayName,
RoleWritable
};

View File

@ -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;
}

View File

@ -182,4 +182,6 @@ private:
QDateTime m_connectionBenchmark;
};
Q_DECLARE_METATYPE(QList<QUuid>)
#endif // THINGMANAGER_H

View File

@ -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 &params)
{
ActionType *actionType = m_thingClass->actionTypes()->findByName(actionName);

View File

@ -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 &params);
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;
};

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -98,7 +98,7 @@ Item {
// Icon/graph colors for various interfaces
property var interfaceColors: {
"temperaturesensor": red,
"temperaturesensor": orange,
"humiditysensor": lightBlue,
"moisturesensor": blue,
"lightsensor": yellow,

View File

@ -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

View File

@ -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() {

View 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
}
}
}
}
}
}
}

View File

@ -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]);
}

View File

@ -36,6 +36,8 @@ import Nymea 1.0
import "../components"
import "../customviews"
// Legacy for jsonrpc < 8.0
Page {
id: root

View File

@ -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

View File

@ -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

View File

@ -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: {
}
}
}

View 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
}
}
}
}

View File

@ -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")
}
}
}
}

View 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

View 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

View File

@ -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)

View File

@ -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
// }
}

View File

@ -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
}
}
}

View 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
}
}
}
}
}
}

View File

@ -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;