Merge PR #1010: More work on the new logs

This commit is contained in:
jenkins 2023-04-24 11:39:34 +02:00
commit f687f97e5a
22 changed files with 591 additions and 375 deletions

View File

@ -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 &timestamp) 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<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() && !m_endTime.isNull()) {
params.insert("startTime", m_startTime.toMSecsSinceEpoch());
params.insert("endTime", m_endTime.toMSecsSinceEpoch());
QMetaEnum sampleRateEnum = QMetaEnum::fromType<SampleRate>();
params.insert("sampleRate", sampleRateEnum.valueToKey(m_sampleRate));
} else {
qCWarning(dcLogEngine()) << "startTime and endTime is required when asking for resampling";
return;
}
}
// if (!m_startTime.isNull()) {
// params.insert("startTime", m_startTime.toMSecsSinceEpoch());
// }
// if (!m_endTime.isNull()) {
// params.insert("endTime", m_endTime.toMSecsSinceEpoch());
// }
QMetaEnum sortOrderEnum = QMetaEnum::fromType<Qt::SortOrder>();
params.insert("sortOrder", sortOrderEnum.valueToKey(m_sortOrder));
qCDebug(dcLogEngine()) << "Fetching logs:" << QJsonDocument::fromVariant(params).toJson();
m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "logsReply");
}
void NewLogsModel::logsReply(int commandId, const QVariantMap &data)
{
Q_UNUSED(commandId)
QList<NewLogEntry*> 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<NewLogEntry*> 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();

View File

@ -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<NewLogEntry*> &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<NewLogEntry*> m_list;
};

View File

@ -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 &params
qDebug() << "DisconnectIO response" << commandId << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
}
void ThingManager::setStateLoggingResponse(int commandId, const QVariantMap &params)
{
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());

View File

@ -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 &params = 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 &params);
Q_INVOKABLE void connectIOResponse(int commandId, const QVariantMap &params);
Q_INVOKABLE void disconnectIOResponse(int commandId, const QVariantMap &params);
Q_INVOKABLE void setStateLoggingResponse(int commandId, const QVariantMap &params);
public slots:
ThingGroup* createGroup(Interface *interface, ThingsProxy *things);

View File

@ -50,7 +50,6 @@
<file>ui/devicepages/ShutterDevicePage.qml</file>
<file>ui/devicepages/GarageThingPage.qml</file>
<file>ui/devicepages/AwningThingPage.qml</file>
<file>ui/devicepages/NotificationsDevicePage.qml</file>
<file>ui/devicepages/LightThingPage.qml</file>
<file>ui/devicepages/FingerprintReaderDevicePage.qml</file>
<file>ui/devicepages/DeviceLogPage.qml</file>
@ -312,5 +311,6 @@
<file>ui/mainviews/airconditioning/LegendDelegate.qml</file>
<file>ui/customviews/StateChart.qml</file>
<file>ui/devicepages/ThingLogPage.qml</file>
<file>ui/devicepages/NotificationsThingPage.qml</file>
</qresource>
</RCC>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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