From e7316ea28251c45ceb667588fae8deb479fe5b3d Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 24 Apr 2023 01:07:26 +0200 Subject: [PATCH] Polishing of new logs --- libnymea-app/models/newlogsmodel.cpp | 180 +++++++---------- libnymea-app/models/newlogsmodel.h | 16 +- libnymea-app/thingmanager.cpp | 11 + libnymea-app/thingmanager.h | 2 + nymea-app/resources.qrc | 2 +- nymea-app/ui/customviews/StateChart.qml | 87 ++++---- nymea-app/ui/devicepages/ButtonThingPage.qml | 191 ++++++++++++++---- nymea-app/ui/devicepages/GenericThingPage.qml | 17 +- nymea-app/ui/devicepages/LightThingPage.qml | 6 +- ...icePage.qml => NotificationsThingPage.qml} | 123 ++++++++++- nymea-app/ui/devicepages/SensorDevicePage.qml | 4 +- .../ui/devicepages/SmartMeterDevicePage.qml | 6 +- nymea-app/ui/devicepages/StateLogPage.qml | 95 +++------ .../ui/devicepages/ThermostatDevicePage.qml | 2 +- nymea-app/ui/devicepages/ThingLogPage.qml | 64 +++--- nymea-app/ui/devicepages/ThingPageBase.qml | 5 +- .../ui/devicepages/WeatherDevicePage.qml | 16 +- .../airconditioning/ACChartsPage.qml | 77 ++++--- .../airconditioning/TooltipDelegate.qml | 9 +- .../ui/mainviews/energy/ConsumersHistory.qml | 33 ++- .../mainviews/energy/PowerBalanceHistory.qml | 18 +- nymea-app/ui/utils/NymeaUtils.qml | 2 +- 22 files changed, 591 insertions(+), 375 deletions(-) rename nymea-app/ui/devicepages/{NotificationsDevicePage.qml => NotificationsThingPage.qml} (70%) diff --git a/libnymea-app/models/newlogsmodel.cpp b/libnymea-app/models/newlogsmodel.cpp index 8f5071ea..6e031999 100644 --- a/libnymea-app/models/newlogsmodel.cpp +++ b/libnymea-app/models/newlogsmodel.cpp @@ -58,7 +58,7 @@ void NewLogsModel::componentComplete() bool NewLogsModel::canFetchMore(const QModelIndex &parent) const { Q_UNUSED(parent) - return m_canFetchMore; + return m_canFetchMore && m_sources.count() == 1; } void NewLogsModel::fetchMore(const QModelIndex &parent) @@ -185,6 +185,19 @@ void NewLogsModel::setSampleRate(SampleRate sampleRate) } } +Qt::SortOrder NewLogsModel::sortOrder() const +{ + return m_sortOrder; +} + +void NewLogsModel::setSortOrder(Qt::SortOrder sortOrder) +{ + if (m_sortOrder != sortOrder) { + m_sortOrder = sortOrder; + emit sortOrderChanged(); + } +} + bool NewLogsModel::busy() const { return m_busy; @@ -217,30 +230,30 @@ NewLogEntry *NewLogsModel::find(const QDateTime ×tamp) const 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) { + if (idx == 0) { // qCDebug(dcLogEngine()) << "Is oldest."; return entry; } - NewLogEntry *previousEntry = m_list.at(idx+1); + 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; + idx -= jump; } else if (entry->timestamp() < timestamp) { // qCDebug(dcLogEngine()) << "entry is older than searched:" << entry->timestamp().toString() << timestamp.toString(); - if (idx == 0) { + if (idx == m_list.count() - 1) { // qCDebug(dcLogEngine()) << "Is newest."; return entry; } - NewLogEntry *nextEntry = m_list.at(idx-1); + 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; + idx += jump; } jump = qMax(1, jump / 2); }; @@ -269,57 +282,52 @@ void NewLogsModel::fetchLogs() {"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 (m_sampleRate == SampleRateAny) { // Discrete logs + + if (!m_startTime.isNull() && !m_endTime.isNull()) { // Either specific time frame + params.insert("startTime", m_startTime.toMSecsSinceEpoch()); + params.insert("endTime", m_endTime.toMSecsSinceEpoch()); - 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; + params.insert("limit", m_blockSize); + if (m_list.count() > 0) { + params.insert("offset", m_lastOffset); + if (m_lastOffset == 0) { + if (m_endTime.isNull()) { + m_currentNewest = QDateTime::currentDateTime(); + } else { + m_currentNewest = m_endTime; + } + } + params.insert("endTime", m_currentNewest.toMSecsSinceEpoch()); + m_lastOffset += m_blockSize; } } - qCDebug(dcLogEngine()) << "Actual request:" << startTime.toString() << " - " << endTime.toString(); - params.insert("startTime", startTime.toMSecsSinceEpoch()); - params.insert("endTime", endTime.toMSecsSinceEpoch()); - QMetaEnum sampleRateEnum = QMetaEnum::fromType(); - 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() && !m_endTime.isNull()) { + params.insert("startTime", m_startTime.toMSecsSinceEpoch()); + params.insert("endTime", m_endTime.toMSecsSinceEpoch()); + + QMetaEnum sampleRateEnum = QMetaEnum::fromType(); + params.insert("sampleRate", sampleRateEnum.valueToKey(m_sampleRate)); + } else { + qCWarning(dcLogEngine()) << "startTime and endTime is required when asking for resampling"; + return; } } -// if (!m_startTime.isNull()) { -// params.insert("startTime", m_startTime.toMSecsSinceEpoch()); -// } -// if (!m_endTime.isNull()) { -// params.insert("endTime", m_endTime.toMSecsSinceEpoch()); -// } + QMetaEnum sortOrderEnum = QMetaEnum::fromType(); + params.insert("sortOrder", sortOrderEnum.valueToKey(m_sortOrder)); + 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) { + Q_UNUSED(commandId) QList entries; foreach (const QVariant &entryVariant, data.value("logEntries").toList()) { @@ -329,77 +337,37 @@ void NewLogsModel::logsReply(int commandId, const QVariantMap &data) 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; + qCDebug(dcLogEngine()) << "Logs received:" << entries.count(); - 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); + if (entries.empty()) { + return; + } - } else if (entries.last()->timestamp() == m_list.last()->timestamp()) { - qCDebug(dcLogEngine()) << "First item of new list already existing... no new data..."; - qDeleteAll(entries); + if (!m_startTime.isNull() && !m_endTime.isNull()) { + beginResetModel(); + QList oldEntries = m_list; + m_list.clear(); + endResetModel(); + emit entriesRemoved(0, oldEntries.count()); + qDeleteAll(oldEntries); - } 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); + beginInsertRows(QModelIndex(), 0, entries.count() - 1); + m_list = entries; + endInsertRows(); + emit entriesAdded(0, entries); - 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); - } + } else { + beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + entries.count() - 1); + qSort(entries.begin(), entries.end(), [](NewLogEntry *left, NewLogEntry *right){ + return left->timestamp() > right->timestamp(); + }); + m_list.append(entries); + endInsertRows(); + emit entriesAdded(m_list.count(), entries); } emit countChanged(); diff --git a/libnymea-app/models/newlogsmodel.h b/libnymea-app/models/newlogsmodel.h index ad95be1e..183757bc 100644 --- a/libnymea-app/models/newlogsmodel.h +++ b/libnymea-app/models/newlogsmodel.h @@ -19,6 +19,7 @@ class NewLogsModel : public QAbstractListModel, public QQmlParserStatus 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(Qt::SortOrder sortOrder READ sortOrder WRITE setSortOrder NOTIFY sortOrderChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) @@ -79,6 +80,9 @@ public: SampleRate sampleRate() const; void setSampleRate(SampleRate sampleRate); + Qt::SortOrder sortOrder() const; + void setSortOrder(Qt::SortOrder sortOrder); + bool busy() const; Q_INVOKABLE NewLogEntry *get(int index) const; @@ -101,6 +105,7 @@ signals: void startTimeChanged(); void endTimeChanged(); void sampleRateChanged(); + void sortOrderChanged(); void entriesAdded(int index, const QList &entries); void entriesRemoved(int index, int count); @@ -113,14 +118,21 @@ private: QStringList m_sources; QStringList m_columns; QVariantMap m_filter; + Qt::SortOrder m_sortOrder = Qt::AscendingOrder; + + bool m_busy = false; + + // For time based sampling QDateTime m_startTime; QDateTime m_endTime; SampleRate m_sampleRate = SampleRateAny; + // For continuous scrolling lists bool m_completed = false; bool m_canFetchMore = true; - bool m_busy = false; - int m_blockSize = 30; + int m_blockSize = 5; + int m_lastOffset = 0; + QDateTime m_currentNewest; QList m_list; }; diff --git a/libnymea-app/thingmanager.cpp b/libnymea-app/thingmanager.cpp index 744f5a76..ee0914b7 100644 --- a/libnymea-app/thingmanager.cpp +++ b/libnymea-app/thingmanager.cpp @@ -643,6 +643,11 @@ int ThingManager::executeBrowserItemAction(const QUuid &thingId, const QString & return m_jsonClient->sendCommand("Integrations.ExecuteBrowserItemAction", data, this, "executeBrowserItemActionResponse"); } +int ThingManager::setStateLogging(const QUuid &thingId, const QUuid &stateTypeId, bool enabled) +{ + return m_jsonClient->sendCommand("Integrations.SetStateLogging", {{"thingId", thingId}, {"stateTypeId", stateTypeId}, {"enabled", enabled}}, this, "setStateLoggingResponse"); +} + int ThingManager::connectIO(const QUuid &inputThingId, const QUuid &inputStateTypeId, const QUuid &outputThingId, const QUuid &outputStateTypeId, bool inverted) { QVariantMap data; @@ -694,6 +699,12 @@ void ThingManager::disconnectIOResponse(int commandId, const QVariantMap ¶ms qDebug() << "DisconnectIO response" << commandId << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); } +void ThingManager::setStateLoggingResponse(int commandId, const QVariantMap ¶ms) +{ + Q_UNUSED(commandId) + qCDebug(dcThingManager()) << "Set state logging response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); +} + Vendor *ThingManager::unpackVendor(const QVariantMap &vendorMap) { Vendor *v = new Vendor(vendorMap.value("id").toString(), vendorMap.value("name").toString()); diff --git a/libnymea-app/thingmanager.h b/libnymea-app/thingmanager.h index c8cb119c..7b7304a2 100644 --- a/libnymea-app/thingmanager.h +++ b/libnymea-app/thingmanager.h @@ -98,6 +98,7 @@ public: Q_INVOKABLE BrowserItem* browserItem(const QUuid &thingId, const QString &itemId); Q_INVOKABLE int executeBrowserItem(const QUuid &thingId, const QString &itemId); Q_INVOKABLE int executeBrowserItemAction(const QUuid &thingId, const QString &itemId, const QUuid &actionTypeId, const QVariantList ¶ms = QVariantList()); + Q_INVOKABLE int setStateLogging(const QUuid &thingId, const QUuid &stateTypeId, bool enabled); Q_INVOKABLE int connectIO(const QUuid &inputThingId, const QUuid &inputStateTypeId, const QUuid &outputThingId, const QUuid &outputStateTypeId, bool inverted); Q_INVOKABLE int disconnectIO(const QUuid &ioConnectionId); @@ -123,6 +124,7 @@ private: Q_INVOKABLE void getIOConnectionsResponse(int commandId, const QVariantMap ¶ms); Q_INVOKABLE void connectIOResponse(int commandId, const QVariantMap ¶ms); Q_INVOKABLE void disconnectIOResponse(int commandId, const QVariantMap ¶ms); + Q_INVOKABLE void setStateLoggingResponse(int commandId, const QVariantMap ¶ms); public slots: ThingGroup* createGroup(Interface *interface, ThingsProxy *things); diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 02aec37c..0a9fa341 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -50,7 +50,6 @@ ui/devicepages/ShutterDevicePage.qml ui/devicepages/GarageThingPage.qml ui/devicepages/AwningThingPage.qml - ui/devicepages/NotificationsDevicePage.qml ui/devicepages/LightThingPage.qml ui/devicepages/FingerprintReaderDevicePage.qml ui/devicepages/DeviceLogPage.qml @@ -312,5 +311,6 @@ ui/mainviews/airconditioning/LegendDelegate.qml ui/customviews/StateChart.qml ui/devicepages/ThingLogPage.qml + ui/devicepages/NotificationsThingPage.qml diff --git a/nymea-app/ui/customviews/StateChart.qml b/nymea-app/ui/customviews/StateChart.qml index 8b1d19f2..d8a87e65 100644 --- a/nymea-app/ui/customviews/StateChart.qml +++ b/nymea-app/ui/customviews/StateChart.qml @@ -55,10 +55,8 @@ Item { 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) + startTime: new Date(d.startTime.getTime() - d.range * 1.1 * 60000) + endTime: new Date(d.endTime.getTime() + d.range * 1.1 * 60000) sampleRate: d.sampleRate property double minValue @@ -66,11 +64,11 @@ Item { 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()) - } +// 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] @@ -83,17 +81,17 @@ Item { 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) + var insertIdx = (index + i) * 2 + valueSeries.insert(insertIdx, entry.timestamp.getTime() - 500, !value) + valueSeries.insert(insertIdx+1, 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)) - } + +// valueSeries.removePoints(0, 1); +// if (insertIdx == 0) { +// // first index, we'll have to update the "now" value +// valueSeries.insert(0, entry.timestamp.getTime() + 2000, value) +// zeroSeries.ensureValue(new Date(entry.timestamp.getTime() + 2000)) +// } } else { var value = entry.values[root.stateType.name] @@ -104,25 +102,27 @@ Item { minValue = minValue == undefined ? value : Math.min(minValue, value) maxValue = maxValue == undefined ? value : Math.max(maxValue, value) - var insertIdx = (index + i) + 1 + var insertIdx = index + i 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)) - } } } + + if (root.stateType.type.toLowerCase() == "bool") { + var last = valueSeries.at(valueSeries.count-1); + if (last.x < d.endTime) { + valueSeries.append(d.endTime, last.y) + zeroSeries.ensureValue(d.endTime) + } + } + 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) + valueSeries.removePoints((index * 2) /*+ 1*/, count * 2) } else { - valueSeries.removePoints(index + 1, count) + valueSeries.removePoints(index /*+ 1*/, count) } zeroSeries.shrink() @@ -291,24 +291,24 @@ Item { borderWidth: 2 lowerSeries: LineSeries { id: zeroSeries - XYPoint { x: dateTimeAxis.max.getTime(); y: 0 } XYPoint { x: dateTimeAxis.min.getTime(); y: 0 } + XYPoint { x: dateTimeAxis.max.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 { + append(timestamp, 0) } } else { - if (timestamp.getTime() < at(1).x) { - remove(1) - append(timestamp, 0) - } else if (timestamp.getTime() > at(0).x) { + if (timestamp.getTime() < at(0).x) { remove(0) insert(0, timestamp, 0) + } else if (timestamp.getTime() > at(1).x) { + remove(1) + append(timestamp, 0) } } } @@ -468,14 +468,14 @@ Item { 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) + property NewLogEntry entry: logsModel.count > 0 ? logsModel.find(timestamp) : null // 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 + property var value: entry ? entry.values[root.stateType.name] : null 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 @@ -497,16 +497,11 @@ Item { 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 - ? "" + text: toolTip.value === null + ? qsTr("No data") : 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) + : Types.toUiValue(toolTip.value, root.stateType.unit).toFixed(root.roundTo) + Types.toUiUnit(root.stateType.unit) font: Style.smallFont } diff --git a/nymea-app/ui/devicepages/ButtonThingPage.qml b/nymea-app/ui/devicepages/ButtonThingPage.qml index c9a22c15..a24db2c9 100644 --- a/nymea-app/ui/devicepages/ButtonThingPage.qml +++ b/nymea-app/ui/devicepages/ButtonThingPage.qml @@ -44,54 +44,161 @@ ThingPageBase { readonly property State powerState: thing ? thing.stateByName("power") : null - EmptyViewPlaceholder { - anchors { left: parent.left; right: parent.right; margins: app.margins } - anchors.verticalCenter: parent.verticalCenter - - title: qsTr("This switch has not been used yet.") - text: qsTr("Press a button on the switch to see logs appearing here.") - visible: !logsModel.busy && logsModel.count === 0 && !root.isVirtual - buttonVisible: false - imageSource: "../images/system-shutdown.svg" - } - - GenericTypeLogView { - id: logView + Loader { anchors.fill: parent visible: !root.isVirtual - - logsModel: LogsModel { - id: logsModel - engine: _engine - thingId: root.thing.id - live: true - typeIds: { - var ret = []; - ret.push(root.thing.thingClass.eventTypes.findByName("pressed").id) - if (root.thing.thingClass.eventTypes.findByName("longPressed")) { - ret.push(root.thing.thingClass.eventTypes.findByName("longPressed").id) - } - return ret; + sourceComponent: { + if (engine.jsonRpcClient.ensureServerVersion("8.0")) { + return logViewComponent + } else { + return logViewComponentPre80 } } + } - onAddRuleClicked: { - var value = logView.logsModel.get(index).value - var typeId = logView.logsModel.get(index).typeId - var rule = engine.ruleManager.createNewRule(); - var eventDescriptor = rule.eventDescriptors.createNewEventDescriptor(); - eventDescriptor.thingId = root.thing.id; - var eventType = root.thing.thingClass.eventTypes.getEventType(typeId); - eventDescriptor.eventTypeId = eventType.id; - rule.name = root.thing.name + " - " + eventType.displayName; - if (eventType.paramTypes.count === 1) { - var paramType = eventType.paramTypes.get(0); - eventDescriptor.paramDescriptors.setParamDescriptor(paramType.id, value, ParamDescriptor.ValueOperatorEquals); - rule.eventDescriptors.addEventDescriptor(eventDescriptor); - rule.name = rule.name + " - " + value + Component { + id: logViewComponent + + ListView { + id: logView + anchors.fill: parent + ScrollBar.vertical: ScrollBar {} + + model: NewLogsModel { + id: logsModel + engine: _engine + sources: ["event-" + root.thing.id + "-pressed", "event-" + root.thing.id + "-longPressed"] +// live: true + } + + delegate: NymeaItemDelegate { + id: entryDelegate + width: logView.width + + property NewLogEntry entry: logsModel.get(index) + property EventType eventType: { + switch (entry.source) { + case "event-" + root.thing.id + "-pressed": + return root.thing.thingClass.eventTypes.findByName("pressed") + case "event-" + root.thing.id + "-longPressed": + return root.thing.thingClass.eventTypes.findByName("longPressed") + } + return null + } + + contentItem: ColumnLayout { + RowLayout { + Label { + text: entryDelegate.eventType.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 + } + } + Label { + Layout.fillWidth: true + text: { + 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(", ") + } + } + } + } + +// onAddRuleClicked: { +// var value = logView.logsModel.get(index).value +// var typeId = logView.logsModel.get(index).typeId +// var rule = engine.ruleManager.createNewRule(); +// var eventDescriptor = rule.eventDescriptors.createNewEventDescriptor(); +// eventDescriptor.thingId = root.thing.id; +// var eventType = root.thing.thingClass.eventTypes.getEventType(typeId); +// eventDescriptor.eventTypeId = eventType.id; +// rule.name = root.thing.name + " - " + eventType.displayName; +// if (eventType.paramTypes.count === 1) { +// var paramType = eventType.paramTypes.get(0); +// eventDescriptor.paramDescriptors.setParamDescriptor(paramType.id, value, ParamDescriptor.ValueOperatorEquals); +// rule.eventDescriptors.addEventDescriptor(eventDescriptor); +// rule.name = rule.name + " - " + value +// } +// var rulePage = pageStack.push(Qt.resolvedUrl("../magic/ThingRulesPage.qml"), {thing: root.thing}); +// rulePage.addRule(rule); +// } + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + + title: qsTr("This switch has not been used yet.") + text: qsTr("Press a button on the switch to see logs appearing here.") + visible: !logsModel.busy && logsModel.count === 0 + buttonVisible: false + imageSource: "../images/system-shutdown.svg" + } + } + } + + Component { + id: logViewComponentPre80 + + GenericTypeLogView { + id: logView + anchors.fill: parent + + logsModel: LogsModel { + id: logsModel + engine: _engine + thingId: root.thing.id + live: true + typeIds: { + var ret = []; + ret.push(root.thing.thingClass.eventTypes.findByName("pressed").id) + if (root.thing.thingClass.eventTypes.findByName("longPressed")) { + ret.push(root.thing.thingClass.eventTypes.findByName("longPressed").id) + } + return ret; + } + } + + onAddRuleClicked: { + var value = logView.logsModel.get(index).value + var typeId = logView.logsModel.get(index).typeId + var rule = engine.ruleManager.createNewRule(); + var eventDescriptor = rule.eventDescriptors.createNewEventDescriptor(); + eventDescriptor.thingId = root.thing.id; + var eventType = root.thing.thingClass.eventTypes.getEventType(typeId); + eventDescriptor.eventTypeId = eventType.id; + rule.name = root.thing.name + " - " + eventType.displayName; + if (eventType.paramTypes.count === 1) { + var paramType = eventType.paramTypes.get(0); + eventDescriptor.paramDescriptors.setParamDescriptor(paramType.id, value, ParamDescriptor.ValueOperatorEquals); + rule.eventDescriptors.addEventDescriptor(eventDescriptor); + rule.name = rule.name + " - " + value + } + var rulePage = pageStack.push(Qt.resolvedUrl("../magic/ThingRulesPage.qml"), {thing: root.thing}); + rulePage.addRule(rule); + } + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + + title: qsTr("This switch has not been used yet.") + text: qsTr("Press a button on the switch to see logs appearing here.") + visible: !logsModel.busy && logsModel.count === 0 && !root.isVirtual + buttonVisible: false + imageSource: "../images/system-shutdown.svg" } - var rulePage = pageStack.push(Qt.resolvedUrl("../magic/ThingRulesPage.qml"), {thing: root.thing}); - rulePage.addRule(rule); } } diff --git a/nymea-app/ui/devicepages/GenericThingPage.qml b/nymea-app/ui/devicepages/GenericThingPage.qml index 65c927a8..bd763ef6 100644 --- a/nymea-app/ui/devicepages/GenericThingPage.qml +++ b/nymea-app/ui/devicepages/GenericThingPage.qml @@ -80,8 +80,11 @@ ThingPageBase { Layout.fillWidth: true topPadding: model.type === ThingModel.TypeActionType ? app.margins / 2 : 0 bottomPadding: 0 + contentItem: Loader { id: inlineLoader + Layout.fillWidth: true + Layout.preferredHeight: Style.smallDelegateHeight sourceComponent: { switch (model.type) { case ThingModel.TypeStateType: @@ -113,7 +116,19 @@ ThingPageBase { } } - onClicked: swipe.close() + onClicked: { + print("clicked") + if (swipe.complete) { + swipe.close() + } else { + swipe.open(SwipeDelegate.Right) + } + } + Connections { + target: flickable + onContentYChanged: if (swipe.completed) swipe.close() + } + onPressAndHold: swipe.open(SwipeDelegate.Right) swipe.right: RowLayout { height: delegate.height diff --git a/nymea-app/ui/devicepages/LightThingPage.qml b/nymea-app/ui/devicepages/LightThingPage.qml index bac051a5..6e1eb9a2 100644 --- a/nymea-app/ui/devicepages/LightThingPage.qml +++ b/nymea-app/ui/devicepages/LightThingPage.qml @@ -43,13 +43,13 @@ ThingPageBase { readonly property State powerState: thing.stateByName("power") readonly property State brightnessState: thing.stateByName("brightness") - readonly property ActionType brightnessActionType: thingClass.actionTypes.findByName("brightness"); + readonly property ActionType brightnessActionType: thing.thingClass.actionTypes.findByName("brightness"); readonly property State colorState: thing.stateByName("color") - readonly property StateType ctStateType: thingClass.stateTypes.findByName("colorTemperature") + readonly property StateType ctStateType: thing.thingClass.stateTypes.findByName("colorTemperature") readonly property State ctState: thing.stateByName("colorTemperature") - readonly property ActionType ctActionType: thingClass.actionTypes.findByName("colorTemperature") + readonly property ActionType ctActionType: thing.thingClass.actionTypes.findByName("colorTemperature") readonly property int statesCount: (powerState !== null ? 1 : 0) + (brightnessState !== null ? 1 : 0) + diff --git a/nymea-app/ui/devicepages/NotificationsDevicePage.qml b/nymea-app/ui/devicepages/NotificationsThingPage.qml similarity index 70% rename from nymea-app/ui/devicepages/NotificationsDevicePage.qml rename to nymea-app/ui/devicepages/NotificationsThingPage.qml index 24f71bdf..767fd6c4 100644 --- a/nymea-app/ui/devicepages/NotificationsDevicePage.qml +++ b/nymea-app/ui/devicepages/NotificationsThingPage.qml @@ -136,12 +136,33 @@ ThingPageBase { } + Loader { + Layout.fillWidth: true + Layout.fillHeight: true + sourceComponent: { + if (engine.jsonRpcClient.ensureServerVersion("8.0")) { + return logViewComponent + } else { + return logViewComponentPre80 + } + } + } + } + + Component { + id: logViewComponentPre80 ListView { id: logView Layout.fillHeight: true Layout.fillWidth: true clip: true + BusyIndicator { + anchors.centerIn: parent + visible: logsModel.busy + running: visible + } + model: LogsModel { id: logsModel thingId: root.thing.id @@ -206,7 +227,7 @@ ThingPageBase { { timestamp: model.timestamp, notificationTitle: itemDelegate.title, - notificationBody: itemDelegate.text, + notificationBody: itemDelegate.tet, errorCode: model.errorCode }); popup.open(); @@ -225,10 +246,98 @@ ThingPageBase { } } - BusyIndicator { - anchors.centerIn: parent - visible: logsModel.busy - running: visible + Component { + id: logViewComponent + ListView { + id: logView + Layout.fillHeight: true + Layout.fillWidth: true + clip: true + + BusyIndicator { + anchors.centerIn: parent + visible: logsModel.busy + running: visible + } + + model: NewLogsModel { + id: logsModel + engine: _engine +// live: true + source: "action-" + root.thing.id + "-notify" + } + + delegate: BigTile { + id: itemDelegate + showHeader: false + width: logView.width - app.margins + anchors.horizontalCenter: parent.horizontalCenter + + property var params: JSON.parse(model.values.params) + + contentItem: RowLayout { + ColumnLayout { + Label { + Layout.fillWidth: true + text: itemDelegate.params.title + elide: Text.ElideRight + } + GridLayout { + Layout.fillWidth: true + columns: textLabel.implicitWidth + dateLayout.implicitWidth < width ? 2 : 1 + + Label { + id: textLabel + Layout.fillWidth: true + text: itemDelegate.params.body + font.pixelSize: app.smallFont + wrapMode: Text.WordWrap + } + + RowLayout { + id: dateLayout + Layout.fillWidth: true + spacing: app.margins / 2 + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + text: Qt.formatDateTime(model.timestamp) + font.pixelSize: app.extraSmallFont + } + ColorIcon { + Layout.preferredWidth: Style.smallIconSize + Layout.preferredHeight: Style.smallIconSize + name: "../images/dialog-warning-symbolic.svg" + color: "red" + visible: model.values.status !== "ThingErrorNoError" + } + } + } + } + } + + onClicked: { + var popup = detailsPopup.createObject(root, + { + timestamp: model.timestamp, + notificationTitle: itemDelegate.params.title, + notificationBody: itemDelegate.params.body, + errorCode: model.status + }); + popup.open(); + } + } + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + title: qsTr("No messages sent yet.") + text: qsTr("Sent messages will appear here.") + imageSource: "../images/messaging-app-symbolic.svg" + buttonVisible: false + visible: logsModel.count == 0 && !logsModel.busy + } + } } Component { @@ -248,7 +357,7 @@ ThingPageBase { Label { Layout.fillWidth: true - text: detailsDialog.errorCode == "" ? qsTr("Date sent") : qsTr("Sending failed") + text: detailsDialog.errorCode == "" || detailsDialog.errorCode == "ThingErrorNoError" ? qsTr("Date sent") : qsTr("Sending failed") font.bold: true } Label { @@ -266,7 +375,7 @@ ThingPageBase { } Label { - Layout.topMargin: app.margins + Layout.topMargin: Style.margins Layout.fillWidth: true text: qsTr("Title") font.bold: true diff --git a/nymea-app/ui/devicepages/SensorDevicePage.qml b/nymea-app/ui/devicepages/SensorDevicePage.qml index ddceecb9..7a858dbb 100644 --- a/nymea-app/ui/devicepages/SensorDevicePage.qml +++ b/nymea-app/ui/devicepages/SensorDevicePage.qml @@ -73,7 +73,7 @@ ThingPageBase { Component.onCompleted: { var supportedInterfaces = Object.keys(interfaceStateMap) for (var i = 0; i < supportedInterfaces.length; i++) { - if (root.thingClass.interfaces.indexOf(supportedInterfaces[i]) >= 0) { + if (root.thing.thingClass.interfaces.indexOf(supportedInterfaces[i]) >= 0) { append({name: supportedInterfaces[i]}); } } @@ -138,7 +138,7 @@ ThingPageBase { Layout.fillWidth: true Layout.preferredHeight: item.implicitHeight - property StateType stateType: root.thingClass.stateTypes.findByName(interfaceStateMap[modelData]) + property StateType stateType: root.thing.thingClass.stateTypes.findByName(interfaceStateMap[modelData]) property State state: root.thing.stateByName(interfaceStateMap[modelData]) property string interfaceName: modelData diff --git a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml index 71c0eb3d..f4e0d51b 100644 --- a/nymea-app/ui/devicepages/SmartMeterDevicePage.qml +++ b/nymea-app/ui/devicepages/SmartMeterDevicePage.qml @@ -41,9 +41,9 @@ ThingPageBase { readonly property bool isEnergyMeter: root.thing && root.thing.thingClass.interfaces.indexOf("energymeter") >= 0 readonly property bool isConsumer: root.thing && root.thing.thingClass.interfaces.indexOf("smartmeterconsumer") >= 0 - readonly property bool isProducer: root.thing && root.thingClass.interfaces.indexOf("smartmeterproducer") >= 0 - readonly property bool isBattery: root.thing && root.thingClass.interfaces.indexOf("energystorage") >= 0 - readonly property bool isEvCharger: itemDelegate.thing.thingClass.interfaces.indexOf("evcharger") >= 0 + readonly property bool isProducer: root.thing && root.thing.thingClass.interfaces.indexOf("smartmeterproducer") >= 0 + readonly property bool isBattery: root.thing && root.thing.thingClass.interfaces.indexOf("energystorage") >= 0 + readonly property bool isEvCharger: root.thing && root.thing.thingClass.interfaces.indexOf("evcharger") >= 0 readonly property State currentPowerState: root.thing.stateByName("currentPower") diff --git a/nymea-app/ui/devicepages/StateLogPage.qml b/nymea-app/ui/devicepages/StateLogPage.qml index 955546c7..476060dd 100644 --- a/nymea-app/ui/devicepages/StateLogPage.qml +++ b/nymea-app/ui/devicepages/StateLogPage.qml @@ -58,14 +58,30 @@ Page { header: NymeaHeader { text: qsTr("History for %1").arg(root.stateType.displayName) onBackPressed: pageStack.pop() + + HeaderButton { + imageSource: "delete" + onClicked: { + var popup = deleteLogsComponent.createObject(root) + popup.open() + } + + Component { + id: deleteLogsComponent + NymeaDialog { + title: qsTr("Remove logs?") + text: qsTr("Do you want to remove the log for this state and disable logging?") + onAccepted: engine.thingManager.setStateLogging(root.thing.id, root.stateType.id, false) + } + } + } } NewLogsModel { id: logsModel engine: _engine - columns: [root.stateType.name] - source: "states-" + root.thing.id - filter: ({state: root.stateType.name}) + source: "state-" + root.thing.id + "-" + root.stateType.name + sortOrder: Qt.DescendingOrder } Component.onCompleted: { @@ -75,13 +91,21 @@ Page { GridLayout { anchors.fill: parent columns: app.landscape ? 2 : 1 + visible: root.isLogged - StateChart { + Loader { Layout.fillWidth: true - thing: root.thing - stateType: root.stateType + active: root.canShowGraph + + sourceComponent: Component { + StateChart { + thing: root.thing + stateType: root.stateType + } + } } + ListView { id: listView Layout.fillWidth: true @@ -100,61 +124,6 @@ Page { 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 { @@ -166,9 +135,9 @@ Page { buttonText: qsTr("Enable logging") visible: !root.isLogged onButtonClicked: { - + print("enabming logging") + engine.thingManager.setStateLogging(root.thing.id, root.stateType.id, true) } - } } diff --git a/nymea-app/ui/devicepages/ThermostatDevicePage.qml b/nymea-app/ui/devicepages/ThermostatDevicePage.qml index e39e2a1d..59df13f4 100644 --- a/nymea-app/ui/devicepages/ThermostatDevicePage.qml +++ b/nymea-app/ui/devicepages/ThermostatDevicePage.qml @@ -44,7 +44,7 @@ ThingPageBase { readonly property StateType targetTemperatureStateType: thing.thingClass.stateTypes.findByName("targetTemperature") readonly property State targetTemperatureState: targetTemperatureStateType ? thing.states.getState(targetTemperatureStateType.id) : null - readonly property StateType powerStateType: thingClass.stateTypes.findByName("power") + readonly property StateType powerStateType: thing.thingClass.stateTypes.findByName("power") readonly property State powerState: powerStateType ? thing.states.getState(powerStateType.id) : null readonly property StateType temperatureStateType: thing.thingClass.stateTypes.findByName("temperature") readonly property State temperatureState: temperatureStateType ? thing.states.getState(temperatureStateType.id) : null diff --git a/nymea-app/ui/devicepages/ThingLogPage.qml b/nymea-app/ui/devicepages/ThingLogPage.qml index a71eef18..7854fbe7 100644 --- a/nymea-app/ui/devicepages/ThingLogPage.qml +++ b/nymea-app/ui/devicepages/ThingLogPage.qml @@ -47,41 +47,51 @@ Page { HeaderButton { imageSource: "../images/filters.svg" - color: logsModelNg.filterEnabled ? Style.accentColor : Style.iconColor - onClicked: logsModelNg.filterEnabled = !logsModelNg.filterEnabled + color: logsModel.filterEnabled ? Style.accentColor : Style.iconColor + onClicked: logsModel.filterEnabled = !logsModel.filterEnabled visible: root.filterTypeIds.length === 0 } } NewLogsModel { - id: logsModelNg + id: logsModel engine: _engine - columns: [root.stateType.name] - sources: ["states-" + root.thing.id, "events-" + root.thing.id, "actions-" + root.thing.id] - filter: { - if (!filterEnabled) { - return ({}) +// columns: [root.stateType.name] + sources: { + var ret = [] + if (filterEnabled) { + if (isStateFilter) { + ret.push("state-" + root.thing.id + "-" + filterTypeName) + } else if (isEventFilter) { + ret.push("event-" + root.thing.id + "-" + filterTypeName) + } else if (isActionFilter) { + ret.push("action-" + root.thing.id + "-" + filterTypeName) + } + return ret; } - print("*** filter updated", isStateFilter, isEventFilter, isActionFilter, filterTypeName, thing.thingClass.stateTypes.findByName(filterTypeName)) - if (isStateFilter) { - return ({state: filterTypeName}) + + for (var i = 0; i < root.thing.thingClass.stateTypes.count; i++) { + var stateType = root.thing.thingClass.stateTypes.get(i) + ret.push("state-" + root.thing.id + "-" + stateType.name) } - if (isEventFilter) { - return ({event: filterTypeName}) + for (var i = 0; i < root.thing.thingClass.eventTypes.count; i++) { + var eventType = root.thing.thingClass.eventTypes.get(i) + ret.push("event-" + root.thing.id + "-" + eventType.name) } - if (isActionFilter) { - return ({action: filterTypeName}) + for (var i = 0; i < root.thing.thingClass.actionTypes.count; i++) { + var actionType = root.thing.thingClass.actionTypes.get(i) + ret.push("action-" + root.thing.id + "-" + actionType.name) } - return ({}) + return ret; } 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() + onSourcesChanged: { + logsModel.clear() + logsModel.fetchLogs() } // thingId: root.thing.id @@ -109,7 +119,7 @@ Page { 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 + height: logsModel.filterEnabled ? implicitHeight + app.margins * 2 : 0 Material.elevation: 1 leftPadding: 0; rightPadding: 0; topPadding: 0; bottomPadding: 0 @@ -180,22 +190,22 @@ Page { ListView { anchors { left: parent.left; top: graphLoader.bottom; right: parent.right; bottom: parent.bottom } clip: true - model: logsModelNg + model: logsModel ScrollBar.vertical: ScrollBar {} BusyIndicator { anchors.centerIn: parent - visible: logsModelNg.busy + visible: logsModel.busy } delegate: ItemDelegate { id: entryDelegate width: parent.width - property NewLogEntry entry: logsModelNg.get(index) + property NewLogEntry entry: logsModel.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 + property StateType stateType: entry && entry.source.indexOf("state-") == 0 ? root.thing.thingClass.stateTypes.findByName(entry.source.replace(/.*-.*-/, "")) : null + property EventType eventType: entry && entry.source.indexOf("event-") == 0 ? root.thing.thingClass.eventTypes.findByName(entry.source.replace(/.*-.*-/, "")) : null + property ActionType actionType: entry && entry.source.indexOf("action-") == 0 ? root.thing.thingClass.actionTypes.findByName(entry.source.replace(/.*-.*-/, "")) : null contentItem: RowLayout { ColorIcon { @@ -280,7 +290,7 @@ Page { when: entryDelegate.stateType != null target: valueLoader.item; property: "value"; - value: entryDelegate.stateType ? Types.toUiValue(entry.values[entry.values.state], entryDelegate.stateType.unit) : "" + value: entryDelegate.stateType ? Types.toUiValue(entry.values[entryDelegate.stateType.name], entryDelegate.stateType.unit) : "" } Binding { when: entryDelegate.stateType != null diff --git a/nymea-app/ui/devicepages/ThingPageBase.qml b/nymea-app/ui/devicepages/ThingPageBase.qml index ed6e22bd..6f57f555 100644 --- a/nymea-app/ui/devicepages/ThingPageBase.qml +++ b/nymea-app/ui/devicepages/ThingPageBase.qml @@ -37,9 +37,8 @@ import "../components" Page { id: root property Thing thing: null - readonly property ThingClass thingClass: thing.thingClass - property bool showLogsButton: true + property bool showLogsButton: false property bool showDetailsButton: true property bool showBrowserButton: true property bool popStackOnBackButton: true @@ -59,7 +58,7 @@ Page { HeaderButton { imageSource: "../images/folder.svg" - visible: root.thingClass.browsable && root.showBrowserButton + visible: root.thing.thingClass.browsable && root.showBrowserButton onClicked: { pageStack.push(Qt.resolvedUrl("DeviceBrowserPage.qml"), {thing: root.thing}) } diff --git a/nymea-app/ui/devicepages/WeatherDevicePage.qml b/nymea-app/ui/devicepages/WeatherDevicePage.qml index b745ffc4..7b6b241a 100644 --- a/nymea-app/ui/devicepages/WeatherDevicePage.qml +++ b/nymea-app/ui/devicepages/WeatherDevicePage.qml @@ -71,7 +71,7 @@ ThingPageBase { id: tempComponent StateChart { thing: root.thing - stateType: root.thingClass.stateTypes.findByName("temperature") + stateType: root.thing.thingClass.stateTypes.findByName("temperature") color: app.interfaceToColor("temperaturesensor") } } @@ -82,7 +82,7 @@ ThingPageBase { GenericTypeGraph { Layout.fillWidth: true thing: root.thing - stateType: root.thingClass.stateTypes.findByName("temperature") + stateType: root.thing.thingClass.stateTypes.findByName("temperature") iconSource: app.interfaceToIcon("temperaturesensor") color: app.interfaceToColor("temperaturesensor") } @@ -102,7 +102,7 @@ ThingPageBase { id: humidityComponent StateChart { thing: root.thing - stateType: root.thingClass.stateTypes.findByName("humidity") + stateType: root.thing.thingClass.stateTypes.findByName("humidity") color: app.interfaceToColor("humiditysensor") } } @@ -112,7 +112,7 @@ ThingPageBase { GenericTypeGraph { Layout.fillWidth: true thing: root.thing - stateType: root.thingClass.stateTypes.findByName("humidity") + stateType: root.thing.thingClass.stateTypes.findByName("humidity") iconSource: app.interfaceToIcon("humiditysensor") color: app.interfaceToColor("humiditysensor") } @@ -132,7 +132,7 @@ ThingPageBase { id: pressureComponent StateChart { thing: root.thing - stateType: root.thingClass.stateTypes.findByName("pressure") + stateType: root.thing.thingClass.stateTypes.findByName("pressure") color: app.interfaceToColor("pressuresensor") } } @@ -142,7 +142,7 @@ ThingPageBase { GenericTypeGraph { Layout.fillWidth: true thing: root.thing - stateType: root.thingClass.stateTypes.findByName("pressure") + stateType: root.thing.thingClass.stateTypes.findByName("pressure") iconSource: app.interfaceToIcon("pressuresensor") color: app.interfaceToColor("pressuresensor") } @@ -162,7 +162,7 @@ ThingPageBase { id: windSpeedComponent StateChart { thing: root.thing - stateType: root.thingClass.stateTypes.findByName("windSpeed") + stateType: root.thing.thingClass.stateTypes.findByName("windSpeed") color: app.interfaceToColor("windspeedsensor") } } @@ -172,7 +172,7 @@ ThingPageBase { GenericTypeGraph { Layout.fillWidth: true thing: root.thing - stateType: root.thingClass.stateTypes.findByName("windSpeed") + stateType: root.thing.thingClass.stateTypes.findByName("windSpeed") iconSource: app.interfaceToIcon("windspeedsensor") color: app.interfaceToColor("windspeedsensor") } diff --git a/nymea-app/ui/mainviews/airconditioning/ACChartsPage.qml b/nymea-app/ui/mainviews/airconditioning/ACChartsPage.qml index fa003883..e14a4089 100644 --- a/nymea-app/ui/mainviews/airconditioning/ACChartsPage.qml +++ b/nymea-app/ui/mainviews/airconditioning/ACChartsPage.qml @@ -207,8 +207,7 @@ Page { readonly property NewLogsModel logsModel: NewLogsModel { objectName: "temp: " + thing.name engine: _engine - source: "states-" + thing.id - filter: ({state: "temperature"}) + source: "state-" + thing.id + "-temperature" startTime: new Date(d.startTime.getTime() - d.range * 60000) endTime: new Date(d.endTime.getTime() + d.range * 60000) sampleRate: d.sampleRate @@ -258,8 +257,7 @@ Page { readonly property NewLogsModel logsModel: NewLogsModel { objectName: "temp: " + thing.name engine: _engine - source: "states-" + thing.id - filter: ({state: "temperature"}) + source: "state-" + thing.id + "-temperature" startTime: new Date(d.startTime.getTime() - d.range * 60000) endTime: new Date(d.endTime.getTime() + d.range * 60000) sampleRate: d.sampleRate @@ -309,8 +307,7 @@ Page { readonly property NewLogsModel logsModel: NewLogsModel { objectName: "hum: " + thing.name engine: _engine - source: "states-" + thing.id - filter: ({state: "humidity"}) + source: "state-" + thing.id + "-humidity" startTime: new Date(d.startTime.getTime() - d.range * 60000) endTime: new Date(d.endTime.getTime() + d.range * 60000) sampleRate: d.sampleRate @@ -344,38 +341,37 @@ Page { Component.onDestruction: { chartView.removeSeries(series) } - } } Repeater { id: vocRepeater -// model: zoneWrapper.indoorVocSensors + model: zoneWrapper.indoorVocSensors delegate: Item { id: vocDelegate readonly property Thing thing: zoneWrapper.indoorVocSensors.get(index) property XYSeries series: null - readonly property LogsModel logsModel: LogsModel { + readonly property NewLogsModel logsModel: NewLogsModel { objectName: "voc: " + 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("voc").id) - return ret; + engine: _engine + source: "state-" + thing.id + "-voc" + 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["voc"] + if (value == null) { + value = 0; + } + series.insert(index + i, entry.timestamp, value) + } } - } - - XYSeriesAdapter { - logsModel: vocDelegate.logsModel - xySeries: series - sampleRate: XYSeriesAdapter.SampleRate10Minutes + onEntriesRemoved: { + series.removePoints(index, count) + } + Component.onCompleted: fetchLogs() } Component.onCompleted: { @@ -411,18 +407,13 @@ Page { XYPoint {x: dateTimeAxis.max.getTime(); y: 0} } - readonly property LogsModel logsModel: LogsModel { + readonly property NewLogsModel logsModel: NewLogsModel { id: logsModelNg engine: typeIds.length ? _engine : null - thingId: thing ? thing.id : "" - typeIds: { - var ret = []; - ret.push(thing.thingClass.stateTypes.findByName("closed").id) - return ret; - } - sourceFilter: LogsModel.SourceStates - live: true - viewStartTime: new Date(d.startTime.getTime() - d.range * 60000) + source: "state-" + thing.id + "-closed" + startTime: new Date(d.startTime.getTime() - d.range * 60000) + endTime: new Date(d.endTime.getTime() + d.range * 60000) + sampleRate: d.sampleRate } BoolSeriesAdapter { @@ -675,8 +666,9 @@ Page { delegate: TooltipDelegate { visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging thing: thermostatsRepeater.itemAt(index).thing - entry: thermostatsRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + entry: thermostatsRepeater.itemAt(index).logsModel.find(tooltips.timestamp) color: app.interfaceToColor("temperaturesensor") + valueName: "temperature" axis: temperatureAxis x: tooltips.tooltipX width: tooltips.tooltipWidth @@ -693,7 +685,8 @@ Page { delegate: TooltipDelegate { visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging thing: tempRepeater.itemAt(index).thing - entry: tempRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + entry: tempRepeater.itemAt(index).logsModel.find(tooltips.timestamp) + valueName: "temperature" color: app.interfaceToColor("temperaturesensor") axis: temperatureAxis x: tooltips.tooltipX @@ -711,8 +704,9 @@ Page { delegate: TooltipDelegate { visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging thing: humidityRepeater.itemAt(index).thing - entry: humidityRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + entry: humidityRepeater.itemAt(index).logsModel.find(tooltips.timestamp) color: app.interfaceToColor("humiditysensor") + valueName: "humidity" axis: humidityAxis x: tooltips.tooltipX width: tooltips.tooltipWidth @@ -729,7 +723,8 @@ Page { delegate: TooltipDelegate { visible: (mouseArea.containsMouse || mouseArea.tooltipping) && !mouseArea.dragging thing: vocRepeater.itemAt(index).thing - entry: vocRepeater.itemAt(index).logsModel.findClosest(tooltips.timestamp) + entry: vocRepeater.itemAt(index).logsModel.find(tooltips.timestamp) + valueName: "voc" color: app.interfaceToColor("vocsensor") axis: vocAxis x: tooltips.tooltipX diff --git a/nymea-app/ui/mainviews/airconditioning/TooltipDelegate.qml b/nymea-app/ui/mainviews/airconditioning/TooltipDelegate.qml index f3613a07..28a9b789 100644 --- a/nymea-app/ui/mainviews/airconditioning/TooltipDelegate.qml +++ b/nymea-app/ui/mainviews/airconditioning/TooltipDelegate.qml @@ -8,16 +8,19 @@ import Nymea.AirConditioning 1.0 import QtCharts 2.3 NymeaToolTip { + id: root width: layout.implicitWidth + Style.smallMargins * 2 height: layout.implicitHeight + Style.smallMargins * 2 property Thing thing: null - property LogEntry entry: null + property NewLogEntry entry: null + property string valueName: "" property alias color: rect.color property ValueAxis axis: null property int unit: Types.UnitNone - readonly property int realY: entry ? Math.min(Math.max(mouseArea.height - (entry.value * mouseArea.height / axis.max) - height / 2 /*- Style.margins*/, 0), mouseArea.height - height) : 0 + readonly property var value: entry.values[valueName] + readonly property int realY: entry ? Math.min(Math.max(mouseArea.height - (root.value * mouseArea.height / axis.max) - height / 2 /*- Style.margins*/, 0), mouseArea.height - height) : 0 property int fixedY: 0 y: fixedY // Animated @@ -32,7 +35,7 @@ NymeaToolTip { height: width } Label { - text: "%1: %2%3".arg(thing.name).arg(entry ? round(Types.toUiValue(entry.value, unit)) : "-").arg(Types.toUiUnit(unit)) + text: "%1: %2%3".arg(thing.name).arg(entry ? round(Types.toUiValue(root.value, unit)) : "-").arg(Types.toUiUnit(unit)) Layout.fillWidth: true font: Style.extraSmallFont elide: Text.ElideMiddle diff --git a/nymea-app/ui/mainviews/energy/ConsumersHistory.qml b/nymea-app/ui/mainviews/energy/ConsumersHistory.qml index 70b84965..432e521e 100644 --- a/nymea-app/ui/mainviews/energy/ConsumersHistory.qml +++ b/nymea-app/ui/mainviews/energy/ConsumersHistory.qml @@ -37,7 +37,7 @@ Item { } onEntriesRemoved: { - consumptionUpperSeries.removePoints(index, count) + consumptionUpperSeries.removePoints(index, Math.min(count, consumptionUpperSeries.count)) zeroSeries.shrink() } } @@ -423,8 +423,12 @@ Item { // Remove the leading 0-value entry - lowerSeries.removePoints(0, 1); - upperSeries.removePoints(0, 1); + if (lowerSeries.count > 0) { + lowerSeries.removePoints(0, 1); + } + if (upperSeries.count > 0) { + upperSeries.removePoints(0, 1); + } @@ -472,12 +476,25 @@ Item { } onEntriesRemoved: { - // Remove the leading 0-value entry - consumerDelegate.lowerSeries.removePoints(0, 1); - consumerDelegate.upperSeries.removePoints(0, 1); + // Note QtCharts crash when calling removePoints() for points that don't exist. + // Additionally it may decide to ignore values we add, e.g. if we try to add an Inf or undefined value for whatever reason + // So, even though in theory the series should always 1:1 reflect the model, it may not do so in practice and we'll have to make sure not crash here - consumerDelegate.lowerSeries.removePoints(index, count) - consumerDelegate.upperSeries.removePoints(index, count) + // Remove the leading 0-value entry + if (consumerDelegate.lowerSeries.count > 0) { + consumerDelegate.lowerSeries.removePoints(0, 1); + } + if (consumerDelegate.upperSeries.count > 0) { + consumerDelegate.upperSeries.removePoints(0, 1); + } + + print("removing:", index, count, "from", consumerDelegate.lowerSeries.count, consumerDelegate.upperSeries.count) + if (consumerDelegate.lowerSeries.count >= index + count) { + consumerDelegate.lowerSeries.removePoints(index, count) + } + if (consumerDelegate.upperSeries.count >= index + count) { + consumerDelegate.upperSeries.removePoints(index, count) + } // Add the leading 0-value entry back consumerDelegate.lowerSeries.insert(0, consumerDelegate.series.upperSeries.at(0).x, 0) diff --git a/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml b/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml index a816f73d..be02e35d 100644 --- a/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml +++ b/nymea-app/ui/mainviews/energy/PowerBalanceHistory.qml @@ -123,13 +123,17 @@ Item { } onEntriesRemoved: { - acquisitionUpperSeries.removePoints(index, count) - returnUpperSeries.removePoints(index, count) - fromStorageUpperSeries.removePoints(index, count) - toStorageUpperSeries.removePoints(index, count) - selfProductionConsumptionUpperSeries.removePoints(index, count) - productionSeries.removePoints(index, count) -// consumptionSeries.removePoints(index, count) + // Note QtCharts crash when calling removePoints() for points that don't exist. + // Additionally it may decide to ignore values we add, e.g. if we try to add an Inf or undefined value for whatever reason + // So, even though in theory the series should always 1:1 reflect the model, it may not do so in practice and we'll have to make sure not crash here + + acquisitionUpperSeries.removePoints(index, Math.min(count, acquisitionUpperSeries.count - index)) + returnUpperSeries.removePoints(index, Math.min(count, returnUpperSeries.count - index)) + fromStorageUpperSeries.removePoints(index, Math.min(count, fromStorageUpperSeries.count - index)) + toStorageUpperSeries.removePoints(index, Math.min(count, toStorageUpperSeries.count -index)) + selfProductionConsumptionUpperSeries.removePoints(index, Math.min(count, selfProductionConsumptionUpperSeries.count - index)) + productionSeries.removePoints(index, Math.min(count, productionSeries.count - index)) +// consumptionSeries.removePoints(index, Math.min(count, consumptionSeries.count - index)) zeroSeries.shrink() } } diff --git a/nymea-app/ui/utils/NymeaUtils.qml b/nymea-app/ui/utils/NymeaUtils.qml index 1e185a75..77d8383c 100644 --- a/nymea-app/ui/utils/NymeaUtils.qml +++ b/nymea-app/ui/utils/NymeaUtils.qml @@ -54,7 +54,7 @@ Item { } else if (interfaceList.indexOf("awning") >= 0) { page = "AwningThingPage.qml"; } else if (interfaceList.indexOf("notifications") >= 0) { - page = "NotificationsDevicePage.qml"; + page = "NotificationsThingPage.qml"; } else if (interfaceList.indexOf("fingerprintreader") >= 0) { page = "FingerprintReaderDevicePage.qml"; } else if (interfaceList.indexOf("evcharger") >= 0) {