From 0f9caa03dfd667395f814f256918f1b39768e679 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 27 Aug 2020 01:16:03 +0200 Subject: [PATCH] Add media and energy views --- libnymea-app/devicemanager.cpp | 2 +- libnymea-app/devices.cpp | 18 +- libnymea-app/jsonrpc/jsonrpcclient.cpp | 2 +- libnymea-app/libnymea-app-core.h | 6 +- libnymea-app/libnymea-app.pro | 6 +- libnymea-app/models/barseriesadapter.cpp | 203 +++++++++++ libnymea-app/models/barseriesadapter.h | 66 ++++ libnymea-app/models/devicemodel.cpp | 30 +- libnymea-app/models/interfacesproxy.cpp | 10 +- libnymea-app/models/logsmodel.cpp | 314 ++++++++++-------- libnymea-app/models/logsmodel.h | 49 +-- libnymea-app/models/logsmodelng.cpp | 6 +- libnymea-app/models/valuelogsproxymodel.cpp | 182 ---------- libnymea-app/models/valuelogsproxymodel.h | 83 ----- libnymea-app/models/xyseriesadapter.cpp | 178 ++++++++++ libnymea-app/models/xyseriesadapter.h | 87 +++++ libnymea-app/ruletemplates/ruletemplates.cpp | 2 +- libnymea-app/scripting/codecompletion.cpp | 28 +- libnymea-app/system/systemcontroller.cpp | 10 +- libnymea-app/thinggroup.cpp | 10 +- libnymea-app/types/device.cpp | 53 +-- libnymea-app/types/device.h | 13 +- nymea-app/images.qrc | 2 +- nymea-app/resources.qrc | 9 +- nymea-app/ui/MainPage.qml | 1 + nymea-app/ui/Nymea.qml | 2 +- nymea-app/ui/components/MediaArtworkImage.qml | 10 +- nymea-app/ui/components/MediaBrowser.qml | 101 ++++++ nymea-app/ui/components/MediaControls.qml | 15 +- .../components/ShuffleRepeatVolumeControl.qml | 145 ++++++++ nymea-app/ui/components/SmartMeterChart.qml | 6 - nymea-app/ui/components/ThrottledSlider.qml | 3 + nymea-app/ui/customviews/GenericTypeGraph.qml | 4 +- .../ui/customviews/GenericTypeGraphPre110.qml | 114 ------- nymea-app/ui/customviews/SensorView.qml | 145 -------- nymea-app/ui/delegates/InterfaceTile.qml | 2 +- .../devicelistpages/MediaDeviceListPage.qml | 2 +- nymea-app/ui/devicepages/DeviceLogPage.qml | 7 +- nymea-app/ui/devicepages/MediaDevicePage.qml | 163 +-------- nymea-app/ui/devicepages/SensorDevicePage.qml | 162 ++++++++- .../devicepages/SensorDevicePagePost110.qml | 192 ----------- .../ui/devicepages/SensorDevicePagePre110.qml | 61 ---- .../ui/devicepages/WeatherDevicePage.qml | 55 ++- .../devicepages/WeatherDevicePagePost110.qml | 92 ----- .../devicepages/WeatherDevicePagePre110.qml | 74 ----- .../browser/MediaBrowserIconLibrary.svg | 2 +- ...mediaplayer-app-symbolic.svg => media.svg} | 0 nymea-app/ui/mainviews/EnergyView.qml | 280 ++++++++++++++++ nymea-app/ui/mainviews/GroupsView.qml | 2 +- nymea-app/ui/mainviews/MediaView.qml | 186 +++++++++++ nymea-app/ui/system/LogViewerPage.qml | 29 +- 51 files changed, 1800 insertions(+), 1424 deletions(-) create mode 100644 libnymea-app/models/barseriesadapter.cpp create mode 100644 libnymea-app/models/barseriesadapter.h delete mode 100644 libnymea-app/models/valuelogsproxymodel.cpp delete mode 100644 libnymea-app/models/valuelogsproxymodel.h create mode 100644 libnymea-app/models/xyseriesadapter.cpp create mode 100644 libnymea-app/models/xyseriesadapter.h create mode 100644 nymea-app/ui/components/MediaBrowser.qml create mode 100644 nymea-app/ui/components/ShuffleRepeatVolumeControl.qml delete mode 100644 nymea-app/ui/customviews/GenericTypeGraphPre110.qml delete mode 100644 nymea-app/ui/customviews/SensorView.qml delete mode 100644 nymea-app/ui/devicepages/SensorDevicePagePost110.qml delete mode 100644 nymea-app/ui/devicepages/SensorDevicePagePre110.qml delete mode 100644 nymea-app/ui/devicepages/WeatherDevicePagePost110.qml delete mode 100644 nymea-app/ui/devicepages/WeatherDevicePagePre110.qml rename nymea-app/ui/images/{mediaplayer-app-symbolic.svg => media.svg} (100%) create mode 100644 nymea-app/ui/mainviews/MediaView.qml diff --git a/libnymea-app/devicemanager.cpp b/libnymea-app/devicemanager.cpp index b14e1852..c95b568b 100644 --- a/libnymea-app/devicemanager.cpp +++ b/libnymea-app/devicemanager.cpp @@ -343,7 +343,7 @@ void DeviceManager::getConfiguredDevicesResponse(const QVariantMap ¶ms) QVariantList stateVariantList = deviceVariant.toMap().value("states").toList(); foreach (const QVariant &stateMap, stateVariantList) { QString stateTypeId = stateMap.toMap().value("stateTypeId").toString(); - StateType *st = device->deviceClass()->stateTypes()->getStateType(stateTypeId); + StateType *st = device->thingClass()->stateTypes()->getStateType(stateTypeId); if (!st) { qWarning() << "Can't find a statetype for this state"; continue; diff --git a/libnymea-app/devices.cpp b/libnymea-app/devices.cpp index 43ea45b8..0d8225cf 100644 --- a/libnymea-app/devices.cpp +++ b/libnymea-app/devices.cpp @@ -72,24 +72,24 @@ QVariant Devices::data(const QModelIndex &index, int role) const if (index.row() < 0 || index.row() >= m_devices.count()) return QVariant(); - Device *device = m_devices.at(index.row()); + Device *thing = m_devices.at(index.row()); switch (role) { case RoleName: - return device->name(); + return thing->name(); case RoleId: - return device->id().toString(); + return thing->id().toString(); case RoleDeviceClass: - return device->deviceClassId().toString(); + return thing->deviceClassId().toString(); case RoleParentDeviceId: - return device->parentDeviceId().toString(); + return thing->parentDeviceId().toString(); case RoleSetupStatus: - return device->setupStatus(); + return thing->setupStatus(); case RoleSetupDisplayMessage: - return device->setupDisplayMessage(); + return thing->setupDisplayMessage(); case RoleInterfaces: - return device->deviceClass()->interfaces(); + return thing->thingClass()->interfaces(); case RoleBaseInterface: - return device->deviceClass()->baseInterface(); + return thing->thingClass()->baseInterface(); } return QVariant(); } diff --git a/libnymea-app/jsonrpc/jsonrpcclient.cpp b/libnymea-app/jsonrpc/jsonrpcclient.cpp index eb482c77..394f57ae 100644 --- a/libnymea-app/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app/jsonrpc/jsonrpcclient.cpp @@ -592,7 +592,7 @@ void JsonRpcClient::helloReply(const QVariantMap ¶ms) qDebug() << "Handshake reply:" << "Protocol version:" << protoVersionString << "InitRequired:" << m_initialSetupRequired << "AuthRequired:" << m_authenticationRequired << "PushButtonAvailable:" << m_pushButtonAuthAvailable;; - QVersionNumber minimumRequiredVersion = QVersionNumber(1, 0); + QVersionNumber minimumRequiredVersion = QVersionNumber(1, 10); if (m_jsonRpcVersion < minimumRequiredVersion) { qWarning() << "Nymea core doesn't support minimum required version. Required:" << minimumRequiredVersion << "Found:" << m_jsonRpcVersion; m_connection->disconnect(); diff --git a/libnymea-app/libnymea-app-core.h b/libnymea-app/libnymea-app-core.h index d0448342..66518bca 100644 --- a/libnymea-app/libnymea-app-core.h +++ b/libnymea-app/libnymea-app-core.h @@ -65,7 +65,8 @@ #include "types/browseritem.h" #include "models/logsmodel.h" #include "models/logsmodelng.h" -#include "models/valuelogsproxymodel.h" +#include "models/barseriesadapter.h" +#include "models/xyseriesadapter.h" #include "models/interfacesproxy.h" #include "configuration/nymeaconfiguration.h" #include "configuration/serverconfiguration.h" @@ -246,8 +247,9 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "LogsModel"); qmlRegisterType(uri, 1, 0, "LogsModelNg"); - qmlRegisterType(uri, 1, 0, "ValueLogsProxyModel"); qmlRegisterUncreatableType(uri, 1, 0, "LogEntry", "Get them from LogsModel"); + qmlRegisterType(uri, 1, 0, "BarSeriesAdapter"); + qmlRegisterType(uri, 1, 0, "XYSeriesAdapter"); qmlRegisterUncreatableType(uri, 1, 0, "TagsManager", "Get it from Engine"); qmlRegisterUncreatableType(uri, 1, 0, "Tags", "Get it from TagsManager"); diff --git a/libnymea-app/libnymea-app.pro b/libnymea-app/libnymea-app.pro index 4a7233f4..3f7bfff4 100644 --- a/libnymea-app/libnymea-app.pro +++ b/libnymea-app/libnymea-app.pro @@ -27,7 +27,9 @@ INCLUDEPATH += $$top_srcdir/QtZeroConf SOURCES += \ configuration/networkmanager.cpp \ engine.cpp \ + models/barseriesadapter.cpp \ models/sortfilterproxymodel.cpp \ + models/xyseriesadapter.cpp \ ruletemplates/calendaritemtemplate.cpp \ ruletemplates/timedescriptortemplate.cpp \ ruletemplates/timeeventitemtemplate.cpp \ @@ -128,7 +130,6 @@ SOURCES += \ rulemanager.cpp \ models/rulesfiltermodel.cpp \ models/logsmodel.cpp \ - models/valuelogsproxymodel.cpp \ logmanager.cpp \ wifisetup/bluetoothdevice.cpp \ wifisetup/bluetoothdeviceinfo.cpp \ @@ -163,7 +164,9 @@ SOURCES += \ HEADERS += \ configuration/networkmanager.h \ engine.h \ + models/barseriesadapter.h \ models/sortfilterproxymodel.h \ + models/xyseriesadapter.h \ ruletemplates/calendaritemtemplate.h \ ruletemplates/timedescriptortemplate.h \ ruletemplates/timeeventitemtemplate.h \ @@ -265,7 +268,6 @@ HEADERS += \ rulemanager.h \ models/rulesfiltermodel.h \ models/logsmodel.h \ - models/valuelogsproxymodel.h \ logmanager.h \ wifisetup/bluetoothdevice.h \ wifisetup/bluetoothdeviceinfo.h \ diff --git a/libnymea-app/models/barseriesadapter.cpp b/libnymea-app/models/barseriesadapter.cpp new file mode 100644 index 00000000..5c1e9fd2 --- /dev/null +++ b/libnymea-app/models/barseriesadapter.cpp @@ -0,0 +1,203 @@ +#include "barseriesadapter.h" + +BarSeriesAdapter::BarSeriesAdapter(QObject *parent) : QObject(parent) +{ + +} + +LogsModel *BarSeriesAdapter::logsModel() const +{ + return m_logsModel; +} + +void BarSeriesAdapter::setLogsModel(LogsModel *logsModel) +{ + if (m_logsModel != logsModel) { + m_logsModel = logsModel; + emit logsModelChanged(); + update(); + connect(logsModel, &LogsModel::logEntryAdded, this, &BarSeriesAdapter::logEntryAdded); + } +} + +QtCharts::QAbstractBarSeries *BarSeriesAdapter::barSeries() const +{ + return m_barSeries; +} + +void BarSeriesAdapter::setBarSeries(QtCharts::QAbstractBarSeries *barSeries) +{ + if (m_barSeries != barSeries) { + m_barSeries = barSeries; + emit barSeriesChanged(); + update(); + } +} + +BarSeriesAdapter::Interval BarSeriesAdapter::interval() const +{ + return m_interval; +} + +void BarSeriesAdapter::setInterval(BarSeriesAdapter::Interval interval) +{ + if (m_interval != interval) { + m_interval = interval; + emit intervalChanged(); + } +} + +void BarSeriesAdapter::update() +{ + if (!m_barSeries || !m_logsModel) { + return; + } + m_set = new QtCharts::QBarSet(m_barSeries->name()); + m_barSeries->append(m_set); + + for (int i = 0; i < m_logsModel->rowCount(); i++) { + LogEntry *entry = m_logsModel->get(i); + qDebug() << "have entry" << entry->timestamp().toString(); + } +} + +void BarSeriesAdapter::ensureSlots(const QDateTime &start, const QDateTime &end) +{ + if (!m_barSeries || !m_logsModel) { + return; + } + + QDateTime startTime = start; + switch (m_interval) { + case IntervalMinutes: + startTime.setTime(QTime(startTime.time().hour(), startTime.time().minute())); + break; + case IntervalHours: + startTime.setTime(QTime(startTime.time().hour(), 0)); + break; + case IntervalDays: + startTime.setTime(QTime(0, 0)); + break; + } + + + QDateTime endTime = end; + if (!endTime.isValid()) { + endTime = QDateTime::currentDateTime(); + } + endTime.setTime(QTime(endTime.time().hour(), endTime.time().minute())); + + QDateTime oldestExistingSlot; + if (m_timeslots.isEmpty()) { + oldestExistingSlot = endTime; + } else { + oldestExistingSlot = m_timeslots.first().datetime; + } + + if (startTime < oldestExistingSlot) { + long duration = oldestExistingSlot.toMSecsSinceEpoch() - startTime.toMSecsSinceEpoch(); + long slotCount = duration / (m_interval * 1000); + qDebug() << "Need" << slotCount << "new slots appended"; + + for (int i = 0; i < slotCount; i++) { + QDateTime slotTime = oldestExistingSlot.addSecs(-m_interval * (i + 1)); +// qDebug() << "Adding" << slotTime.toString(); + TimeSlot timeslot; + timeslot.datetime = slotTime; + m_set->insert(0, 0); + m_timeslots.prepend(timeslot); + } + } + + QDateTime newestExistingSlot; + if (m_timeslots.isEmpty()) { + newestExistingSlot = startTime; + } else { + newestExistingSlot = m_timeslots.last().datetime; + } + + if (endTime > newestExistingSlot) { + long duration = endTime.toMSecsSinceEpoch() - newestExistingSlot.toMSecsSinceEpoch(); + long slotCount = duration / (m_interval * 1000); +// qDebug() << "Need" << slotCount << "new slots prepended"; + + for (int i = 0; i < slotCount; i++) { + QDateTime slotTime = newestExistingSlot.addSecs(m_interval * (i + 1)); + TimeSlot timeslot; + timeslot.datetime = slotTime; + m_set->append(0); + m_timeslots.append(timeslot); + } + } + + if (m_timeslots.isEmpty()) { +// qDebug() << "Need to initialize list with 1 entry"; + TimeSlot timeslot; + timeslot.datetime = startTime; + m_set->append(0); + m_timeslots.append(timeslot); + } + + qDebug() << "Ensuring slots from" << start << "to" << end << "oldest" << oldestExistingSlot << "newest" << newestExistingSlot; + +} + +void BarSeriesAdapter::logEntryAdded(LogEntry *entry) +{ + qDebug() << "****" << m_barSeries << m_logsModel; + if (!m_barSeries || !m_logsModel) { + return; + } + ensureSlots(QDateTime::fromMSecsSinceEpoch(qMin(m_logsModel->startTime().toMSecsSinceEpoch(), entry->timestamp().toMSecsSinceEpoch())), QDateTime::fromMSecsSinceEpoch(qMax(m_logsModel->endTime().toMSecsSinceEpoch(), entry->timestamp().toMSecsSinceEpoch()))); + + QDateTime timestamp = entry->timestamp(); + + QDateTime timeSlotStart = timestamp; + switch (m_interval) { + case IntervalMinutes: + timeSlotStart.setTime(QTime(timestamp.time().hour(), timestamp.time().minute())); + break; + case IntervalHours: + timeSlotStart.setTime(QTime(timestamp.time().hour(), 0)); + break; + case IntervalDays: + timeSlotStart.setTime(QTime(0, 0)); + break; + } + +// qDebug() << "Item time:" << timeSlotStart; + + TimeSlot first = m_timeslots.first(); + TimeSlot last = m_timeslots.last(); + long slotIdx = (timeSlotStart.toMSecsSinceEpoch() - first.datetime.toMSecsSinceEpoch()) / (m_interval * 1000); + qDebug() << "first" << first.datetime.toString(); + qDebug() << "last" << last.datetime.toString(); + qDebug() << "this" << timeSlotStart.toString(); + qDebug() << "idx" << slotIdx; + + + m_timeslots[slotIdx].entries.append(entry); + m_set->replace(slotIdx, m_timeslots[slotIdx].value()); + qDebug() << "Adding entry" << entry->timestamp() << "timestlot" << timeSlotStart << "at" << slotIdx << "value" << m_timeslots[slotIdx].value(); + +// if (!m_timeslots.contains(timeSlotStart)) { +// TimeSlot timeslot; +// timeslot.startTime = timeSlotStart; + + +// } +// TimeSlot timeslot = m_timeslots.value(timeSlotStart); +// timeslot.entries.append(entry); +} + +qreal BarSeriesAdapter::TimeSlot::value() const +{ + qreal value = 0; + foreach (LogEntry *entry, entries) { + value += entry->value().toDouble(); + } + if (entries.count() > 1) { + value /= entries.count(); + } + return value; +} diff --git a/libnymea-app/models/barseriesadapter.h b/libnymea-app/models/barseriesadapter.h new file mode 100644 index 00000000..c5545447 --- /dev/null +++ b/libnymea-app/models/barseriesadapter.h @@ -0,0 +1,66 @@ +#ifndef BARSERIESADAPTER_H +#define BARSERIESADAPTER_H + +#include "logsmodel.h" + +#include +#include +#include + +class BarSeriesAdapter : public QObject +{ + Q_OBJECT + Q_PROPERTY(LogsModel* logsModel READ logsModel WRITE setLogsModel NOTIFY logsModelChanged) + Q_PROPERTY(QtCharts::QAbstractBarSeries* barSeries READ barSeries WRITE setBarSeries NOTIFY barSeriesChanged) + + Q_PROPERTY(Interval interval READ interval WRITE setInterval NOTIFY intervalChanged) + +public: + enum Interval { + IntervalMinutes = 60, + IntervalHours = 60 * 60, + IntervalDays = 24 * 60 * 60 + }; + Q_ENUM(Interval) + + explicit BarSeriesAdapter(QObject *parent = nullptr); + + LogsModel *logsModel() const; + void setLogsModel(LogsModel *logsModel); + + QtCharts::QAbstractBarSeries *barSeries() const; + void setBarSeries(QtCharts::QAbstractBarSeries *barSeries); + + Interval interval() const; + void setInterval(Interval interval); + +signals: + void logsModelChanged(); + void barSeriesChanged(); + void intervalChanged(); + +private: + void update(); + + void ensureSlots(const QDateTime &start, const QDateTime &end); + +private slots: + void logEntryAdded(LogEntry *entry); + +private: + class TimeSlot { + public: + QDateTime datetime; + QList entries; + qreal value() const; + }; + + LogsModel *m_logsModel = nullptr; + QtCharts::QAbstractBarSeries *m_barSeries = nullptr; + QtCharts::QBarSet *m_set = nullptr; + Interval m_interval = IntervalMinutes; + + QList m_timeslots; +}; + +#endif // BARSERIESADAPTER_H diff --git a/libnymea-app/models/devicemodel.cpp b/libnymea-app/models/devicemodel.cpp index a863702f..ee105f8f 100644 --- a/libnymea-app/models/devicemodel.cpp +++ b/libnymea-app/models/devicemodel.cpp @@ -49,35 +49,35 @@ QVariant DeviceModel::data(const QModelIndex &index, int role) const return m_list.at(index.row()); } if (role == RoleType) { - StateType* stateType = m_device->deviceClass()->stateTypes()->getStateType(m_list.at(index.row())); + StateType* stateType = m_device->thingClass()->stateTypes()->getStateType(m_list.at(index.row())); if (stateType) { return TypeStateType; } - ActionType* actionType = m_device->deviceClass()->actionTypes()->getActionType(m_list.at(index.row())); + ActionType* actionType = m_device->thingClass()->actionTypes()->getActionType(m_list.at(index.row())); if (actionType) { return TypeActionType; } - EventType* eventType = m_device->deviceClass()->eventTypes()->getEventType(m_list.at(index.row())); + EventType* eventType = m_device->thingClass()->eventTypes()->getEventType(m_list.at(index.row())); if (eventType) { return TypeEventType; } } if (role == RoleDisplayName) { - StateType* stateType = m_device->deviceClass()->stateTypes()->getStateType(m_list.at(index.row())); + StateType* stateType = m_device->thingClass()->stateTypes()->getStateType(m_list.at(index.row())); if (stateType) { return stateType->displayName(); } - ActionType* actionType = m_device->deviceClass()->actionTypes()->getActionType(m_list.at(index.row())); + ActionType* actionType = m_device->thingClass()->actionTypes()->getActionType(m_list.at(index.row())); if (actionType) { return actionType->displayName(); } - EventType* eventType = m_device->deviceClass()->eventTypes()->getEventType(m_list.at(index.row())); + EventType* eventType = m_device->thingClass()->eventTypes()->getEventType(m_list.at(index.row())); if (eventType) { return eventType->displayName(); } } if (role == RoleWritable) { - ActionType* actionType = m_device->deviceClass()->actionTypes()->getActionType(m_list.at(index.row())); + ActionType* actionType = m_device->thingClass()->actionTypes()->getActionType(m_list.at(index.row())); return actionType != nullptr; } return QVariant(); @@ -169,22 +169,22 @@ void DeviceModel::updateList() beginResetModel(); m_list.clear(); if (m_showStates) { - for (int i = 0; i < m_device->deviceClass()->stateTypes()->rowCount(); i++) { - m_list.append(m_device->deviceClass()->stateTypes()->get(i)->id()); + for (int i = 0; i < m_device->thingClass()->stateTypes()->rowCount(); i++) { + m_list.append(m_device->thingClass()->stateTypes()->get(i)->id()); } } if (m_showActions) { - for (int i = 0; i < m_device->deviceClass()->actionTypes()->rowCount(); i++) { - if (!m_list.contains(m_device->deviceClass()->actionTypes()->get(i)->id())) { - m_list.append(m_device->deviceClass()->actionTypes()->get(i)->id()); + for (int i = 0; i < m_device->thingClass()->actionTypes()->rowCount(); i++) { + if (!m_list.contains(m_device->thingClass()->actionTypes()->get(i)->id())) { + m_list.append(m_device->thingClass()->actionTypes()->get(i)->id()); } } } if (m_showEvents) { - for (int i = 0; i < m_device->deviceClass()->eventTypes()->rowCount(); i++) { - if (!m_list.contains(m_device->deviceClass()->eventTypes()->get(i)->id())) { - m_list.append(m_device->deviceClass()->eventTypes()->get(i)->id()); + for (int i = 0; i < m_device->thingClass()->eventTypes()->rowCount(); i++) { + if (!m_list.contains(m_device->thingClass()->eventTypes()->get(i)->id())) { + m_list.append(m_device->thingClass()->eventTypes()->get(i)->id()); } } } diff --git a/libnymea-app/models/interfacesproxy.cpp b/libnymea-app/models/interfacesproxy.cpp index e1980fc8..b16eea8c 100644 --- a/libnymea-app/models/interfacesproxy.cpp +++ b/libnymea-app/models/interfacesproxy.cpp @@ -103,11 +103,11 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source bool found = false; for (int i = 0; i < m_devicesFilter->rowCount(); i++) { Device *d = m_devicesFilter->get(i); - if (!d->deviceClass()) { + if (!d->thingClass()) { qWarning() << "Cannot find DeviceClass for device:" << d->id() << d->name(); return false; } - if (d->deviceClass()->interfaces().contains(interfaceName)) { + if (d->thingClass()->interfaces().contains(interfaceName)) { found = true; break; } @@ -121,11 +121,11 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source bool found = false; for (int i = 0; i < m_devicesProxyFilter->rowCount(); i++) { Device *d = m_devicesProxyFilter->get(i); - if (!d->deviceClass()) { - qWarning() << "Cannot find DeviceClass for device:" << d->id() << d->name(); + if (!d->thingClass()) { + qWarning() << "Cannot find ThingClass for thing:" << d->id() << d->name(); return false; } - if (d->deviceClass()->interfaces().contains(interfaceName)) { + if (d->thingClass()->interfaces().contains(interfaceName)) { found = true; break; } diff --git a/libnymea-app/models/logsmodel.cpp b/libnymea-app/models/logsmodel.cpp index 53ea9918..f6637489 100644 --- a/libnymea-app/models/logsmodel.cpp +++ b/libnymea-app/models/logsmodel.cpp @@ -29,14 +29,18 @@ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "logsmodel.h" +#include +#include +#include +#include #include "engine.h" +#include "types/logentry.h" #include "logmanager.h" -#include - LogsModel::LogsModel(QObject *parent) : QAbstractListModel(parent) { + } Engine *LogsModel::engine() const @@ -53,11 +57,6 @@ void LogsModel::setEngine(Engine *engine) } } -bool LogsModel::busy() const -{ - return m_busy; -} - int LogsModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) @@ -72,6 +71,7 @@ QVariant LogsModel::data(const QModelIndex &index, int role) const case RoleValue: return m_list.at(index.row())->value(); case RoleThingId: + case RoleDeviceId: return m_list.at(index.row())->thingId(); case RoleTypeId: return m_list.at(index.row())->typeId(); @@ -88,13 +88,19 @@ QHash LogsModel::roleNames() const QHash roles; roles.insert(RoleTimestamp, "timestamp"); roles.insert(RoleValue, "value"); - roles.insert(RoleThingId, "deviceId"); + roles.insert(RoleThingId, "thingId"); + roles.insert(RoleDeviceId, "deviceId"); roles.insert(RoleTypeId, "typeId"); roles.insert(RoleSource, "source"); roles.insert(RoleLoggingEventType, "loggingEventType"); return roles; } +bool LogsModel::busy() const +{ + return m_busy; +} + bool LogsModel::live() const { return m_live; @@ -108,29 +114,42 @@ void LogsModel::setLive(bool live) } } -QString LogsModel::deviceId() const +QUuid LogsModel::thingId() const { - return m_deviceId; + return m_thingId; } -void LogsModel::setDeviceId(const QString &deviceId) +void LogsModel::setThingId(const QUuid &thingId) { - if (m_deviceId != deviceId) { - m_deviceId = deviceId; - emit deviceIdChanged(); + if (m_thingId != thingId) { + m_thingId = thingId; + emit thingIdChanged(); } } QStringList LogsModel::typeIds() const { - return m_typeIds; + QStringList strings; + foreach (const QUuid &id, m_typeIds) { + strings.append(id.toString()); + } + return strings; } void LogsModel::setTypeIds(const QStringList &typeIds) { - if (m_typeIds != typeIds) { - m_typeIds = typeIds; + QList fixedTypeIds; + foreach (const QString &id, typeIds) { + fixedTypeIds.append(QUuid(id)); + } + if (m_typeIds != fixedTypeIds) { + m_typeIds = fixedTypeIds; emit typeIdsChanged(); + beginResetModel(); + qDeleteAll(m_list); + m_list.clear(); + endResetModel(); + fetchMore(); } } @@ -160,6 +179,24 @@ void LogsModel::setEndTime(const QDateTime &endTime) } } +QDateTime LogsModel::viewStartTime() const +{ + return m_viewStartTime; +} + +void LogsModel::setViewStartTime(const QDateTime &viewStartTime) +{ + if (m_viewStartTime != viewStartTime) { + m_viewStartTime = viewStartTime; + emit viewStartTimeChanged(); + if (m_list.count() == 0 || m_list.last()->timestamp() > m_viewStartTime) { + if (m_canFetchMore) { + fetchMore(); + } + } + } +} + LogEntry *LogsModel::get(int index) const { if (index >= 0 && index < m_list.count()) { @@ -168,163 +205,172 @@ LogEntry *LogsModel::get(int index) const return nullptr; } -void LogsModel::notificationReceived(const QVariantMap &data) -{ - qDebug() << "KLogModel notificatiion" << data; -} - -void LogsModel::update() -{ - if (!m_engine) { - qWarning() << "LogsModel: Can't update, no engine set"; - return; - } - if (m_busy) { - return; - } - m_busy = true; - emit busyChanged(); - - QVariantMap params; - if (!m_deviceId.isEmpty()) { - QVariantList deviceIds; - deviceIds.append(m_deviceId); - params.insert("deviceIds", deviceIds); - } - if (!m_typeIds.isEmpty()) { - QVariantList typeIds; - foreach (const QString &typeId, m_typeIds) { - typeIds.append(typeId); - } - params.insert("typeIds", typeIds); - } - QVariantList timeFilters; - QVariantMap timeFilter; - timeFilter.insert("startDate", m_startTime.toSecsSinceEpoch()); - timeFilter.insert("endDate", m_endTime.toSecsSinceEpoch()); - timeFilters.append(timeFilter); - params.insert("timeFilters", timeFilters); - m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "logsReply"); -} - -void LogsModel::fetchEarlier(int hours) -{ - if (!m_engine) { - return; - } - if (m_busy) { - return; - } - m_busy = true; - emit busyChanged(); - - QVariantMap params; - if (!m_deviceId.isEmpty()) { - QVariantList deviceIds; - deviceIds.append(m_deviceId); - params.insert("deviceIds", deviceIds); - } - if (!m_typeIds.isEmpty()) { - QVariantList typeIds; - foreach (const QString &typeId, m_typeIds) { - typeIds.append(typeId); - } - params.insert("typeIds", typeIds); - } - QVariantList timeFilters; - QVariantMap timeFilter; - timeFilter.insert("endDate", m_startTime.toSecsSinceEpoch()); - m_startTime = m_startTime.addSecs(-60*60*hours); - timeFilter.insert("startDate", m_startTime.toSecsSinceEpoch()); - timeFilters.append(timeFilter); - params.insert("timeFilters", timeFilters); - m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "fetchEarlierReply"); -} - void LogsModel::logsReply(const QVariantMap &data) { -// qDebug() << "logs reply" << data; - beginResetModel(); - qDeleteAll(m_list); - m_list.clear(); + int offset = data.value("params").toMap().value("offset").toInt(); + int count = data.value("params").toMap().value("count").toInt(); +// qDebug() << qUtf8Printable(QJsonDocument::fromVariant(data).toJson()); + + QList newBlock; QList logEntries = data.value("params").toMap().value("logEntries").toList(); foreach (const QVariant &logEntryVariant, logEntries) { QVariantMap entryMap = logEntryVariant.toMap(); QDateTime timeStamp = QDateTime::fromMSecsSinceEpoch(entryMap.value("timestamp").toLongLong()); - QString deviceId = entryMap.value("deviceId").toString(); + QString thingId; + if (m_engine->jsonRpcClient()->ensureServerVersion("5.0")) { + thingId = entryMap.value("thingId").toString(); + } else { + thingId = entryMap.value("deviceId").toString(); + } QString typeId = entryMap.value("typeId").toString(); QMetaEnum sourceEnum = QMetaEnum::fromType(); - LogEntry::LoggingSource loggingSource = (LogEntry::LoggingSource)sourceEnum.keyToValue(entryMap.value("source").toByteArray()); + LogEntry::LoggingSource loggingSource = static_cast(sourceEnum.keyToValue(entryMap.value("source").toByteArray())); QMetaEnum loggingEventTypeEnum = QMetaEnum::fromType(); - LogEntry::LoggingEventType loggingEventType = (LogEntry::LoggingEventType)loggingEventTypeEnum.keyToValue(entryMap.value("eventType").toByteArray()); + LogEntry::LoggingEventType loggingEventType = static_cast(loggingEventTypeEnum.keyToValue(entryMap.value("eventType").toByteArray())); QVariant value = loggingEventType == LogEntry::LoggingEventTypeActiveChange ? entryMap.value("active").toBool() : entryMap.value("value"); - LogEntry *entry = new LogEntry(timeStamp, value, deviceId, typeId, loggingSource, loggingEventType, this); - m_list.append(entry); + LogEntry *entry = new LogEntry(timeStamp, value, thingId, typeId, loggingSource, loggingEventType, this); + newBlock.append(entry); } - endResetModel(); - emit countChanged(); +// qDebug() << "Received logs from" << offset << "to" << offset + count << "Actual count:" << newBlock.count(); - m_busy = false; - emit busyChanged(); -} - -void LogsModel::fetchEarlierReply(const QVariantMap &data) -{ -// qDebug() << "logs reply" << data; - - QList logEntries = data.value("params").toMap().value("logEntries").toList(); - QList newEntries; - foreach (const QVariant &logEntryVariant, logEntries) { - QVariantMap entryMap = logEntryVariant.toMap(); - QDateTime timeStamp = QDateTime::fromMSecsSinceEpoch(entryMap.value("timestamp").toLongLong()); - QString deviceId = entryMap.value("deviceId").toString(); - QString typeId = entryMap.value("typeId").toString(); - QMetaEnum sourceEnum = QMetaEnum::fromType(); - LogEntry::LoggingSource loggingSource = (LogEntry::LoggingSource)sourceEnum.keyToValue(entryMap.value("source").toByteArray()); - QMetaEnum loggingEventTypeEnum = QMetaEnum::fromType(); - LogEntry::LoggingEventType loggingEventType = (LogEntry::LoggingEventType)loggingEventTypeEnum.keyToValue(entryMap.value("eventType").toByteArray()); - QVariant value = loggingEventType == LogEntry::LoggingEventTypeActiveChange ? entryMap.value("active").toBool() : entryMap.value("value"); - LogEntry *entry = new LogEntry(timeStamp, value, deviceId, typeId, loggingSource, loggingEventType, this); - newEntries.append(entry); + if (count < m_blockSize) { + m_canFetchMore = false; + } + + if (newBlock.isEmpty()) { + m_busy = false; + emit busyChanged(); + return; + } + + beginInsertRows(QModelIndex(), offset, offset + newBlock.count() - 1); + for (int i = 0; i < newBlock.count(); i++) { + LogEntry *entry = newBlock.at(i); + m_list.insert(offset + i, entry); + emit logEntryAdded(entry); } - beginInsertRows(QModelIndex(), 0, newEntries.count() - 1); - newEntries.append(m_list); - m_list = newEntries; endInsertRows(); emit countChanged(); m_busy = false; emit busyChanged(); + + if (m_viewStartTime.isValid() && m_list.count() > 0 && m_list.last()->timestamp() > m_viewStartTime && m_canFetchMore) { + fetchMore(); + } +} + +void LogsModel::fetchMore(const QModelIndex &parent) +{ + Q_UNUSED(parent) + + if (!m_engine) { + qWarning() << "Cannot update. Engine not set"; + return; + } + if (m_busy) { + return; + } + + if ((!m_startTime.isNull() && m_endTime.isNull()) || (m_startTime.isNull() && !m_endTime.isNull())) { + qDebug() << "Need neither or both, startTime and endTime set"; + return; + } + + m_busy = true; + emit busyChanged(); + + + QVariantMap params; + if (!m_thingId.isNull()) { + QVariantList thingIds; + thingIds.append(m_thingId); + if (m_engine->jsonRpcClient()->ensureServerVersion("5.0")) { + params.insert("thingIds", thingIds); + } else { + params.insert("deviceIds", thingIds); + } + } + if (!m_typeIds.isEmpty()) { + QVariantList typeIds; + foreach (const QUuid &typeId, m_typeIds) { + typeIds.append(typeId); + } + params.insert("typeIds", typeIds); + } + if (!m_startTime.isNull() && !m_endTime.isNull()) { + QVariantList timeFilters; + QVariantMap timeFilter; + timeFilter.insert("startDate", m_startTime.toSecsSinceEpoch()); + timeFilter.insert("endDate", m_endTime.toSecsSinceEpoch()); + timeFilters.append(timeFilter); + params.insert("timeFilters", timeFilters); + } + + params.insert("limit", m_blockSize); + params.insert("offset", m_list.count()); + + qDebug() << "Fetching logs from" << m_startTime.toString() << "to" << m_endTime.toString() << "with offset" << m_list.count() << "and limit" << m_blockSize; + + m_engine->jsonRpcClient()->sendCommand("Logging.GetLogEntries", params, this, "logsReply"); + // qDebug() << "GetLogEntries called"; +} + +void LogsModel::classBegin() +{ + +} + +void LogsModel::componentComplete() +{ + m_busy = false; + fetchMore(); +} + +bool LogsModel::canFetchMore(const QModelIndex &parent) const +{ + Q_UNUSED(parent) +// qDebug() << "canFetchMore" << (m_engine && m_canFetchMore); + return m_engine && m_canFetchMore; } void LogsModel::newLogEntryReceived(const QVariantMap &data) { +// qDebug() << "***** model NG" << data << m_live; if (!m_live) { return; } QVariantMap entryMap = data; - QString deviceId = entryMap.value("deviceId").toString(); - if (!m_deviceId.isNull() && deviceId != m_deviceId) { + QUuid thingId; + if (m_engine->jsonRpcClient()->ensureServerVersion("5.0")) { + thingId = entryMap.value("deviceId").toUuid(); + } else { + thingId = entryMap.value("thingId").toUuid(); + } + if (!m_thingId.isNull() && thingId != m_thingId) { return; } - QString typeId = entryMap.value("typeId").toString(); + QUuid typeId = entryMap.value("typeId").toUuid(); if (!m_typeIds.isEmpty() && !m_typeIds.contains(typeId)) { return; } - beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); + beginInsertRows(QModelIndex(), 0, 0); QDateTime timeStamp = QDateTime::fromMSecsSinceEpoch(entryMap.value("timestamp").toLongLong()); QMetaEnum sourceEnum = QMetaEnum::fromType(); - LogEntry::LoggingSource loggingSource = (LogEntry::LoggingSource)sourceEnum.keyToValue(entryMap.value("source").toByteArray()); + LogEntry::LoggingSource loggingSource = static_cast(sourceEnum.keyToValue(entryMap.value("source").toByteArray())); QMetaEnum loggingEventTypeEnum = QMetaEnum::fromType(); - LogEntry::LoggingEventType loggingEventType = (LogEntry::LoggingEventType)loggingEventTypeEnum.keyToValue(entryMap.value("eventType").toByteArray()); + LogEntry::LoggingEventType loggingEventType = static_cast(loggingEventTypeEnum.keyToValue(entryMap.value("eventType").toByteArray())); QVariant value = loggingEventType == LogEntry::LoggingEventTypeActiveChange ? entryMap.value("active").toBool() : entryMap.value("value"); - LogEntry *entry = new LogEntry(timeStamp, value, deviceId, typeId, loggingSource, loggingEventType, this); - m_list.append(entry); + LogEntry *entry = new LogEntry(timeStamp, value, thingId, typeId, loggingSource, loggingEventType, this); + m_list.prepend(entry); endInsertRows(); emit countChanged(); + + emit logEntryAdded(entry); + } diff --git a/libnymea-app/models/logsmodel.h b/libnymea-app/models/logsmodel.h index ac017c06..03180efd 100644 --- a/libnymea-app/models/logsmodel.h +++ b/libnymea-app/models/logsmodel.h @@ -32,26 +32,26 @@ #define LOGSMODEL_H #include +#include #include "jsonrpc/jsonhandler.h" #include "types/logentry.h" class Engine; -class LogsModel : public QAbstractListModel +class LogsModel : public QAbstractListModel, public QQmlParserStatus { Q_OBJECT Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged) - Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) - Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged) + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged) + Q_PROPERTY(QUuid thingId READ thingId WRITE setThingId NOTIFY thingIdChanged) Q_PROPERTY(QStringList typeIds READ typeIds WRITE setTypeIds NOTIFY typeIdsChanged) Q_PROPERTY(QDateTime startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged) Q_PROPERTY(QDateTime endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged) -// Q_PROPERTY(int paginationCount READ paginationCount WRITE setPaginationCount NOTIFY paginationCountChanged) - - Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged) + Q_PROPERTY(QDateTime viewStartTime READ viewStartTime WRITE setViewStartTime NOTIFY viewStartTimeChanged) public: enum Roles { @@ -72,12 +72,16 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent = QModelIndex()) override; + void classBegin() override; + void componentComplete() override; bool live() const; void setLive(bool live); - QString deviceId() const; - void setDeviceId(const QString &deviceId); + QUuid thingId() const; + void setThingId(const QUuid &deviceId); QStringList typeIds() const; void setTypeIds(const QStringList &typeIds); @@ -88,44 +92,43 @@ public: QDateTime endTime() const; void setEndTime(const QDateTime &endTime); -// int paginationCount() const; -// void setPaginationCount(int paginationCount); + QDateTime viewStartTime() const; + void setViewStartTime(const QDateTime &viewStartTime); Q_INVOKABLE LogEntry* get(int index) const; - Q_INVOKABLE void notificationReceived(const QVariantMap &data); signals: void engineChanged(); void busyChanged(); void liveChanged(); void countChanged(); - void deviceIdChanged(); + void thingIdChanged(); void typeIdsChanged(); void startTimeChanged(); void endTimeChanged(); -// void paginationCountChanged(); + void viewStartTimeChanged(); -public slots: - virtual void update(); - virtual void fetchEarlier(int hours); -// virtual void fetchLater(int hours); + void logEntryAdded(LogEntry *entry); private slots: virtual void logsReply(const QVariantMap &data); - virtual void fetchEarlierReply(const QVariantMap &data); void newLogEntryReceived(const QVariantMap &data); protected: Engine *m_engine = nullptr; QList m_list; - QString m_deviceId; - QStringList m_typeIds; - QDateTime m_startTime = QDateTime::currentDateTime().addDays(-1); - QDateTime m_endTime = QDateTime::currentDateTime(); + QUuid m_thingId; + QList m_typeIds; + QDateTime m_startTime; + QDateTime m_endTime; + QDateTime m_viewStartTime; - bool m_busy = false; + bool m_busy = true; bool m_live = false; + int m_blockSize = 100; + + bool m_canFetchMore = true; }; diff --git a/libnymea-app/models/logsmodelng.cpp b/libnymea-app/models/logsmodelng.cpp index 3ea664be..4845921c 100644 --- a/libnymea-app/models/logsmodelng.cpp +++ b/libnymea-app/models/logsmodelng.cpp @@ -278,7 +278,7 @@ void LogsModelNg::logsReply(const QVariantMap &data) continue; } - StateType *entryStateType = dev->deviceClass()->stateTypes()->getStateType(entry->typeId()); + StateType *entryStateType = dev->thingClass()->stateTypes()->getStateType(entry->typeId()); if (m_graphSeries) { if (entryStateType->type().toLower() == "bool") { @@ -453,9 +453,9 @@ void LogsModelNg::newLogEntryReceived(const QVariantMap &data) Device *dev = m_engine->thingManager()->devices()->getDevice(entry->thingId()); - StateType *entryStateType = dev->deviceClass()->stateTypes()->getStateType(entry->typeId()); + StateType *entryStateType = dev->thingClass()->stateTypes()->getStateType(entry->typeId()); - if (dev && dev->deviceClass()->stateTypes()->getStateType(entry->typeId())->type().toLower() == "bool") { + if (dev && dev->thingClass()->stateTypes()->getStateType(entry->typeId())->type().toLower() == "bool") { // First, remove the 2 rightmost (newest on the timeline) values. They're the ones in the future we added to extend the graph and making it end at 1 if (m_graphSeries->count() > 1) { m_graphSeries->removePoints(0, 2); diff --git a/libnymea-app/models/valuelogsproxymodel.cpp b/libnymea-app/models/valuelogsproxymodel.cpp deleted file mode 100644 index 852913e5..00000000 --- a/libnymea-app/models/valuelogsproxymodel.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* Copyright 2013 - 2020, nymea GmbH -* Contact: contact@nymea.io -* -* This file is part of nymea. -* This project including source code and documentation is protected by -* copyright law, and remains the property of nymea GmbH. All rights, including -* reproduction, publication, editing and translation, are reserved. The use of -* this project is subject to the terms of a license agreement to be concluded -* with nymea GmbH in accordance with the terms of use of nymea GmbH, available -* under https://nymea.io/license -* -* GNU General Public License Usage -* Alternatively, this project may be redistributed and/or modified under the -* terms of the GNU General Public License as published by the Free Software -* Foundation, GNU version 3. This project is distributed in the hope that it -* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -* Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this project. If not, see . -* -* For any further details and any questions please contact us under -* contact@nymea.io or see our FAQ/Licensing Information on -* https://nymea.io/license/faq -* -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#include "valuelogsproxymodel.h" - -#include - -ValueLogsProxyModel::ValueLogsProxyModel(QObject *parent) : LogsModel(parent) -{ - m_minimumValue = QVariant(0); - m_maximumValue = QVariant(0); -} - -void ValueLogsProxyModel::update() -{ - // modify starttime to add a day earlier so we have more chances to have meaningful data right from the start - m_startTime = m_startTime.addDays(-1); - LogsModel::update(); - m_startTime = m_startTime.addDays(1); -} - -ValueLogsProxyModel::Average ValueLogsProxyModel::average() const -{ - return m_average; -} - -void ValueLogsProxyModel::setAverage(ValueLogsProxyModel::Average average) -{ - if (m_average != average) { - m_average = average; - emit averageChanged(); - } -} - -QVariant ValueLogsProxyModel::minimumValue() const -{ - return m_minimumValue; -} - -QVariant ValueLogsProxyModel::maximumValue() const -{ - return m_maximumValue; -} - -void ValueLogsProxyModel::logsReply(const QVariantMap &data) -{ - - qDebug() << "logs reply"; - - beginResetModel(); - - m_minimumValue = QVariant(); - m_maximumValue = QVariant(); - - int stepSize = 1; - switch (m_average) { - case AverageMonth: - stepSize *= 30; - // fall through - case AverageDay: - stepSize *= 8; - // fall through - case AverageDayTime: - stepSize *= 3; - // fall through - case AverageHourly: - stepSize *= 4; - // fall through - case AverageQuarterHour: - stepSize *= 15; - // fall through - case AverageMinute: - stepSize *= 60; - } - int totalSlots = startTime().secsTo(endTime()) / stepSize; - qDebug() << "slots" << totalSlots; - - QHash > entries; - - QList logEntries = data.value("params").toMap().value("logEntries").toList(); - - QVariant startValue; - for (int i = 0; i < logEntries.count(); i++) { - QVariantMap entryMap = logEntries.at(i).toMap(); - QDateTime entryTimestamp = QDateTime::fromMSecsSinceEpoch(entryMap.value("timestamp").toLongLong()); - int slot = startTime().secsTo(entryTimestamp) / stepSize; - if (slot < 0) { - // We're before the actual starttime (see update()). store the most recent value - startValue = entryMap.value("value"); -// qDebug() << "have new startvalue" << startValue << entryTimestamp; - continue; - } - QList tmp = entries[slot]; - QVariant value = entryMap.value("value"); - value.convert(QVariant::Double); - tmp.append(value); - entries[slot] = tmp; -// qDebug() << "adding value to slot" << slot << entryMap.value("value") << QDateTime::fromMSecsSinceEpoch(entryMap.value("timestamp").toLongLong()); - } - if (!startValue.isNull() && entries[0].isEmpty()) { - QList tmp; - tmp.append(startValue); - entries[0] = tmp; - } -// qDebug() << "slotsize:" << stepSize << entries.keys(); - - qDeleteAll(m_list); - m_list.clear(); - for (int i = 0; i <= totalSlots; i++) { - QVariant avg = 0; - int counter = 0; - foreach (const QVariant &value, entries[i]) { - avg = avg.toDouble() + value.toDouble(); - counter++; - } - if (counter > 0) { - avg = avg.toDouble() / counter; - } else if (entries[i-1].count() > 0) { - avg = entries[i-1].last().toDouble(); - } else if (m_list.count() > 0){ - avg = m_list.last()->value().toDouble(); - } else { - continue; - } - LogEntry *entry = new LogEntry(startTime().addSecs(stepSize * i)/*.addSecs(stepSize * .5)*/, avg, m_deviceId, QString(), LogEntry::LoggingSourceStates, LogEntry::LoggingEventTypeTrigger, this); - m_list.append(entry); - - if (m_minimumValue.isNull() || entry->value() < m_minimumValue) { - m_minimumValue = qRound(entry->value().toDouble()); - } - if (m_maximumValue.isNull() || entry->value() > m_maximumValue) { - m_maximumValue = qRound(entry->value().toDouble()); - } -// qDebug() << "filling slot" << i << "average:" << avg << entry->timestamp().toString() << "min:" << m_minimumValue << "max:" << m_maximumValue; - - } - - endResetModel(); - - if (m_minimumValue.isNull()) { - m_minimumValue = 0; - } - if (m_maximumValue.isNull()) { - m_maximumValue = 0; - } - - emit minimumValueChanged(); - emit maximumValueChanged(); - emit countChanged(); - qDebug() << "min" << minimumValue() << "max" << maximumValue(); - - m_busy = false; - emit busyChanged(); - -} diff --git a/libnymea-app/models/valuelogsproxymodel.h b/libnymea-app/models/valuelogsproxymodel.h deleted file mode 100644 index 1455e9c3..00000000 --- a/libnymea-app/models/valuelogsproxymodel.h +++ /dev/null @@ -1,83 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* Copyright 2013 - 2020, nymea GmbH -* Contact: contact@nymea.io -* -* This file is part of nymea. -* This project including source code and documentation is protected by -* copyright law, and remains the property of nymea GmbH. All rights, including -* reproduction, publication, editing and translation, are reserved. The use of -* this project is subject to the terms of a license agreement to be concluded -* with nymea GmbH in accordance with the terms of use of nymea GmbH, available -* under https://nymea.io/license -* -* GNU General Public License Usage -* Alternatively, this project may be redistributed and/or modified under the -* terms of the GNU General Public License as published by the Free Software -* Foundation, GNU version 3. This project is distributed in the hope that it -* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -* Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this project. If not, see . -* -* For any further details and any questions please contact us under -* contact@nymea.io or see our FAQ/Licensing Information on -* https://nymea.io/license/faq -* -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -#ifndef VALUELOGSPROXYMODEL_H -#define VALUELOGSPROXYMODEL_H - -#include - -#include "logsmodel.h" - -class ValueLogsProxyModel : public LogsModel -{ - Q_OBJECT - Q_PROPERTY(Average average READ average WRITE setAverage NOTIFY averageChanged) - - Q_PROPERTY(QVariant minimumValue READ minimumValue NOTIFY minimumValueChanged) - Q_PROPERTY(QVariant maximumValue READ maximumValue NOTIFY maximumValueChanged) - -public: - enum Average { - AverageMonth, - AverageDay, - AverageDayTime, - AverageHourly, - AverageQuarterHour, - AverageMinute - }; - Q_ENUM(Average) - - explicit ValueLogsProxyModel(QObject *parent = nullptr); - - void update() override; - - Average average() const; - void setAverage(Average average); - - QVariant minimumValue() const; - QVariant maximumValue() const; - -signals: - void averageChanged(); - - void minimumValueChanged(); - void maximumValueChanged(); - -protected: - void logsReply(const QVariantMap &data) override; - -private: - Average m_average = AverageHourly; - - QVariant m_minimumValue; - QVariant m_maximumValue; -}; - -#endif // VALUELOGSPROXYMODEL_H diff --git a/libnymea-app/models/xyseriesadapter.cpp b/libnymea-app/models/xyseriesadapter.cpp new file mode 100644 index 00000000..5cdf345a --- /dev/null +++ b/libnymea-app/models/xyseriesadapter.cpp @@ -0,0 +1,178 @@ +#include "xyseriesadapter.h" + +XYSeriesAdapter::XYSeriesAdapter(QObject *parent) : QObject(parent) +{ + +} + +LogsModel *XYSeriesAdapter::logsModel() const +{ + return m_model; +} + +void XYSeriesAdapter::setLogsModel(LogsModel *logsModel) +{ + if (m_model != logsModel) { + m_model = logsModel; + emit logsModelChanged(); +// update(); + connect(logsModel, &LogsModel::logEntryAdded, this, &XYSeriesAdapter::logEntryAdded); + } +} + +QtCharts::QXYSeries *XYSeriesAdapter::xySeries() const +{ + return m_series; +} + +void XYSeriesAdapter::setXySeries(QtCharts::QXYSeries *series) +{ + if (m_series != series) { + m_series = series; + emit xySeriesChanged(); + } +} + +QtCharts::QXYSeries *XYSeriesAdapter::baseSeries() const +{ + return m_baseSeries; +} + +void XYSeriesAdapter::setBaseSeries(QtCharts::QXYSeries *series) +{ + if (m_baseSeries != series) { + m_baseSeries = series; + emit baseSeriesChanged(); + + connect(m_baseSeries, &QtCharts::QXYSeries::pointAdded, this, [=](int index){ + if (m_series->count() > index) { + m_series->replace(index, m_series->at(index).x(), calculateSampleValue(index)); + } + }); + connect(m_baseSeries, &QtCharts::QXYSeries::pointReplaced, this, [=](int index){ + if (m_series->count() > index) { + m_series->replace(index, m_series->at(index).x(), calculateSampleValue(index)); + } + }); + } +} + +XYSeriesAdapter::SampleRate XYSeriesAdapter::sampleRate() const +{ + return m_sampleRate; +} + +void XYSeriesAdapter::setSampleRate(XYSeriesAdapter::SampleRate sampleRate) +{ + if (m_sampleRate != sampleRate) { + m_sampleRate = sampleRate; + emit sampleRateChanged(); + } +} + +bool XYSeriesAdapter::smooth() const +{ + return m_smooth; +} + +void XYSeriesAdapter::setSmooth(bool smooth) +{ + if (m_smooth != smooth) { + m_smooth = smooth; + emit smoothChanged(); + } +} + +qreal XYSeriesAdapter::maxValue() const +{ + return m_maxValue; +} + +qreal XYSeriesAdapter::minValue() const +{ + return m_minValue; +} + +void XYSeriesAdapter::logEntryAdded(LogEntry *entry) +{ + if (!m_series) { + return; + } + + if (m_samples.isEmpty()) { + Sample *sample = new Sample(); + sample->timestamp = entry->timestamp().addSecs(m_sampleRate); + sample->entries.append(entry); + sample->last = entry; + m_newestSample = sample->timestamp; + m_oldestSample = m_newestSample; + m_samples.append(sample); + m_series->insert(0, QPointF(sample->timestamp.toMSecsSinceEpoch(), entry->value().toDouble())); + return; + } + + while (entry->timestamp() > m_newestSample) { + Sample *sample = new Sample(); + sample->timestamp = m_newestSample.addSecs(m_sampleRate); + sample->last = m_samples.value(0)->last; + m_newestSample = sample->timestamp; + m_samples.prepend(sample); + m_series->insert(0, QPointF(sample->timestamp.toMSecsSinceEpoch(), sample->last->value().toDouble())); + } + + while (entry->timestamp() < m_oldestSample.addSecs(m_sampleRate)) { + Sample *sample = new Sample(); + sample->timestamp = m_oldestSample.addSecs(-m_sampleRate); + sample->last = entry; + m_oldestSample = sample->timestamp; + m_samples.append(sample); + m_series->append(sample->timestamp.toMSecsSinceEpoch(), sample->last->value().toDouble()); + } + + + int idx = entry->timestamp().secsTo(m_newestSample) / m_sampleRate; + if (idx > m_samples.count()) { + qWarning() << "Overflowing integer size for XYSeriesAdapter!"; + return; + } + Sample *sample = m_samples.at(static_cast(idx)); + sample->entries.append(entry); + + qreal value = calculateSampleValue(idx); + m_series->replace(idx, sample->timestamp.toMSecsSinceEpoch(), value); + + if (value < m_minValue) { + m_minValue = value; + emit minValueChanged(); + } + if (value > m_maxValue) { + m_maxValue = value; + emit maxValueChanged(); + } +} + +qreal XYSeriesAdapter::calculateSampleValue(int index) +{ + Sample *sample = m_samples.at(index); + qreal value = 0; + int count = 0; + if (m_samples.length() > index + 1) { + Sample *previousSample = m_samples.at(static_cast(index) + 1); + value = previousSample->last->value().toDouble(); + count++; + } + foreach (LogEntry *entry, sample->entries) { + value += entry->value().toDouble(); + count++; + } + + if (count > 1) { + value /= count; + } + + if (m_baseSeries && m_baseSeries->count() > index) { + value += m_baseSeries->at(index).y(); + } + + return value; +} diff --git a/libnymea-app/models/xyseriesadapter.h b/libnymea-app/models/xyseriesadapter.h new file mode 100644 index 00000000..4b9f051c --- /dev/null +++ b/libnymea-app/models/xyseriesadapter.h @@ -0,0 +1,87 @@ +#ifndef XYSERIESADAPTER_H +#define XYSERIESADAPTER_H + +#include "logsmodel.h" + +#include +#include + +class XYSeriesAdapter : public QObject +{ + Q_OBJECT + Q_PROPERTY(LogsModel* logsModel READ logsModel WRITE setLogsModel NOTIFY logsModelChanged) + Q_PROPERTY(QtCharts::QXYSeries* xySeries READ xySeries WRITE setXySeries NOTIFY xySeriesChanged) + Q_PROPERTY(QtCharts::QXYSeries* baseSeries READ baseSeries WRITE setBaseSeries NOTIFY baseSeriesChanged) + + Q_PROPERTY(SampleRate sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged) + Q_PROPERTY(bool smooth READ smooth WRITE setSmooth NOTIFY smoothChanged) + + Q_PROPERTY(qreal maxValue READ maxValue NOTIFY maxValueChanged) + Q_PROPERTY(qreal minValue READ minValue NOTIFY minValueChanged) + +public: + enum SampleRate { + SampleRateSecond = 1, + SampleRateMinute = 60, + SampleRateHour = 60 * 60, + SampleRateDays = 24 * 60 * 60 + }; + Q_ENUM(SampleRate) + + explicit XYSeriesAdapter(QObject *parent = nullptr); + + LogsModel* logsModel() const; + void setLogsModel(LogsModel *logsModel); + + QtCharts::QXYSeries* xySeries() const; + void setXySeries(QtCharts::QXYSeries *series); + + QtCharts::QXYSeries* baseSeries() const; + void setBaseSeries(QtCharts::QXYSeries *series); + + SampleRate sampleRate() const; + void setSampleRate(SampleRate sampleRate); + + bool smooth() const; + void setSmooth(bool smooth); + + qreal maxValue() const; + qreal minValue() const; + +signals: + void xySeriesChanged(); + void logsModelChanged(); + void baseSeriesChanged(); + void sampleRateChanged(); + void smoothChanged(); + void maxValueChanged(); + void minValueChanged(); + +private slots: + void logEntryAdded(LogEntry *entry); + +private: + qreal calculateSampleValue(int index); + +private: + class Sample { + public: + QDateTime timestamp; // The timestamp where this sample *ends* + QList entries; // all log entries in this sample, that is, from timestamp - m_sampleRate + LogEntry *last = nullptr; + }; + LogsModel* m_model = nullptr; + QtCharts::QXYSeries* m_series = nullptr; + QtCharts::QXYSeries* m_baseSeries = nullptr; + SampleRate m_sampleRate = SampleRateSecond; + bool m_smooth = true; + + QVector m_samples; + QDateTime m_newestSample; + QDateTime m_oldestSample; + + qreal m_maxValue = 0; + qreal m_minValue = 0; +}; + +#endif // XYSERIESADAPTER_H diff --git a/libnymea-app/ruletemplates/ruletemplates.cpp b/libnymea-app/ruletemplates/ruletemplates.cpp index 05e7fcbb..13d1bdaa 100644 --- a/libnymea-app/ruletemplates/ruletemplates.cpp +++ b/libnymea-app/ruletemplates/ruletemplates.cpp @@ -302,7 +302,7 @@ bool RuleTemplatesFilterModel::filterAcceptsRow(int source_row, const QModelInde bool found = false; for (int i = 0; i < m_filterDevicesProxy->rowCount(); i++) { // qDebug() << "Checking device:" << m_filterDevicesProxy->get(i)->deviceClass()->interfaces(); - if (m_filterDevicesProxy->get(i)->deviceClass()->interfaces().contains(toBeFound)) { + if (m_filterDevicesProxy->get(i)->thingClass()->interfaces().contains(toBeFound)) { found = true; break; } diff --git a/libnymea-app/scripting/codecompletion.cpp b/libnymea-app/scripting/codecompletion.cpp index 20b986cc..9c1fc99b 100644 --- a/libnymea-app/scripting/codecompletion.cpp +++ b/libnymea-app/scripting/codecompletion.cpp @@ -169,7 +169,7 @@ void CodeCompletion::update() if (thingIdExp.exactMatch(blockText)) { for (int i = 0; i < m_engine->deviceManager()->devices()->rowCount(); i++) { Device *dev = m_engine->deviceManager()->devices()->get(i); - entries.append(CompletionModel::Entry(dev->id().toString() + "\" // " + dev->name(), dev->name(), "thing", dev->deviceClass()->interfaces().join(","))); + entries.append(CompletionModel::Entry(dev->id().toString() + "\" // " + dev->name(), dev->name(), "thing", dev->thingClass()->interfaces().join(","))); } blockText.remove(QRegExp(".*thingId: \"")); m_model->update(entries); @@ -182,7 +182,7 @@ void CodeCompletion::update() if (deviceIdExp.exactMatch(blockText)) { for (int i = 0; i < m_engine->deviceManager()->devices()->rowCount(); i++) { Device *dev = m_engine->deviceManager()->devices()->get(i); - entries.append(CompletionModel::Entry(dev->id().toString() + "\" // " + dev->name(), dev->name(), "thing", dev->deviceClass()->interfaces().join(","))); + entries.append(CompletionModel::Entry(dev->id().toString() + "\" // " + dev->name(), dev->name(), "thing", dev->thingClass()->interfaces().join(","))); } blockText.remove(QRegExp(".*deviceId: \"")); m_model->update(entries); @@ -209,8 +209,8 @@ void CodeCompletion::update() return; } - for (int i = 0; i < device->deviceClass()->stateTypes()->rowCount(); i++) { - StateType *stateType = device->deviceClass()->stateTypes()->get(i); + for (int i = 0; i < device->thingClass()->stateTypes()->rowCount(); i++) { + StateType *stateType = device->thingClass()->stateTypes()->get(i); entries.append(CompletionModel::Entry(stateType->id().toString() + "\" // " + stateType->name(), stateType->name(), "stateType")); } blockText.remove(QRegExp(".*stateTypeId: \"")); @@ -241,8 +241,8 @@ void CodeCompletion::update() } qDebug() << "Device is" << device->name(); - for (int i = 0; i < device->deviceClass()->stateTypes()->rowCount(); i++) { - StateType *stateType = device->deviceClass()->stateTypes()->get(i); + for (int i = 0; i < device->thingClass()->stateTypes()->rowCount(); i++) { + StateType *stateType = device->thingClass()->stateTypes()->get(i); entries.append(CompletionModel::Entry(stateType->name() + "\"", stateType->name(), "stateType")); } blockText.remove(QRegExp(".*stateName: \"")); @@ -270,8 +270,8 @@ void CodeCompletion::update() return; } - for (int i = 0; i < device->deviceClass()->actionTypes()->rowCount(); i++) { - ActionType *actionType = device->deviceClass()->actionTypes()->get(i); + for (int i = 0; i < device->thingClass()->actionTypes()->rowCount(); i++) { + ActionType *actionType = device->thingClass()->actionTypes()->get(i); entries.append(CompletionModel::Entry(actionType->id().toString() + "\" // " + actionType->name(), actionType->name(), "actionType")); } blockText.remove(QRegExp(".*actionTypeId: \"")); @@ -299,8 +299,8 @@ void CodeCompletion::update() return; } - for (int i = 0; i < device->deviceClass()->actionTypes()->rowCount(); i++) { - ActionType *actionType = device->deviceClass()->actionTypes()->get(i); + for (int i = 0; i < device->thingClass()->actionTypes()->rowCount(); i++) { + ActionType *actionType = device->thingClass()->actionTypes()->get(i); entries.append(CompletionModel::Entry(actionType->name() + "\"", actionType->name(), "actionType")); } blockText.remove(QRegExp(".*actionName: \"")); @@ -328,8 +328,8 @@ void CodeCompletion::update() return; } - for (int i = 0; i < device->deviceClass()->eventTypes()->rowCount(); i++) { - EventType *eventType = device->deviceClass()->eventTypes()->get(i); + for (int i = 0; i < device->thingClass()->eventTypes()->rowCount(); i++) { + EventType *eventType = device->thingClass()->eventTypes()->get(i); entries.append(CompletionModel::Entry(eventType->id().toString() + "\" // " + eventType->name(), eventType->name(), "eventType")); } blockText.remove(QRegExp(".*eventTypeId: \"")); @@ -358,8 +358,8 @@ void CodeCompletion::update() return; } - for (int i = 0; i < device->deviceClass()->eventTypes()->rowCount(); i++) { - EventType *eventType = device->deviceClass()->eventTypes()->get(i); + for (int i = 0; i < device->thingClass()->eventTypes()->rowCount(); i++) { + EventType *eventType = device->thingClass()->eventTypes()->get(i); entries.append(CompletionModel::Entry(eventType->name() + "\"", eventType->name(), "eventType")); } blockText.remove(QRegExp(".*eventName: \"")); diff --git a/libnymea-app/system/systemcontroller.cpp b/libnymea-app/system/systemcontroller.cpp index aeb7565b..f16a2db5 100644 --- a/libnymea-app/system/systemcontroller.cpp +++ b/libnymea-app/system/systemcontroller.cpp @@ -389,9 +389,15 @@ void SystemController::notificationReceived(const QVariantMap &data) emit powerManagementAvailableChanged(); emit updateManagementAvailableChanged(); } else if (notification == "System.TimeConfigurationChanged") { - qDebug() << "System time configuration changed"; + qDebug() << "System time configuration changed" << data.value("params").toMap().value("timeZone").toByteArray(); m_serverTime = QDateTime::fromSecsSinceEpoch(data.value("params").toMap().value("time").toUInt()); - m_serverTime.setTimeZone(QTimeZone(data.value("params").toMap().value("timeZone").toByteArray())); + + // NOTE: Ideally we'd just set the TimeZone of our serverTime prooperly, however, there's a bug on Android + // Which doesn't allow to create QTimeZone objects by IANA id.... So, let's keep that separated in a string + // https://bugreports.qt.io/browse/QTBUG-83438 + // m_serverTime.setTimeZone(QTimeZone(data.value("params").toMap().value("timeZone").toByteArray())); + m_serverTimeZone = data.value("params").toMap().value("timeZone").toString(); + emit serverTimeChanged(); emit serverTimeZoneChanged(); m_automaticTimeAvailable = data.value("params").toMap().value("automaticTimeAvailable").toBool(); diff --git a/libnymea-app/thinggroup.cpp b/libnymea-app/thinggroup.cpp index e0f2a84e..492a4837 100644 --- a/libnymea-app/thinggroup.cpp +++ b/libnymea-app/thinggroup.cpp @@ -82,7 +82,7 @@ int ThingGroup::executeAction(const QString &actionName, const QVariantList &par if (device->setupStatus() != Device::DeviceSetupStatusComplete) { continue; } - ActionType *actionType = device->deviceClass()->actionTypes()->findByName(actionName); + ActionType *actionType = device->thingClass()->actionTypes()->findByName(actionName); if (!actionType) { continue; } @@ -110,8 +110,8 @@ int ThingGroup::executeAction(const QString &actionName, const QVariantList &par void ThingGroup::syncStates() { - for (int i = 0; i < deviceClass()->stateTypes()->rowCount(); i++) { - StateType *stateType = deviceClass()->stateTypes()->get(i); + for (int i = 0; i < thingClass()->stateTypes()->rowCount(); i++) { + StateType *stateType = thingClass()->stateTypes()->get(i); State *state = states()->getState(stateType->id()); qDebug() << "syncing state" << stateType->name(); @@ -121,13 +121,13 @@ void ThingGroup::syncStates() for (int j = 0; j < m_devices->rowCount(); j++) { Device *d = m_devices->get(j); // Skip things that don't have the required state - StateType *ds = d->deviceClass()->stateTypes()->findByName(stateType->name()); + StateType *ds = d->thingClass()->stateTypes()->findByName(stateType->name()); if (!ds) { continue; } // Skip disconnected things - StateType *connectedStateType = d->deviceClass()->stateTypes()->findByName("connected"); + StateType *connectedStateType = d->thingClass()->stateTypes()->findByName("connected"); if (connectedStateType) { if (!d->stateValue(connectedStateType->id()).toBool()) { continue; diff --git a/libnymea-app/types/device.cpp b/libnymea-app/types/device.cpp index eb3f845c..989f547f 100644 --- a/libnymea-app/types/device.cpp +++ b/libnymea-app/types/device.cpp @@ -34,11 +34,11 @@ #include -Device::Device(DeviceManager *deviceManager, DeviceClass *deviceClass, const QUuid &parentDeviceId, QObject *parent) : +Device::Device(DeviceManager *deviceManager, DeviceClass *thingClass, const QUuid &parentId, QObject *parent) : QObject(parent), m_deviceManager(deviceManager), - m_parentDeviceId(parentDeviceId), - m_deviceClass(deviceClass) + m_parentId(parentId), + m_thingClass(thingClass) { } @@ -65,22 +65,22 @@ void Device::setId(const QUuid &id) QUuid Device::deviceClassId() const { - return m_deviceClass->id(); + return m_thingClass->id(); } QUuid Device::thingClassId() const { - return m_deviceClass->id(); + return m_thingClass->id(); } QUuid Device::parentDeviceId() const { - return m_parentDeviceId; + return m_parentId; } bool Device::isChild() const { - return !m_parentDeviceId.isNull(); + return !m_parentId.isNull(); } Device::DeviceSetupStatus Device::setupStatus() const @@ -153,14 +153,23 @@ void Device::setStates(States *states) } } -DeviceClass *Device::deviceClass() const +State *Device::state(const QUuid &stateTypeId) const { - return m_deviceClass; + return m_states->getState(stateTypeId); +} + +State *Device::stateByName(const QString &stateName) const +{ + StateType *st = m_thingClass->stateTypes()->findByName(stateName); + if (!st) { + return nullptr; + } + return m_states->getState(st->id()); } DeviceClass *Device::thingClass() const { - return m_deviceClass; + return m_thingClass; } bool Device::hasState(const QUuid &stateTypeId) @@ -195,7 +204,7 @@ void Device::setStateValue(const QUuid &stateTypeId, const QVariant &value) int Device::executeAction(const QString &actionName, const QVariantList ¶ms) { - ActionType *actionType = m_deviceClass->actionTypes()->findByName(actionName); + ActionType *actionType = m_thingClass->actionTypes()->findByName(actionName); QVariantList finalParams; foreach (const QVariant ¶mVariant, params) { @@ -209,30 +218,30 @@ int Device::executeAction(const QString &actionName, const QVariantList ¶ms) return m_deviceManager->executeAction(m_id, actionType->id(), finalParams); } -QDebug operator<<(QDebug &dbg, Device *device) +QDebug operator<<(QDebug &dbg, Device *thing) { - dbg.nospace() << "Device: " << device->name() << " (" << device->id().toString() << ") Class:" << device->deviceClass()->name() << " (" << device->deviceClassId().toString() << ")" << endl; - for (int i = 0; i < device->deviceClass()->paramTypes()->rowCount(); i++) { - ParamType *pt = device->deviceClass()->paramTypes()->get(i); - Param *p = device->params()->getParam(pt->id().toString()); + dbg.nospace() << "Thing: " << thing->name() << " (" << thing->id().toString() << ") Class:" << thing->thingClass()->name() << " (" << thing->thingClassId().toString() << ")" << endl; + for (int i = 0; i < thing->thingClass()->paramTypes()->rowCount(); i++) { + ParamType *pt = thing->thingClass()->paramTypes()->get(i); + Param *p = thing->params()->getParam(pt->id().toString()); if (p) { dbg << " Param " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << p->value() << endl; } else { dbg << " Param " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << "*** Unknown value ***" << endl; } } - for (int i = 0; i < device->deviceClass()->settingsTypes()->rowCount(); i++) { - ParamType *pt = device->deviceClass()->settingsTypes()->get(i); - Param *p = device->settings()->getParam(pt->id().toString()); + for (int i = 0; i < thing->thingClass()->settingsTypes()->rowCount(); i++) { + ParamType *pt = thing->thingClass()->settingsTypes()->get(i); + Param *p = thing->settings()->getParam(pt->id().toString()); if (p) { dbg << " Setting " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << p->value() << endl; } else { dbg << " Setting " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << "*** Unknown value ***" << endl; } } - for (int i = 0; i < device->deviceClass()->stateTypes()->rowCount(); i++) { - StateType *st = device->deviceClass()->stateTypes()->get(i); - State *s = device->states()->getState(st->id()); + for (int i = 0; i < thing->thingClass()->stateTypes()->rowCount(); i++) { + StateType *st = thing->thingClass()->stateTypes()->get(i); + State *s = thing->states()->getState(st->id()); dbg << " State " << i << ": " << st->id() << ": " << st->name() << " = " << s->value() << endl; } return dbg; diff --git a/libnymea-app/types/device.h b/libnymea-app/types/device.h index 8eb97ab4..1f60b3fb 100644 --- a/libnymea-app/types/device.h +++ b/libnymea-app/types/device.h @@ -55,7 +55,7 @@ class Device : public QObject Q_PROPERTY(Params *params READ params NOTIFY paramsChanged) Q_PROPERTY(Params *settings READ settings NOTIFY settingsChanged) Q_PROPERTY(States *states READ states NOTIFY statesChanged) - Q_PROPERTY(DeviceClass *deviceClass READ deviceClass CONSTANT) + Q_PROPERTY(DeviceClass *deviceClass READ thingClass CONSTANT) Q_PROPERTY(DeviceClass *thingClass READ thingClass CONSTANT) public: @@ -67,7 +67,7 @@ public: }; Q_ENUM(DeviceSetupStatus) - explicit Device(DeviceManager *deviceManager, DeviceClass *deviceClass, const QUuid &parentDeviceId = QUuid(), QObject *parent = nullptr); + explicit Device(DeviceManager *deviceManager, DeviceClass *thingClass, const QUuid &parentId = QUuid(), QObject *parent = nullptr); QUuid id() const; void setId(const QUuid &id); @@ -93,10 +93,11 @@ public: States *states() const; void setStates(States *states); - DeviceClass *deviceClass() const; DeviceClass *thingClass() const; Q_INVOKABLE bool hasState(const QUuid &stateTypeId); + Q_INVOKABLE State *state(const QUuid &stateTypeId) const; + Q_INVOKABLE State *stateByName(const QString &stateName) const; Q_INVOKABLE QVariant stateValue(const QUuid &stateTypeId); void setStateValue(const QUuid &stateTypeId, const QVariant &value); @@ -117,15 +118,15 @@ protected: DeviceManager *m_deviceManager = nullptr; QString m_name; QUuid m_id; - QUuid m_parentDeviceId; + QUuid m_parentId; DeviceSetupStatus m_setupStatus = DeviceSetupStatusNone; QString m_setupDisplayMessage; Params *m_params = nullptr; Params *m_settings = nullptr; States *m_states = nullptr; - DeviceClass *m_deviceClass = nullptr; + DeviceClass *m_thingClass = nullptr; }; -QDebug operator<<(QDebug &dbg, Device* device); +QDebug operator<<(QDebug &dbg, Device* thing); #endif // DEVICE_H diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index e49974da..6db2a609 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -99,7 +99,7 @@ ui/images/media-seek-forward.svg ui/images/media-skip-backward.svg ui/images/media-skip-forward.svg - ui/images/mediaplayer-app-symbolic.svg + ui/images/media.svg ui/images/navigation-menu.svg ui/images/network-secure.svg ui/images/network-vpn.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 3690d04a..4a479634 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -50,18 +50,13 @@ ui/customviews/CustomViewBase.qml ui/customviews/WeatherView.qml ui/customviews/MediaControllerView.qml - ui/customviews/SensorView.qml ui/customviews/NotificationsView.qml ui/customviews/ExtendedVolumeController.qml ui/devicepages/MediaDevicePage.qml ui/devicepages/ButtonDevicePage.qml ui/devicepages/GenericDevicePage.qml - ui/devicepages/WeatherDevicePagePre110.qml - ui/devicepages/WeatherDevicePagePost110.qml ui/devicepages/WeatherDevicePage.qml ui/devicepages/SensorDevicePage.qml - ui/devicepages/SensorDevicePagePre110.qml - ui/devicepages/SensorDevicePagePost110.qml ui/devicepages/DevicePageBase.qml ui/devicepages/InputTriggerDevicePage.qml ui/devicepages/StateLogPage.qml @@ -134,7 +129,6 @@ ../LICENSE.OFL ../LICENSE.OpenSSL ../LICENSE.LGPL3 - ui/customviews/GenericTypeGraphPre110.qml ui/customviews/GenericTypeGraph.qml ui/devicepages/SmartMeterDevicePage.qml ui/devicelistpages/SmartMeterDeviceListPage.qml @@ -222,5 +216,8 @@ ui/mainviews/EnergyView.qml ui/components/MainViewBase.qml ui/components/SmartMeterChart.qml + ui/mainviews/MediaView.qml + ui/components/ShuffleRepeatVolumeControl.qml + ui/components/MediaBrowser.qml diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 21e44622..db13f6af 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -120,6 +120,7 @@ Page { ListElement { name: "scenes"; source: "ScenesView"; displayName: qsTr("Scenes"); icon: "slideshow" } ListElement { name: "garages"; source: "GaragesView"; displayName: qsTr("Garages"); icon: "garage/garage-100" } ListElement { name: "energy"; source: "EnergyView"; displayName: qsTr("Energy"); icon: "smartmeter" } + ListElement { name: "media"; source: "MediaView"; displayName: qsTr("Media"); icon: "media" } } ListModel { diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 90843525..bec42a4b 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -259,7 +259,7 @@ ApplicationWindow { case "mediacontroller": case "extendedmediacontroller": case "mediaplayer": - return Qt.resolvedUrl("images/mediaplayer-app-symbolic.svg") + return Qt.resolvedUrl("images/media.svg") case "powersocket": return Qt.resolvedUrl("images/powersocket.svg") case "button": diff --git a/nymea-app/ui/components/MediaArtworkImage.qml b/nymea-app/ui/components/MediaArtworkImage.qml index 14d42bc6..c643367a 100644 --- a/nymea-app/ui/components/MediaArtworkImage.qml +++ b/nymea-app/ui/components/MediaArtworkImage.qml @@ -36,13 +36,13 @@ import Nymea 1.0 Item { id: root - property Device device: null + property Thing thing: null - readonly property StateType artworkStateType: device ? device.deviceClass.stateTypes.findByName("artwork") : null - readonly property State artworkState: artworkStateType ? device.states.getState(artworkStateType.id) : null + readonly property StateType artworkStateType: thing ? thing.thingClass.stateTypes.findByName("artwork") : null + readonly property State artworkState: artworkStateType ? thing.states.getState(artworkStateType.id) : null - readonly property StateType playerTypeStateType: device ? device.deviceClass.stateTypes.findByName("playerType") : null - readonly property State playerTypeState: playerTypeStateType ? device.states.getState(playerTypeStateType.id) : null + readonly property StateType playerTypeStateType: thing ? thing.thingClass.stateTypes.findByName("playerType") : null + readonly property State playerTypeState: playerTypeStateType ? thing.states.getState(playerTypeStateType.id) : null Pane { Material.elevation: 2 diff --git a/nymea-app/ui/components/MediaBrowser.qml b/nymea-app/ui/components/MediaBrowser.qml new file mode 100644 index 00000000..a19627fd --- /dev/null +++ b/nymea-app/ui/components/MediaBrowser.qml @@ -0,0 +1,101 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.5 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.1 +import QtGraphicalEffects 1.0 +import Nymea 1.0 +import "../delegates" + + +Item { + id: root + + property Thing thing: null + + function backPressed() { + if (internalPageStack.depth > 1) { + internalPageStack.pop(); + } else { + swipeView.currentIndex-- + } + } + + StackView { + id: internalPageStack + anchors.fill: parent + initialItem: internalBrowserPage + + Component { + id: internalBrowserPage + ListView { + id: listView + model: browserItems + ScrollBar.vertical: ScrollBar {} + + property string nodeId: "" + + // Need to keep a explicit property here or the GC will eat it too early + property BrowserItems browserItems: null + Component.onCompleted: { + browserItems = engine.thingManager.browseDevice(root.thing.id, nodeId); + } + + delegate: BrowserItemDelegate { + iconName: "../images/browser/" + (model.mediaIcon && model.mediaIcon !== "MediaBrowserIconNone" ? model.mediaIcon : model.icon) + ".svg" + busy: d.pendingItemId === model.id + device: root.thing + + onClicked: { + print("clicked:", model.id) + if (model.executable) { + root.executeBrowserItem(model.id) + } else if (model.browsable) { + internalPageStack.push(internalBrowserPage, {device: root.thing, nodeId: model.id}) + } + } + + onContextMenuActionTriggered: { + root.executeBrowserItemAction(model.id, actionTypeId, params) + } + } + + BusyIndicator { + anchors.centerIn: parent + running: listView.model.busy + visible: running + } + } + } + } +} + diff --git a/nymea-app/ui/components/MediaControls.qml b/nymea-app/ui/components/MediaControls.qml index 9249b0f0..27eaa01b 100644 --- a/nymea-app/ui/components/MediaControls.qml +++ b/nymea-app/ui/components/MediaControls.qml @@ -38,15 +38,18 @@ RowLayout { id: root implicitHeight: iconSize + app.margins - property Device device: null + property Thing thing: null property int iconSize: app.iconSize * 1.5 - readonly property StateType playbackStateType: device ? device.deviceClass.stateTypes.findByName("playbackStatus") : null - readonly property State playbackState: playbackStateType ? device.states.getState(playbackStateType.id) : null + readonly property StateType playbackStateType: thing ? thing.thingClass.stateTypes.findByName("playbackStatus") : null + readonly property State playbackState: playbackStateType ? thing.states.getState(playbackStateType.id) : null function executeAction(actionName, params) { - var actionTypeId = device.deviceClass.actionTypes.findByName(actionName).id; - engine.deviceManager.executeAction(device.id, actionTypeId, params) + if (params === undefined) { + params = [] + } + var actionTypeId = thing.thingClass.actionTypes.findByName(actionName).id; + engine.thingManager.executeAction(thing.id, actionTypeId, params) } Item { Layout.fillWidth: true } @@ -68,7 +71,7 @@ RowLayout { } Item { Layout.fillWidth: true } ProgressButton { - Layout.preferredHeight: root,iconSize + Layout.preferredHeight: root.iconSize Layout.preferredWidth: height imageSource: root.playbackState && root.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg" longpressImageSource: "../images/media-playback-stop.svg" diff --git a/nymea-app/ui/components/ShuffleRepeatVolumeControl.qml b/nymea-app/ui/components/ShuffleRepeatVolumeControl.qml new file mode 100644 index 00000000..cd82b8bb --- /dev/null +++ b/nymea-app/ui/components/ShuffleRepeatVolumeControl.qml @@ -0,0 +1,145 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 + +RowLayout { + id: root + + property Thing thing: null + + property State repeatState: thing.stateByName("repeat") + property State shuffleState: thing.stateByName("shuffle") + + property State volumeState: thing.stateByName("volume") + property State muteState: thing.stateByName("mute") + + Item { + Layout.preferredHeight: app.iconSize + Layout.fillWidth: true + visible: root.repeatState !== null + + HeaderButton { + anchors.centerIn: parent + imageSource: root.repeatState.value === "One" ? "../images/media-playlist-repeat-one.svg" : "../images/media-playlist-repeat.svg" + color: root.repeatState.value === "None" ? keyColor : app.accentColor + property var allowedValues: ["None", "All", "One"] + onClicked: { + var params = [] + var param = {} + param["paramTypeId"] = root.repeatState.stateTypeId; + param["value"] = allowedValues[(allowedValues.indexOf(root.repeatState.value) + 1) % 3] + params.push(param) + engine.thingManager.executeAction(root.thing.id, root.repeatState.stateTypeId, params) + } + } + } + + Item { + Layout.preferredHeight: app.iconSize + Layout.fillWidth: true + visible: root.shuffleState !== null + + HeaderButton { + anchors.centerIn: parent + imageSource: "../images/media-playlist-shuffle.svg" + color: root.shuffleState.value === true ? app.accentColor: keyColor + onClicked: { + var params = [] + var param = {} + param["paramTypeId"] = root.shuffleState.stateTypeId + param["value"] = !root.shuffleState.value + params.push(param) + engine.thingManager.executeAction(root.thing.id, root.shuffleState.stateTypeId, params) + } + } + } + + Item { + id: volumeButtonContainer + Layout.fillWidth: true; Layout.fillHeight: true + HeaderButton { + id: volumeButton + anchors.centerIn: parent + imageSource: "../images/audio-speakers-symbolic.svg" + onClicked: { + print(volumeButton.x, volumeButton.y) + print(Qt.point(volumeButton.x, volumeButton.y)) + print(volumeButton.mapToItem(root, volumeButton.x,0)) + var buttonPosition = root.mapFromItem(volumeButtonContainer, volumeButton.x, 0) + var sliderHeight = 200 + var props = {} + props["x"] = buttonPosition.x + props["y"] = buttonPosition.y - sliderHeight + props["height"] = sliderHeight + var sliderPane = volumeSliderPaneComponent.createObject(root, props) + sliderPane.open() + } + } + } + + + Component { + id: volumeSliderPaneComponent + Dialog { + id: volumeSliderDialog + + leftPadding: 0 + topPadding: app.margins / 2 + rightPadding: 0 + bottomPadding: app.margins / 2 + modal: true + + property int pendingVolumeValue: -1 + + contentItem: ColumnLayout { + ThrottledSlider { + Layout.fillHeight: true + from: 0 + to: 100 + value: root.volumeState.value + orientation: Qt.Vertical + onMoved: engine.thingManager.executeAction(root.thing.id, root.volumeState.stateTypeId, [{paramTypeId: root.volumeState.stateTypeId, value: value}]) + } + + HeaderButton { + imageSource: "../images/audio-speakers-muted-symbolic.svg" + color: root.muteState.value === true ? app.accentColor : keyColor + onClicked: engine.thingManager.executeAction(root.thing.id, root.muteState.stateTypeId, [{paramTypeId: root.muteState.stateTypeId, value: !root.muteState.value}]); + } + } + } + } +} diff --git a/nymea-app/ui/components/SmartMeterChart.qml b/nymea-app/ui/components/SmartMeterChart.qml index 350cc071..540d40d4 100644 --- a/nymea-app/ui/components/SmartMeterChart.qml +++ b/nymea-app/ui/components/SmartMeterChart.qml @@ -64,25 +64,19 @@ ChartView { function refresh() { pieSeries.clear(); d.sliceMap = {} - print("calculating", chart.multiplier) for (var i = 0; i < meters.count; i++) { var thing = meters.get(i); - print("thing:", thing.name) var value = 0; var totalConsumedStateType = thing.thingClass.stateTypes.findByName("totalEnergyConsumed") if (totalConsumedStateType) { var totalConsumedState = thing.states.getState(totalConsumedStateType.id) value = value + (totalConsumedState.value * chart.multiplier) - print("Adding", totalConsumedState.value * chart.multiplier, value) } var totalProducedStateType = thing.thingClass.stateTypes.findByName("totalEnergyProduced") if (totalProducedStateType) { var totalProducedState = thing.states.getState(totalProducedStateType.id) value = value - (totalProducedState.value * chart.multiplier) - print("removing", totalProducedState.value * chart.multiplier, value) } - print("consumed", totalConsumedState.value, "produced", totalProducedState.value) - print("value", value) var slice = pieSeries.append(thing.name, Math.max(0, value)) var color = app.accentColor for (var j = 0; j < i; j+=2) { diff --git a/nymea-app/ui/components/ThrottledSlider.qml b/nymea-app/ui/components/ThrottledSlider.qml index 6c4c6fb4..430a7c7d 100644 --- a/nymea-app/ui/components/ThrottledSlider.qml +++ b/nymea-app/ui/components/ThrottledSlider.qml @@ -36,6 +36,8 @@ Item { implicitHeight: slider.implicitHeight implicitWidth: slider.implicitWidth + property alias orientation: slider.orientation + property real value: 0 property alias from: slider.from property alias to: slider.to @@ -49,6 +51,7 @@ Item { Slider { id: slider anchors.left: parent.left; anchors.right: parent.right + anchors.top: parent.top; anchors.bottom: parent.bottom from: 0 to: 100 property var lastSentTime: new Date() diff --git a/nymea-app/ui/customviews/GenericTypeGraph.qml b/nymea-app/ui/customviews/GenericTypeGraph.qml index c228cff6..ccb863a2 100644 --- a/nymea-app/ui/customviews/GenericTypeGraph.qml +++ b/nymea-app/ui/customviews/GenericTypeGraph.qml @@ -143,8 +143,8 @@ Item { } } min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05)) - onMinChanged: print("min set to", min) - onMaxChanged: print("max set to", min) + onMinChanged: applyNiceNumbers(); + onMaxChanged: applyNiceNumbers(); labelsFont.pixelSize: app.smallFont labelFormat: { switch (root.stateType.type.toLowerCase()) { diff --git a/nymea-app/ui/customviews/GenericTypeGraphPre110.qml b/nymea-app/ui/customviews/GenericTypeGraphPre110.qml deleted file mode 100644 index 60bbfdce..00000000 --- a/nymea-app/ui/customviews/GenericTypeGraphPre110.qml +++ /dev/null @@ -1,114 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* Copyright 2013 - 2020, nymea GmbH -* Contact: contact@nymea.io -* -* This file is part of nymea. -* This project including source code and documentation is protected by -* copyright law, and remains the property of nymea GmbH. All rights, including -* reproduction, publication, editing and translation, are reserved. The use of -* this project is subject to the terms of a license agreement to be concluded -* with nymea GmbH in accordance with the terms of use of nymea GmbH, available -* under https://nymea.io/license -* -* GNU General Public License Usage -* Alternatively, this project may be redistributed and/or modified under the -* terms of the GNU General Public License as published by the Free Software -* Foundation, GNU version 3. This project is distributed in the hope that it -* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -* Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this project. If not, see . -* -* For any further details and any questions please contact us under -* contact@nymea.io or see our FAQ/Licensing Information on -* https://nymea.io/license/faq -* -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -import QtQuick 2.9 -import QtQuick.Controls 2.2 -import QtQuick.Controls.Material 2.2 -import QtQuick.Layouts 1.1 -import Nymea 1.0 -import "../components" -import "../customviews" - -ColumnLayout { - id: root - - property var device: null - property var stateType: null - - TabBar { - id: zoomTabBar - Layout.fillWidth: true - TabButton { - text: qsTr("6 h") - property int avg: ValueLogsProxyModel.AverageQuarterHour - property date startTime: { - var date = new Date(); - date.setHours(new Date().getHours() - 6) - date.setMinutes(0) - date.setSeconds(0) - return date; - } - } - TabButton { - text: qsTr("24 h") - property int avg: ValueLogsProxyModel.AverageHourly - property date startTime: { - var date = new Date(); - date.setHours(new Date().getHours() - 24); - date.setMinutes(0) - date.setSeconds(0) - return date; - } - } - TabButton { - text: qsTr("7 d") - property int avg: ValueLogsProxyModel.AverageDayTime - property date startTime: { - var date = new Date(); - date.setDate(new Date().getDate() - 7); - date.setHours(0) - date.setMinutes(0) - date.setSeconds(0) - return date; - } - } - } - - Graph { - Layout.fillWidth: true - Layout.fillHeight: true - mode: settings.graphStyle - color: app.accentColor - - Timer { - id: updateTimer - interval: 10 - repeat: false - onTriggered: { - graphModel.update() - } - } - - model: ValueLogsProxyModel { - id: graphModel - deviceId: root.device.id - typeIds: [stateType.id] - average: zoomTabBar.currentItem.avg - startTime: zoomTabBar.currentItem.startTime - Component.onCompleted: updateTimer.start(); - onAverageChanged: updateTimer.start() - onStartTimeChanged: updateTimer.start(); - engine: _engine - - // Live doesn't work yet with ValueLogsProxyModel - // live: true - } - } -} diff --git a/nymea-app/ui/customviews/SensorView.qml b/nymea-app/ui/customviews/SensorView.qml deleted file mode 100644 index 3eb4dd65..00000000 --- a/nymea-app/ui/customviews/SensorView.qml +++ /dev/null @@ -1,145 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* Copyright 2013 - 2020, nymea GmbH -* Contact: contact@nymea.io -* -* This file is part of nymea. -* This project including source code and documentation is protected by -* copyright law, and remains the property of nymea GmbH. All rights, including -* reproduction, publication, editing and translation, are reserved. The use of -* this project is subject to the terms of a license agreement to be concluded -* with nymea GmbH in accordance with the terms of use of nymea GmbH, available -* under https://nymea.io/license -* -* GNU General Public License Usage -* Alternatively, this project may be redistributed and/or modified under the -* terms of the GNU General Public License as published by the Free Software -* Foundation, GNU version 3. This project is distributed in the hope that it -* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -* Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this project. If not, see . -* -* For any further details and any questions please contact us under -* contact@nymea.io or see our FAQ/Licensing Information on -* https://nymea.io/license/faq -* -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -import QtQuick 2.5 -import QtQuick.Controls 2.1 -import QtQuick.Controls.Material 2.1 -import QtQuick.Layouts 1.3 -import "../components" -import Nymea 1.0 - -CustomViewBase { - id: root - implicitHeight: grid.implicitHeight + app.margins * 2 - - property string interfaceName - - readonly property string stateTypeName: { - switch (interfaceName) { - case "lightsensor": - return "lightIntensity"; - default: - return interfaceName.replace("sensor", ""); - } - } - - readonly property var stateType: deviceClass.stateTypes.findByName(stateTypeName) - readonly property var deviceState: device.states.getState(stateType.id) - - ValueLogsProxyModel { - id: logsModel - engine: _engine - deviceId: root.device.id - typeIds: [stateType.id] - average: zoomTabBar.currentItem.avg - startTime: zoomTabBar.currentItem.startTime - Component.onCompleted: updateTimer.start(); - onAverageChanged: updateTimer.start() - onStartTimeChanged: updateTimer.start(); - } - - Timer { - id: updateTimer - interval: 10 - repeat: false - onTriggered: { - print("updating:", logsModel.startTime) - logsModel.update() - } - } - - ColumnLayout { - id: grid - anchors { left: parent.left; top: parent.top; right: parent.right } - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - spacing: app.margins - ColorIcon { - name: app.interfaceToIcon(root.interfaceName) - height: app.iconSize - width: height - color: app.interfaceToColor(root.interfaceName) - } - Label { - text: Types.toUiValue(deviceState.value, stateType.unit) + " " + Types.toUiUnit(stateType.unit) - font.pixelSize: app.largeFont - } - - TabBar { - id: zoomTabBar - Layout.fillWidth: true - TabButton { - text: qsTr("6 h") - property int avg: ValueLogsProxyModel.AverageQuarterHour - property date startTime: { - var date = new Date(); - date.setHours(new Date().getHours() - 6) - date.setMinutes(0) - date.setSeconds(0) - return date; - } - } - TabButton { - text: qsTr("24 h") - property int avg: ValueLogsProxyModel.AverageHourly - property date startTime: { - var date = new Date(); - date.setHours(new Date().getHours() - 24); - date.setMinutes(0) - date.setSeconds(0) - return date; - } - } - TabButton { - text: qsTr("7 d") - property int avg: ValueLogsProxyModel.AverageDayTime - property date startTime: { - var date = new Date(); - date.setDate(new Date().getDate() - 7); - date.setHours(0) - date.setMinutes(0) - date.setSeconds(0) - return date; - } - } - } - } - Graph { - Layout.fillWidth: true - Layout.preferredHeight: 200 - model: logsModel - mode: settings.graphStyle - color: app.interfaceToColor(root.interfaceName) - } - } -} - diff --git a/nymea-app/ui/delegates/InterfaceTile.qml b/nymea-app/ui/delegates/InterfaceTile.qml index cc67c627..9d936ca5 100644 --- a/nymea-app/ui/delegates/InterfaceTile.qml +++ b/nymea-app/ui/delegates/InterfaceTile.qml @@ -227,7 +227,7 @@ MainPageTile { MediaControls { iconSize: app.iconSize * 1.2 - device: inlineMediaControl.currentDevice + thing: inlineMediaControl.currentDevice } } } diff --git a/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml b/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml index edb49882..abc251aa 100644 --- a/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml +++ b/nymea-app/ui/devicelistpages/MediaDeviceListPage.qml @@ -145,7 +145,7 @@ DeviceListPageBase { } MediaControls { visible: itemDelegate.deviceClass.interfaces.indexOf("mediacontroller") >= 0 - device: itemDelegate.device + thing: itemDelegate.device } } Item { diff --git a/nymea-app/ui/devicepages/DeviceLogPage.qml b/nymea-app/ui/devicepages/DeviceLogPage.qml index b8b59a90..54346688 100644 --- a/nymea-app/ui/devicepages/DeviceLogPage.qml +++ b/nymea-app/ui/devicepages/DeviceLogPage.qml @@ -141,12 +141,7 @@ Page { return; } - var source; - if (engine.jsonRpcClient.ensureServerVersion("1.10")) { - source = Qt.resolvedUrl("../customviews/GenericTypeGraph.qml"); - } else { - source = Qt.resolvedUrl("../customviews/GenericTypeGraphPre110.qml"); - } + var source = Qt.resolvedUrl("../customviews/GenericTypeGraph.qml"); setSource(source, {device: root.device, stateType: stateType}) } } diff --git a/nymea-app/ui/devicepages/MediaDevicePage.qml b/nymea-app/ui/devicepages/MediaDevicePage.qml index 86f20a59..6e2d29f6 100644 --- a/nymea-app/ui/devicepages/MediaDevicePage.qml +++ b/nymea-app/ui/devicepages/MediaDevicePage.qml @@ -173,7 +173,7 @@ DevicePageBase { MediaArtworkImage { Layout.fillHeight: true Layout.preferredWidth: parent.width / parent.columns - device: root.device + thing: root.device } ColumnLayout { @@ -208,7 +208,7 @@ DevicePageBase { } MediaControls { - device: root.device + thing: root.device iconSize: app.iconSize * 2 } } @@ -220,63 +220,8 @@ DevicePageBase { Component { id: browserComponent - Item { - - function backPressed() { - if (internalPageStack.depth > 1) { - internalPageStack.pop(); - } else { - swipeView.currentIndex-- - } - } - - StackView { - id: internalPageStack - anchors.fill: parent - initialItem: internalBrowserPage - - Component { - id: internalBrowserPage - ListView { - id: listView - model: browserItems - ScrollBar.vertical: ScrollBar {} - - property string nodeId: "" - - // Need to keep a explicit property here or the GC will eat it too early - property BrowserItems browserItems: null - Component.onCompleted: { - browserItems = engine.deviceManager.browseDevice(root.device.id, nodeId); - } - - delegate: BrowserItemDelegate { - iconName: "../images/browser/" + (model.mediaIcon && model.mediaIcon !== "MediaBrowserIconNone" ? model.mediaIcon : model.icon) + ".svg" - busy: d.pendingItemId === model.id - device: root.device - - onClicked: { - print("clicked:", model.id) - if (model.executable) { - root.executeBrowserItem(model.id) - } else if (model.browsable) { - internalPageStack.push(internalBrowserPage, {device: root.device, nodeId: model.id}) - } - } - - onContextMenuActionTriggered: { - root.executeBrowserItemAction(model.id, actionTypeId, params) - } - } - - BusyIndicator { - anchors.centerIn: parent - running: listView.model.busy - visible: running - } - } - } - } + MediaBrowser { + thing: root.device } } @@ -287,7 +232,6 @@ DevicePageBase { swipeView.currentIndex--; } - ColumnLayout { anchors.fill: parent anchors.margins: app.margins @@ -300,49 +244,13 @@ DevicePageBase { MediaControls { Layout.fillWidth: true - device: root.device + thing: root.device } } } } - - Component { - id: volumeSliderPaneComponent - Dialog { - - leftPadding: 0 - topPadding: app.margins / 2 - rightPadding: 0 - bottomPadding: app.margins / 2 - modal: true - - contentItem: ColumnLayout { - Slider { - Layout.fillHeight: true - orientation: Qt.Vertical - from: 0 - to: 100 - value: d.pendingVolumeValue != -1 ? d.pendingVolumeValue : root.stateValue("volume") - onMoved: root.adjustVolume(value) - } - HeaderButton { - imageSource: "../images/audio-speakers-muted-symbolic.svg" - color: root.stateValue("mute") ? app.accentColor : keyColor - onClicked: { - var params = [] - var muteParam = {} - muteParam["paramTypeId"] = root.deviceClass.actionTypes.findByName("mute").id - muteParam["value"] = !root.stateValue("mute"); - params.push(muteParam) - root.executeAction("mute", params); - } - } - } - } - } - footer: Pane { Material.elevation: 1 height: 52 @@ -374,64 +282,11 @@ DevicePageBase { onClicked: swipeView.currentIndex-- } } - Item { - Layout.fillWidth: true; Layout.fillHeight: true - visible: root.deviceClass.interfaces.indexOf("shufflerepeat") >= 0 - HeaderButton { - anchors.centerIn: parent - imageSource: root.stateValue("repeat") === "One" ? "../images/media-playlist-repeat-one.svg" : "../images/media-playlist-repeat.svg" - color: root.stateValue("repeat") === "None" ? keyColor : app.accentColor - property var allowedValues: ["None", "All", "One"] - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.deviceClass.actionTypes.findByName("repeat").id; - param["value"] = allowedValues[(allowedValues.indexOf(root.stateValue("repeat")) + 1) % 3] - params.push(param) - root.executeAction("repeat", params) - } - } - } - Item { - Layout.fillWidth: true; Layout.fillHeight: true - visible: root.deviceClass.interfaces.indexOf("shufflerepeat") >= 0 - HeaderButton { - anchors.centerIn: parent - imageSource: "../images/media-playlist-shuffle.svg" - color: root.stateValue("shuffle") ? app.accentColor: keyColor - onClicked: { - var params = [] - var param = {} - param["paramTypeId"] = root.deviceClass.actionTypes.findByName("shuffle").id; - param["value"] = !root.stateValue("shuffle") - params.push(param) - root.executeAction("shuffle", params) - } - } - } - Item { - id: volumeButtonContainer - Layout.fillWidth: true; Layout.fillHeight: true - HeaderButton { - id: volumeButton - anchors.centerIn: parent - imageSource: "../images/audio-speakers-symbolic.svg" - onClicked: { - print("..."); - print(volumeButton.x, volumeButton.y) - print(Qt.point(volumeButton.x, volumeButton.y)) - print(volumeButton.mapToItem(root, volumeButton.x,0)) - var buttonPosition = root.mapFromItem(volumeButtonContainer, volumeButton.x, 0) - var sliderHeight = 200 - var props = {} - props["x"] = buttonPosition.x - props["y"] = root.height - sliderHeight - root.footer.height - props["height"] = sliderHeight - var sliderPane = volumeSliderPaneComponent.createObject(root, props) - sliderPane.open() - } - } + ShuffleRepeatVolumeControl { + Layout.fillWidth: true + thing: root.device } + Item { Layout.fillHeight: true Layout.preferredWidth: swipeView.count > 1 && swipeView.currentIndex < swipeView.count - 1 ? parent.width / 4 : 0 diff --git a/nymea-app/ui/devicepages/SensorDevicePage.qml b/nymea-app/ui/devicepages/SensorDevicePage.qml index 9b2a8875..df75c0c4 100644 --- a/nymea-app/ui/devicepages/SensorDevicePage.qml +++ b/nymea-app/ui/devicepages/SensorDevicePage.qml @@ -38,16 +38,160 @@ import "../customviews" DevicePageBase { id: root - Loader { - anchors.fill: parent - Component.onCompleted: { - var src - if (engine.jsonRpcClient.ensureServerVersion("1.10")) { - src = "SensorDevicePagePost110.qml" - } else { - src = "SensorDevicePagePre110.qml" + Flickable { + id: listView + anchors { fill: parent } + interactive: contentHeight > height + contentHeight: contentGrid.implicitHeight + + GridLayout { + id: contentGrid + width: parent.width + columns: width / 300 + + Repeater { + model: ListModel { + Component.onCompleted: { + var supportedInterfaces = ["temperaturesensor", "humiditysensor", "pressuresensor", "moisturesensor", "lightsensor", "conductivitysensor", "noisesensor", "co2sensor", "presencesensor", "daylightsensor", "closablesensor"] + for (var i = 0; i < supportedInterfaces.length; i++) { + if (root.deviceClass.interfaces.indexOf(supportedInterfaces[i]) >= 0) { + append({name: supportedInterfaces[i]}); + } + } + } + } + + delegate: Loader { + id: loader + Layout.fillWidth: true + Layout.preferredHeight: item.implicitHeight + + property StateType stateType: root.deviceClass.stateTypes.findByName(interfaceStateMap[modelData]) + property string interfaceName: modelData + + // sourceComponent: stateType && stateType.type.toLowerCase() === "bool" ? boolComponent : graphComponent + sourceComponent: graphComponent + + property var interfaceStateMap: { + "temperaturesensor": "temperature", + "humiditysensor": "humidity", + "pressuresensor": "pressure", + "moisturesensor": "moisture", + "lightsensor": "lightIntensity", + "conductivitysensor": "conductivity", + "noisesensor": "noise", + "co2sensor": "co2", + "presencesensor": "isPresent", + "daylightsensor": "daylight", + "closablesensor": "closed" + } + } + + } + } + + + + Component { + id: graphComponent + + GenericTypeGraph { + device: root.device + color: app.interfaceToColor(interfaceName) + iconSource: app.interfaceToIcon(interfaceName) + implicitHeight: width * .6 + property string interfaceName: parent.interfaceName + stateType: parent.stateType + } + } + + Component { + id: boolComponent + GridLayout { + id: boolView + property string interfaceName: parent.interfaceName + property StateType stateType: parent.stateType + height: listView.height + columns: app.landscape ? 2 : 1 + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumWidth: app.iconSize * 5 + Layout.rowSpan: app.landscape ? 5 : 1 + ColorIcon { + anchors.centerIn: parent + height: app.iconSize * 4 + width: height + name: { + switch (boolView.interfaceName) { + case "closablesensor": + return device.states.getState(boolView.stateType.id).value === true ? Qt.resolvedUrl("../images/lock-closed.svg") : Qt.resolvedUrl("../images/lock-open.svg") + default: + return app.interfaceToIcon(boolView.interfaceName) + } + } + color: { + switch (boolView.interfaceName) { + case "closablesensor": + return device.states.getState(boolView.stateType.id).value === true ? "green" : "red" + default: + device.states.getState(boolView.stateType.id).value === true ? app.interfaceToColor(boolView.interfaceName) : keyColor + } + } + } + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + RowLayout { + Layout.fillWidth: false + Layout.alignment: Qt.AlignHCenter + property StateType lastSeenStateType: root.deviceClass.stateTypes.findByName("lastSeenTime") + property State lastSeenState: lastSeenStateType ? root.device.states.getState(lastSeenStateType.id) : null + visible: lastSeenStateType !== null + Label { + text: qsTr("Last seen:") + font.bold: true + } + Label { + text: parent.lastSeenState ? Qt.formatDateTime(new Date(parent.lastSeenState.value * 1000)) : "" + } + } + RowLayout { + Layout.fillWidth: false + Layout.alignment: Qt.AlignHCenter + property StateType sunriseStateType: root.deviceClass.stateTypes.findByName("sunriseTime") + property State sunriseState: sunriseStateType ? root.device.states.getState(sunriseStateType.id) : null + visible: sunriseStateType !== null + Label { + text: qsTr("Sunrise:") + font.bold: true + } + Label { + text: parent.sunriseStateType ? Qt.formatDateTime(new Date(parent.sunriseState.value * 1000)) : "" + } + } + RowLayout { + Layout.fillWidth: false + Layout.alignment: Qt.AlignHCenter + property StateType sunsetStateType: root.deviceClass.stateTypes.findByName("sunsetTime") + property State sunsetState: sunsetStateType ? root.device.states.getState(sunsetStateType.id) : null + visible: sunsetStateType !== null + Label { + text: qsTr("Sunset:") + font.bold: true + } + Label { + text: parent.sunsetStateType ? Qt.formatDateTime(new Date(parent.sunsetState.value * 1000)) : "" + } + } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } } - setSource(Qt.resolvedUrl(src), {device: root.device, deviceClass: root.deviceClass}) } } } + diff --git a/nymea-app/ui/devicepages/SensorDevicePagePost110.qml b/nymea-app/ui/devicepages/SensorDevicePagePost110.qml deleted file mode 100644 index c0132e65..00000000 --- a/nymea-app/ui/devicepages/SensorDevicePagePost110.qml +++ /dev/null @@ -1,192 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* Copyright 2013 - 2020, nymea GmbH -* Contact: contact@nymea.io -* -* This file is part of nymea. -* This project including source code and documentation is protected by -* copyright law, and remains the property of nymea GmbH. All rights, including -* reproduction, publication, editing and translation, are reserved. The use of -* this project is subject to the terms of a license agreement to be concluded -* with nymea GmbH in accordance with the terms of use of nymea GmbH, available -* under https://nymea.io/license -* -* GNU General Public License Usage -* Alternatively, this project may be redistributed and/or modified under the -* terms of the GNU General Public License as published by the Free Software -* Foundation, GNU version 3. This project is distributed in the hope that it -* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -* Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this project. If not, see . -* -* For any further details and any questions please contact us under -* contact@nymea.io or see our FAQ/Licensing Information on -* https://nymea.io/license/faq -* -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -import QtQuick 2.5 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.1 -import Nymea 1.0 -import "../components" -import "../customviews" - -Flickable { - id: listView - anchors { fill: parent } - interactive: contentHeight > height - contentHeight: contentGrid.implicitHeight - - GridLayout { - id: contentGrid - width: parent.width - columns: width / 300 - - Repeater { - model: ListModel { - Component.onCompleted: { - var supportedInterfaces = ["temperaturesensor", "humiditysensor", "pressuresensor", "moisturesensor", "lightsensor", "conductivitysensor", "noisesensor", "co2sensor", "presencesensor", "daylightsensor", "closablesensor"] - for (var i = 0; i < supportedInterfaces.length; i++) { - if (root.deviceClass.interfaces.indexOf(supportedInterfaces[i]) >= 0) { - append({name: supportedInterfaces[i]}); - } - } - } - } - - delegate: Loader { - id: loader - Layout.fillWidth: true - Layout.preferredHeight: item.implicitHeight - - property StateType stateType: root.deviceClass.stateTypes.findByName(interfaceStateMap[modelData]) - property string interfaceName: modelData - -// sourceComponent: stateType && stateType.type.toLowerCase() === "bool" ? boolComponent : graphComponent - sourceComponent: graphComponent - - property var interfaceStateMap: { - "temperaturesensor": "temperature", - "humiditysensor": "humidity", - "pressuresensor": "pressure", - "moisturesensor": "moisture", - "lightsensor": "lightIntensity", - "conductivitysensor": "conductivity", - "noisesensor": "noise", - "co2sensor": "co2", - "presencesensor": "isPresent", - "daylightsensor": "daylight", - "closablesensor": "closed" - } - } - - } - } - - - - Component { - id: graphComponent - - GenericTypeGraph { - device: root.device - color: app.interfaceToColor(interfaceName) - iconSource: app.interfaceToIcon(interfaceName) - implicitHeight: width * .6 - property string interfaceName: parent.interfaceName - stateType: parent.stateType - } - } - - Component { - id: boolComponent - GridLayout { - id: boolView - property string interfaceName: parent.interfaceName - property StateType stateType: parent.stateType - height: listView.height - columns: app.landscape ? 2 : 1 - Item { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumWidth: app.iconSize * 5 - Layout.rowSpan: app.landscape ? 5 : 1 - ColorIcon { - anchors.centerIn: parent - height: app.iconSize * 4 - width: height - name: { - switch (boolView.interfaceName) { - case "closablesensor": - return device.states.getState(boolView.stateType.id).value === true ? Qt.resolvedUrl("../images/lock-closed.svg") : Qt.resolvedUrl("../images/lock-open.svg") - default: - return app.interfaceToIcon(boolView.interfaceName) - } - } - color: { - switch (boolView.interfaceName) { - case "closablesensor": - return device.states.getState(boolView.stateType.id).value === true ? "green" : "red" - default: - device.states.getState(boolView.stateType.id).value === true ? app.interfaceToColor(boolView.interfaceName) : keyColor - } - } - } - } - Item { - Layout.fillHeight: true - Layout.fillWidth: true - } - RowLayout { - Layout.fillWidth: false - Layout.alignment: Qt.AlignHCenter - property StateType lastSeenStateType: root.deviceClass.stateTypes.findByName("lastSeenTime") - property State lastSeenState: lastSeenStateType ? root.device.states.getState(lastSeenStateType.id) : null - visible: lastSeenStateType !== null - Label { - text: qsTr("Last seen:") - font.bold: true - } - Label { - text: parent.lastSeenState ? Qt.formatDateTime(new Date(parent.lastSeenState.value * 1000)) : "" - } - } - RowLayout { - Layout.fillWidth: false - Layout.alignment: Qt.AlignHCenter - property StateType sunriseStateType: root.deviceClass.stateTypes.findByName("sunriseTime") - property State sunriseState: sunriseStateType ? root.device.states.getState(sunriseStateType.id) : null - visible: sunriseStateType !== null - Label { - text: qsTr("Sunrise:") - font.bold: true - } - Label { - text: parent.sunriseStateType ? Qt.formatDateTime(new Date(parent.sunriseState.value * 1000)) : "" - } - } - RowLayout { - Layout.fillWidth: false - Layout.alignment: Qt.AlignHCenter - property StateType sunsetStateType: root.deviceClass.stateTypes.findByName("sunsetTime") - property State sunsetState: sunsetStateType ? root.device.states.getState(sunsetStateType.id) : null - visible: sunsetStateType !== null - Label { - text: qsTr("Sunset:") - font.bold: true - } - Label { - text: parent.sunsetStateType ? Qt.formatDateTime(new Date(parent.sunsetState.value * 1000)) : "" - } - } - Item { - Layout.fillHeight: true - Layout.fillWidth: true - } - } - } -} diff --git a/nymea-app/ui/devicepages/SensorDevicePagePre110.qml b/nymea-app/ui/devicepages/SensorDevicePagePre110.qml deleted file mode 100644 index 1ed6a8f5..00000000 --- a/nymea-app/ui/devicepages/SensorDevicePagePre110.qml +++ /dev/null @@ -1,61 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* Copyright 2013 - 2020, nymea GmbH -* Contact: contact@nymea.io -* -* This file is part of nymea. -* This project including source code and documentation is protected by -* copyright law, and remains the property of nymea GmbH. All rights, including -* reproduction, publication, editing and translation, are reserved. The use of -* this project is subject to the terms of a license agreement to be concluded -* with nymea GmbH in accordance with the terms of use of nymea GmbH, available -* under https://nymea.io/license -* -* GNU General Public License Usage -* Alternatively, this project may be redistributed and/or modified under the -* terms of the GNU General Public License as published by the Free Software -* Foundation, GNU version 3. This project is distributed in the hope that it -* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -* Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this project. If not, see . -* -* For any further details and any questions please contact us under -* contact@nymea.io or see our FAQ/Licensing Information on -* https://nymea.io/license/faq -* -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -import QtQuick 2.5 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.1 -import Nymea 1.0 -import "../components" -import "../customviews" - -ListView { - anchors { fill: parent } - - property var device - property var deviceClass - - model: ListModel { - Component.onCompleted: { - var supportedInterfaces = ["temperaturesensor", "humiditysensor", "pressuresensor", "moisturesensor", "lightsensor", "conductivitysensor", "noisesensor", "co2sensor"] - for (var i = 0; i < supportedInterfaces.length; i++) { - print("checking", root.deviceClass.name, root.deviceClass.interfaces) - if (root.deviceClass.interfaces.indexOf(supportedInterfaces[i]) >= 0) { - append({name: supportedInterfaces[i]}); - } - } - } - } - delegate: SensorView { - width: parent.width - interfaceName: modelData - device: root.device - deviceClass: root.deviceClass - } -} diff --git a/nymea-app/ui/devicepages/WeatherDevicePage.qml b/nymea-app/ui/devicepages/WeatherDevicePage.qml index a2eb416b..5120933f 100644 --- a/nymea-app/ui/devicepages/WeatherDevicePage.qml +++ b/nymea-app/ui/devicepages/WeatherDevicePage.qml @@ -38,16 +38,55 @@ import "../customviews" DevicePageBase { id: root - Loader { + Flickable { anchors.fill: parent - Component.onCompleted: { - var src - if (engine.jsonRpcClient.ensureServerVersion("1.10")) { - src = "WeatherDevicePagePost110.qml" - } else { - src = "WeatherDevicePagePre110.qml" + clip: true + contentHeight: contentColumn.implicitHeight + + ColumnLayout { + id: contentColumn + width: parent.width + + WeatherView { + Layout.fillWidth: true + device: root.device + deviceClass: root.deviceClass + } + + GridLayout { + id: content + Layout.fillWidth: true + columns: Math.min(width / 300, 4) + + GenericTypeGraph { + Layout.fillWidth: true + device: root.device + stateType: root.deviceClass.stateTypes.findByName("temperature") + iconSource: app.interfaceToIcon("temperaturesensor") + color: app.interfaceToColor("temperaturesensor") + } + GenericTypeGraph { + Layout.fillWidth: true + device: root.device + stateType: root.deviceClass.stateTypes.findByName("humidity") + iconSource: app.interfaceToIcon("humiditysensor") + color: app.interfaceToColor("humiditysensor") + } + GenericTypeGraph { + Layout.fillWidth: true + device: root.device + stateType: root.deviceClass.stateTypes.findByName("pressure") + iconSource: app.interfaceToIcon("pressuresensor") + color: app.interfaceToColor("pressuresensor") + } + GenericTypeGraph { + Layout.fillWidth: true + device: root.device + stateType: root.deviceClass.stateTypes.findByName("windSpeed") + iconSource: app.interfaceToIcon("windspeedsensor") + color: app.interfaceToColor("windspeedsensor") + } } - setSource(Qt.resolvedUrl(src), {device: root.device, deviceClass: root.deviceClass}) } } } diff --git a/nymea-app/ui/devicepages/WeatherDevicePagePost110.qml b/nymea-app/ui/devicepages/WeatherDevicePagePost110.qml deleted file mode 100644 index a51f9238..00000000 --- a/nymea-app/ui/devicepages/WeatherDevicePagePost110.qml +++ /dev/null @@ -1,92 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* Copyright 2013 - 2020, nymea GmbH -* Contact: contact@nymea.io -* -* This file is part of nymea. -* This project including source code and documentation is protected by -* copyright law, and remains the property of nymea GmbH. All rights, including -* reproduction, publication, editing and translation, are reserved. The use of -* this project is subject to the terms of a license agreement to be concluded -* with nymea GmbH in accordance with the terms of use of nymea GmbH, available -* under https://nymea.io/license -* -* GNU General Public License Usage -* Alternatively, this project may be redistributed and/or modified under the -* terms of the GNU General Public License as published by the Free Software -* Foundation, GNU version 3. This project is distributed in the hope that it -* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -* Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this project. If not, see . -* -* For any further details and any questions please contact us under -* contact@nymea.io or see our FAQ/Licensing Information on -* https://nymea.io/license/faq -* -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -import QtQuick 2.5 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.1 -import Nymea 1.0 -import "../components" -import "../customviews" - -Flickable { - anchors.fill: parent - clip: true - contentHeight: contentColumn.implicitHeight - - property var device - property var deviceClass - - ColumnLayout { - id: contentColumn - width: parent.width - - WeatherView { - Layout.fillWidth: true - device: root.device - deviceClass: root.deviceClass - } - - GridLayout { - id: content - Layout.fillWidth: true - columns: Math.min(width / 300, 4) - - GenericTypeGraph { - Layout.fillWidth: true - device: root.device - stateType: root.deviceClass.stateTypes.findByName("temperature") - iconSource: app.interfaceToIcon("temperaturesensor") - color: app.interfaceToColor("temperaturesensor") - } - GenericTypeGraph { - Layout.fillWidth: true - device: root.device - stateType: root.deviceClass.stateTypes.findByName("humidity") - iconSource: app.interfaceToIcon("humiditysensor") - color: app.interfaceToColor("humiditysensor") - } - GenericTypeGraph { - Layout.fillWidth: true - device: root.device - stateType: root.deviceClass.stateTypes.findByName("pressure") - iconSource: app.interfaceToIcon("pressuresensor") - color: app.interfaceToColor("pressuresensor") - } - GenericTypeGraph { - Layout.fillWidth: true - device: root.device - stateType: root.deviceClass.stateTypes.findByName("windSpeed") - iconSource: app.interfaceToIcon("windspeedsensor") - color: app.interfaceToColor("windspeedsensor") - } - } - } - -} diff --git a/nymea-app/ui/devicepages/WeatherDevicePagePre110.qml b/nymea-app/ui/devicepages/WeatherDevicePagePre110.qml deleted file mode 100644 index 6039cbb0..00000000 --- a/nymea-app/ui/devicepages/WeatherDevicePagePre110.qml +++ /dev/null @@ -1,74 +0,0 @@ -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* -* Copyright 2013 - 2020, nymea GmbH -* Contact: contact@nymea.io -* -* This file is part of nymea. -* This project including source code and documentation is protected by -* copyright law, and remains the property of nymea GmbH. All rights, including -* reproduction, publication, editing and translation, are reserved. The use of -* this project is subject to the terms of a license agreement to be concluded -* with nymea GmbH in accordance with the terms of use of nymea GmbH, available -* under https://nymea.io/license -* -* GNU General Public License Usage -* Alternatively, this project may be redistributed and/or modified under the -* terms of the GNU General Public License as published by the Free Software -* Foundation, GNU version 3. This project is distributed in the hope that it -* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty -* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -* Public License for more details. -* -* You should have received a copy of the GNU General Public License along with -* this project. If not, see . -* -* For any further details and any questions please contact us under -* contact@nymea.io or see our FAQ/Licensing Information on -* https://nymea.io/license/faq -* -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - -import QtQuick 2.5 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.1 -import Nymea 1.0 -import "../components" -import "../customviews" - - -Flickable { - anchors.fill: parent - clip: true - contentHeight: content.implicitHeight - - property var device - property var deviceClass - - ColumnLayout { - id: content - width: parent.width - WeatherView { - Layout.fillWidth: true - device: root.device - deviceClass: root.deviceClass - } - SensorView { - Layout.fillWidth: true - device: root.device - deviceClass: root.deviceClass - interfaceName: "temperaturesensor" - } - SensorView { - Layout.fillWidth: true - device: root.device - deviceClass: root.deviceClass - interfaceName: "humiditysensor" - } - SensorView { - Layout.fillWidth: true - device: root.device - deviceClass: root.deviceClass - interfaceName: "pressuresensor" - } - } -} diff --git a/nymea-app/ui/images/browser/MediaBrowserIconLibrary.svg b/nymea-app/ui/images/browser/MediaBrowserIconLibrary.svg index 1762fbfe..3b4dfccb 120000 --- a/nymea-app/ui/images/browser/MediaBrowserIconLibrary.svg +++ b/nymea-app/ui/images/browser/MediaBrowserIconLibrary.svg @@ -1 +1 @@ -../mediaplayer-app-symbolic.svg \ No newline at end of file +../media.svg \ No newline at end of file diff --git a/nymea-app/ui/images/mediaplayer-app-symbolic.svg b/nymea-app/ui/images/media.svg similarity index 100% rename from nymea-app/ui/images/mediaplayer-app-symbolic.svg rename to nymea-app/ui/images/media.svg diff --git a/nymea-app/ui/mainviews/EnergyView.qml b/nymea-app/ui/mainviews/EnergyView.qml index e9cf0415..0914a321 100644 --- a/nymea-app/ui/mainviews/EnergyView.qml +++ b/nymea-app/ui/mainviews/EnergyView.qml @@ -70,6 +70,7 @@ MainViewBase { width: parent.width visible: consumers.count > 0 columns: Math.floor(root.width / 300) + rowSpacing: 0 SmartMeterChart { Layout.fillWidth: true @@ -78,6 +79,285 @@ MainViewBase { title: qsTr("Total consumed energy") visible: consumers.count > 0 } + + ChartView { + id: chartView + Layout.fillWidth: true + Layout.preferredHeight: width * .75 + legend.alignment: Qt.AlignBottom + legend.font.pixelSize: app.smallFont + legend.visible: false + backgroundColor: app.backgroundColor + + property var startTime: xAxis.min + property var endTime: xAxis.max + + property int sampleRate: XYSeriesAdapter.SampleRateMinute + + BusyIndicator { + anchors.centerIn: parent + visible: running + running: { + for (var i = 0; i < consumersRepeater.count; i++) { + if (consumersRepeater.itemAt(i).model.busy) { + return true; + } + } + return false; + } + } + + Repeater { + id: consumersRepeater + model: consumers + + delegate: Item { + id: consumer + property Thing thing: consumers.get(index) + + property var model: LogsModel { + id: logsModel + engine: _engine + thingId: consumer.thing.id + typeIds: [consumer.thing.thingClass.stateTypes.findByName("currentPower").id] + viewStartTime: xAxis.min + live: true + } + property XYSeriesAdapter adapter: XYSeriesAdapter { + id: seriesAdapter + logsModel: logsModel + sampleRate: chartView.sampleRate + xySeries: upperSeries + } + property XYSeries lineSeries: LineSeries { + id: upperSeries + onPointAdded: { + var newPoint = upperSeries.at(index) + + if (newPoint.x > lowerSeries.at(0).x) { + lowerSeries.replace(0, newPoint.x, 0) + } + if (newPoint.x < lowerSeries.at(1).x) { + lowerSeries.replace(1, newPoint.x, 0) + } + } + } + LineSeries { + id: lowerSeries + XYPoint { x: xAxis.max.getTime(); y: 0 } + XYPoint { x: xAxis.min.getTime(); y: 0 } + } + + Component.onCompleted: { + print("creating series") + var areaSeries = chartView.createSeries(ChartView.SeriesTypeArea, consumer.thing.name, xAxis, yAxis) + areaSeries.upperSeries = upperSeries; + if (index > 0) { + areaSeries.lowerSeries = consumersRepeater.itemAt(index - 1).lineSeries + seriesAdapter.baseSeries = consumersRepeater.itemAt(index - 1).lineSeries + } else { + areaSeries.lowerSeries = lowerSeries; + } + + var color = app.accentColor + for (var j = 0; j < index; j+=2) { + if (index % 2 == 0) { + color = Qt.lighter(color, 1.2); + } else { + color = Qt.darker(color, 1.2) + } + } + areaSeries.color = color; + areaSeries.borderColor = color; + areaSeries.borderWidth = 0; + + seriesAdapter.xySeries = series; + } + } + } + + ValueAxis { + id: yAxis + readonly property XYSeriesAdapter adapter: consumersRepeater.itemAt(consumersRepeater.count - 1).adapter; + max: Math.ceil(adapter.maxValue + Math.abs(adapter.maxValue * .05)) + min: Math.floor(adapter.minValue - Math.abs(adapter.minValue * .05)) + onMinChanged: applyNiceNumbers(); + onMaxChanged: applyNiceNumbers(); + labelsFont.pixelSize: app.smallFont + labelFormat: { + return "%d"; + // switch (root.stateType.type.toLowerCase()) { + // case "bool": + // return "x"; + // default: + // return "%d"; + // } + } + labelsColor: app.foregroundColor + // tickCount: root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40 + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .2) + gridLineColor: color + } + + DateTimeAxis { + id: xAxis + gridVisible: false + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .2) + tickCount: chartView.width / 70 + labelsFont.pixelSize: app.smallFont + labelsColor: app.foregroundColor + property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000 + + function getTimeSpanString() { + var td = timeDiff + if (td < 60) { + return qsTr("%1 seconds").arg(Math.round(td)); + } + td = td / 60 + if (td < 60) { + return qsTr("%1 minutes").arg(Math.round(td)); + } + td = td / 60 + if (td < 48) { + return qsTr("%1 hours").arg(Math.round(td)); + } + td = td / 24; + if (td < 14) { + return qsTr("%1 days").arg(Math.round(td)); + } + td = td / 7 + if (td < 9) { + return qsTr("%1 weeks").arg(Math.round(td)); + } + td = td * 7 / 30 + if (td < 24) { + return qsTr("%1 months").arg(Math.round(td)); + } + td = td * 30 / 356 + return qsTr("%1 years").arg(Math.round(td)) + } + + titleText: { + if (xAxis.min.getYear() === xAxis.max.getYear() + && xAxis.min.getMonth() === xAxis.max.getMonth() + && xAxis.min.getDate() === xAxis.max.getDate()) { + return Qt.formatDate(xAxis.min) + " (" + getTimeSpanString() + ")" + } + return Qt.formatDate(xAxis.min) + " - " + Qt.formatDate(xAxis.max) + " (" + getTimeSpanString() + ")" + } + titleBrush: app.foregroundColor + format: { + if (timeDiff < 60) { // one minute + return "mm:ss" + } + if (timeDiff < 60 * 60) { // one hour + return "hh:mm" + } + if (timeDiff < 60 * 60 * 24 * 2) { // two day + return "hh:mm" + } + if (timeDiff < 60 * 60 * 24 * 7) { // one week + return "ddd hh:mm" + } + if (timeDiff < 60 * 60 * 24 * 7 * 30) { // one month + return "dd.MM." + } + return "MMM yy" + } + + min: { + var date = new Date(); + date.setTime(date.getTime() - (1000 * 60 * 60 * 24) + 2000); + return date; + } + max: { + var date = new Date(); + date.setTime(date.getTime() + 2000) + return date; + } + } + + MouseArea { + id: scrollMouseArea + x: chartView.plotArea.x + y: chartView.plotArea.y + width: chartView.plotArea.width + height: chartView.plotArea.height + property int lastX: 0 + property int startX: 0 + preventStealing: false + + property bool autoScroll: true + + function scrollRightLimited(dx) { + chartView.animationOptions = ChartView.NoAnimation + var now = new Date() + // if we're already at the limit, don't even start scrolling + if (dx < 0 || xAxis.max < now) { + chartView.scrollRight(dx) + } + // figure out if we scrolled too far + var overshoot = xAxis.max.getTime() - now.getTime() + // print("overshoot is:", overshoot, "oldMax", xAxis.max, "newMax", now, "oldMin", xAxis.min, "newMin", new Date(xAxis.min.getTime() - overshoot)) + if (overshoot > 0) { + var range = xAxis.max - xAxis.min + xAxis.max = now + xAxis.min = new Date(xAxis.max.getTime() - range) + } + // If the user scrolled closer than 5 pixels to the right edge, enable autoscroll + autoScroll = overshoot > -5; + + chartView.animationOptions = ChartView.SeriesAnimations + } + + function zoomInLimited(dy) { + chartView.animationOptions = ChartView.NoAnimation + var oldMax = xAxis.max; + chartView.scrollRight(dy); + xAxis.min = new Date(xAxis.min.getTime() - xAxis.timeDiff * 1000 * 2) + chartView.animationOptions = ChartView.SeriesAnimations + } + + onPressed: { + lastX = mouse.x + startX = mouse.x + preventStealing = true + } + onClicked: { +// var pt = chartView.mapToValue(Qt.point(mouse.x + chartView.plotArea.x, mouse.y + chartView.plotArea.y), mainSeries) +// mainSeries.markClosestPoint(pt) + } + + onWheel: { + scrollRightLimited(-wheel.pixelDelta.x) + // zoomInLimited(wheel.pixelDelta.y) + } + + onPositionChanged: { + if (lastX !== mouse.x) { + scrollRightLimited(lastX - mouseX) + lastX = mouse.x + } + + if (Math.abs(startX - mouse.x) > 10) { + preventStealing = true; + } + } + + onReleased: preventStealing = false; + + + Timer { + running: scrollMouseArea.autoScroll + interval: 1000 + repeat: true + onTriggered: { + scrollMouseArea.scrollRightLimited(10) + } + } + } + } + SmartMeterChart { Layout.fillWidth: true Layout.preferredHeight: width * .7 diff --git a/nymea-app/ui/mainviews/GroupsView.qml b/nymea-app/ui/mainviews/GroupsView.qml index 408d6e49..ecb203e0 100644 --- a/nymea-app/ui/mainviews/GroupsView.qml +++ b/nymea-app/ui/mainviews/GroupsView.qml @@ -484,7 +484,7 @@ MainViewBase { } // involve count in the statement to make the binding re-evaluate when the group is changed - device: mediaControllers.count > 0 ? mediaControllers.get(0) : null + thing: mediaControllers.count > 0 ? mediaControllers.get(0) : null } } diff --git a/nymea-app/ui/mainviews/MediaView.qml b/nymea-app/ui/mainviews/MediaView.qml new file mode 100644 index 00000000..0f240272 --- /dev/null +++ b/nymea-app/ui/mainviews/MediaView.qml @@ -0,0 +1,186 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +import QtQuick 2.8 +import QtQuick.Controls 2.1 +import QtQuick.Controls.Material 2.1 +import QtQuick.Layouts 1.2 +import QtCharts 2.2 +import Nymea 1.0 +import "../components" +import "../delegates" + +MainViewBase { + id: root + + ThingsProxy { + id: mediaDevices + engine: _engine + shownInterfaces: ["mediaplayer"] + } + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + visible: !engine.thingManager.fetchingData && mediaDevices.count == 0 + title: qsTr("There are no media players set up.") + text: qsTr("Connect your media players in order to control them from here.") + imageSource: "../images/media.svg" + buttonText: qsTr("Add things") + } + + SwipeView { + id: swipeView + anchors.fill: parent + currentIndex: pageIndicator.currentIndex + + Repeater { + model: mediaDevices + delegate: Item { + id: playerDelegate + height: swipeView.height + width: swipeView.width + property Thing thing: mediaDevices.get(index) + property State titleState: thing.stateByName("title") + property State artistState: thing.stateByName("artist") + property State collectionState: thing.stateByName("collection") + + GridLayout { + anchors.fill: parent + anchors.margins: app.margins + columns: 1 + rowSpacing: app.margins + + MediaArtworkImage { + Layout.fillWidth: true + Layout.fillHeight: true + thing: playerDelegate.thing + } + ColumnLayout { + spacing: app.margins + Label { + text: playerDelegate.titleState.value + Layout.fillWidth: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + font.pixelSize: app.largeFont + } + Label { + text: playerDelegate.artistState.value + Layout.fillWidth: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } + Label { + text: playerDelegate.collectionState.value + Layout.fillWidth: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } + } + + MediaControls { + Layout.fillWidth: true + thing: playerDelegate.thing + } + + RowLayout { + + Item { + Layout.preferredHeight: app.iconSize + Layout.fillWidth: true + visible: playerDelegate.thing.thingClass.browsable + + HeaderButton { + anchors.centerIn: parent + imageSource: "../images/navigationpad.svg" + onClicked: { + pageStack.push(navigationPadPage) + } + } + Component { + id: navigationPadPage + Page { + header: NymeaHeader { text: playerDelegate.thing.name; onBackPressed: pageStack.pop() } + ColumnLayout { + anchors.fill: parent + anchors.margins: app.margins + spacing: app.margins + + NavigationPad { Layout.fillWidth: true; Layout.fillHeight: true; device: playerDelegate.thing } + MediaControls { Layout.fillWidth: true; thing: playerDelegate.thing } + ShuffleRepeatVolumeControl { Layout.fillWidth: true; Layout.fillHeight: false; Layout.preferredHeight: app.iconSize; thing: playerDelegate.thing } + } + } + } + } + + ShuffleRepeatVolumeControl { + Layout.fillWidth: true + Layout.fillHeight: false + Layout.preferredHeight: app.iconSize + thing: playerDelegate.thing + } + + Item { + Layout.preferredHeight: app.iconSize + Layout.fillWidth: true + visible: playerDelegate.thing.thingClass.interfaces.indexOf("navigationpad") >= 0 + + HeaderButton { + anchors.centerIn: parent + imageSource: "../images/folder-symbolic.svg" + onClicked: { + pageStack.push(browserPage) + } + } + Component { + id: browserPage + Page { + header: NymeaHeader { text: playerDelegate.thing.name; onBackPressed: pageStack.pop() } + MediaBrowser { anchors.fill: parent; thing: playerDelegate.thing } + } + } + } + } + } + } + } + } + PageIndicator { + id: pageIndicator + count: swipeView.count + currentIndex: swipeView.currentIndex + interactive: true + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + } + +} diff --git a/nymea-app/ui/system/LogViewerPage.qml b/nymea-app/ui/system/LogViewerPage.qml index 91d9764b..520cc592 100644 --- a/nymea-app/ui/system/LogViewerPage.qml +++ b/nymea-app/ui/system/LogViewerPage.qml @@ -56,23 +56,6 @@ Page { LogsModel { id: logsModel engine: _engine - startTime: { - var date = new Date(); - date.setHours(new Date().getHours() - 2); - return date; - } - endTime: new Date() - live: true - onCountChanged: { - if (root.autoScroll) { - listView.positionViewAtEnd() - } - } - } - - LogsModelNg { - id: logsModelNg - engine: _engine live: true } @@ -83,13 +66,11 @@ Page { ListView { id: listView - model: engine.jsonRpcClient.ensureServerVersion("1.10") ? logsModelNg : logsModel + model: logsModel anchors.fill: parent clip: true headerPositioning: ListView.OverlayHeader - Component.onCompleted: model.update() - onDraggingChanged: { if (dragging) { root.autoScroll = false; @@ -103,14 +84,6 @@ Page { visible: listView.model.busy } - onContentYChanged: { - if (!engine.jsonRpcClient.ensureServerVersion("1.10")) { - if (!logsModel.busy && contentY - originY < 5 * height) { - logsModel.fetchEarlier(1) - } - } - } - delegate: ItemDelegate { id: delegate width: parent.width