diff --git a/libnymea-app/devicemanager.cpp b/libnymea-app/devicemanager.cpp index 8d9ba643..c95b568b 100644 --- a/libnymea-app/devicemanager.cpp +++ b/libnymea-app/devicemanager.cpp @@ -76,7 +76,7 @@ void DeviceManager::init() qWarning() << "received an event from a device we don't know..." << deviceId << event; return; } - qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson()); +// qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson()); dev->eventTriggered(eventTypeId.toString(), event.value("params").toMap()); emit eventTriggered(deviceId.toString(), eventTypeId.toString(), event.value("params").toMap()); }); @@ -231,7 +231,7 @@ void DeviceManager::notificationReceived(const QVariantMap &data) qWarning() << "received an event from a device we don't know..." << deviceId << qUtf8Printable(QJsonDocument::fromVariant(data).toJson()); return; } - qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson()); +// qDebug() << "Event received" << deviceId.toString() << eventTypeId.toString() << qUtf8Printable(QJsonDocument::fromVariant(event).toJson()); dev->eventTriggered(eventTypeId.toString(), event.value("params").toMap()); } else if (notification == "Integrations.IOConnectionAdded") { QVariantMap connectionMap = data.value("params").toMap().value("ioConnection").toMap(); @@ -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; @@ -750,7 +750,7 @@ void DeviceManager::executeBrowserItemActionResponse(const QVariantMap ¶ms) void DeviceManager::getIOConnectionsResponse(const QVariantMap ¶ms) { - qDebug() << "Get IO connections response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); +// qDebug() << "Get IO connections response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson()); foreach (const QVariant &connectionVariant, params.value("params").toMap().value("ioConnections").toList()) { QVariantMap connectionMap = connectionVariant.toMap(); 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 1b6b0117..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" @@ -92,6 +93,7 @@ #include "ruletemplates/ruleactionparamtemplate.h" #include "connection/awsclient.h" #include "models/devicemodel.h" +#include "models/sortfilterproxymodel.h" #include "system/systemcontroller.h" #include "types/package.h" #include "types/packages.h" @@ -177,8 +179,11 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "VendorsProxy"); qmlRegisterUncreatableType(uri, 1, 0, "Device", "Can't create this in QML. Get it from the Devices."); + qmlRegisterUncreatableType(uri, 1, 0, "Thing", "Can't create this in QML. Get it from the Things."); qmlRegisterUncreatableType(uri, 1, 0, "Devices", "Can't create this in QML. Get it from the DeviceManager."); + qmlRegisterUncreatableType(uri, 1, 0, "Things", "Can't create this in QML. Get it from the ThingManager."); qmlRegisterType(uri, 1, 0, "DevicesProxy"); + qmlRegisterType(uri, 1, 0, "ThingsProxy"); qmlRegisterType(uri, 1, 0, "InterfacesModel"); qmlRegisterType(uri, 1, 0, "InterfacesSortModel"); @@ -242,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"); @@ -311,6 +317,8 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "IOConnection", "Get it from IOConnections"); qmlRegisterType(uri, 1, 0, "IOInputConnectionWatcher"); qmlRegisterType(uri, 1, 0, "IOOutputConnectionWatcher"); + + qmlRegisterType(uri, 1, 0, "SortFilterProxyModel"); } #endif // LIBNYMEAAPPCORE_H diff --git a/libnymea-app/libnymea-app.pro b/libnymea-app/libnymea-app.pro index 8e3e1925..3f7bfff4 100644 --- a/libnymea-app/libnymea-app.pro +++ b/libnymea-app/libnymea-app.pro @@ -27,6 +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 \ @@ -127,7 +130,6 @@ SOURCES += \ rulemanager.cpp \ models/rulesfiltermodel.cpp \ models/logsmodel.cpp \ - models/valuelogsproxymodel.cpp \ logmanager.cpp \ wifisetup/bluetoothdevice.cpp \ wifisetup/bluetoothdeviceinfo.cpp \ @@ -162,6 +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 \ @@ -263,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..d947d4d8 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,177 @@ 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_busyInternal = false; + 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(); + m_busyInternal = false; + + if (m_viewStartTime.isValid() && m_list.count() > 0 && m_list.last()->timestamp() > m_viewStartTime && m_canFetchMore) { + fetchMore(); + } else { + m_busy = false; + emit busyChanged(); + } +} + +void LogsModel::fetchMore(const QModelIndex &parent) +{ + Q_UNUSED(parent) + + if (!m_engine) { + qWarning() << "Cannot update. Engine not set"; + return; + } + if (m_busyInternal) { + 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_busyInternal = true; + if (!m_busy) { + 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() +{ + 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..11dfb1aa 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,45 @@ 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_live = false; + int m_blockSize = 100; + + bool m_busyInternal = false; + + 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/sortfilterproxymodel.cpp b/libnymea-app/models/sortfilterproxymodel.cpp new file mode 100644 index 00000000..d2e3f9f0 --- /dev/null +++ b/libnymea-app/models/sortfilterproxymodel.cpp @@ -0,0 +1,62 @@ +#include "sortfilterproxymodel.h" + +#include + +SortFilterProxyModel::SortFilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + connect(this, &QSortFilterProxyModel::sourceModelChanged, this, [=](){ + connect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &SortFilterProxyModel::countChanged); + connect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, &SortFilterProxyModel::countChanged); + connect(sourceModel(), &QAbstractItemModel::modelReset, this, &SortFilterProxyModel::countChanged); + emit countChanged(); + }); +} + +QString SortFilterProxyModel::filterRoleName() const +{ + return m_filterRoleName; +} + +void SortFilterProxyModel::setFilterRoleName(const QString &filterRoleName) +{ + if (m_filterRoleName != filterRoleName) { + m_filterRoleName = filterRoleName; + emit filterRoleNameChanged(); + invalidateFilter(); + emit countChanged(); + } +} + +QStringList SortFilterProxyModel::filterList() const +{ + return m_filterList; +} + +void SortFilterProxyModel::setFilterList(const QStringList &filterList) +{ + if (m_filterList != filterList) { + m_filterList = filterList; + emit filterListChanged(); + invalidateFilter(); + emit countChanged(); + } +} + +QVariant SortFilterProxyModel::data(int row, const QString &role) const +{ + int roleId = roleNames().key(role.toUtf8()); + return QSortFilterProxyModel::data(index(row, 0), roleId); +} + +bool SortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (!m_filterList.isEmpty() && !m_filterRoleName.isEmpty()) { + QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + int filterRole = sourceModel()->roleNames().key(m_filterRoleName.toUtf8()); + QVariant data = sourceModel()->data(idx, filterRole); + if (!m_filterList.contains(data.toString())) { + return false; + } + } + return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); +} diff --git a/libnymea-app/models/sortfilterproxymodel.h b/libnymea-app/models/sortfilterproxymodel.h new file mode 100644 index 00000000..02dccaba --- /dev/null +++ b/libnymea-app/models/sortfilterproxymodel.h @@ -0,0 +1,37 @@ +#ifndef SORTFILTERPROXYMODEL_H +#define SORTFILTERPROXYMODEL_H + +#include + +class SortFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QString filterRoleName READ filterRoleName WRITE setFilterRoleName NOTIFY filterRoleNameChanged) + Q_PROPERTY(QStringList filterList READ filterList WRITE setFilterList NOTIFY filterListChanged) + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + +public: + explicit SortFilterProxyModel(QObject *parent = nullptr); + + QString filterRoleName() const; + void setFilterRoleName(const QString &filterRoleName); + + QStringList filterList() const; + void setFilterList(const QStringList &filterList); + + Q_INVOKABLE QVariant data(int row, const QString &role) const; + +signals: + void filterRoleNameChanged(); + void filterListChanged(); + void countChanged(); + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + +private: + QString m_filterRoleName; + QStringList m_filterList; +}; + +#endif // SORTFILTERPROXYMODEL_H 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..325df9d5 --- /dev/null +++ b/libnymea-app/models/xyseriesadapter.cpp @@ -0,0 +1,192 @@ +#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::ensureSamples(const QDateTime &from, const QDateTime &to) +{ + if (!m_series) { + return; + } + + if (m_samples.isEmpty()) { + Sample *sample = new Sample(); + sample->timestamp = from.addSecs(m_sampleRate); + m_newestSample = sample->timestamp; + m_oldestSample = m_newestSample; + m_samples.append(sample); + m_series->insert(0, QPointF(sample->timestamp.toMSecsSinceEpoch(), 0)); + } + + while (to > m_newestSample) { + Sample *sample = new Sample(); + sample->timestamp = m_newestSample.addSecs(m_sampleRate); + m_newestSample = sample->timestamp; + m_samples.prepend(sample); + m_series->insert(0, QPointF(sample->timestamp.toMSecsSinceEpoch(), 0)); + } + + while (from < m_oldestSample.addSecs(m_sampleRate)) { + Sample *sample = new Sample(); + sample->timestamp = m_oldestSample.addSecs(-m_sampleRate); + m_oldestSample = sample->timestamp; + m_samples.append(sample); + m_series->append(sample->timestamp.toMSecsSinceEpoch(), 0); + } +} + +void XYSeriesAdapter::logEntryAdded(LogEntry *entry) +{ + if (!m_series) { + return; + } + + ensureSamples(entry->timestamp(), entry->timestamp()); + + 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); + for (int i = idx; i > 0; i--) { + Sample *nextSample = m_samples.at(i); + if (!nextSample->last) { + nextSample->last = entry; + m_series->replace(i, nextSample->timestamp.toMSecsSinceEpoch(), calculateSampleValue(i)); + } else { + break; + } + } + + 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); + if (previousSample->last) { + 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..feb114aa --- /dev/null +++ b/libnymea-app/models/xyseriesadapter.h @@ -0,0 +1,89 @@ +#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; + + Q_INVOKABLE void ensureSamples(const QDateTime &from, const QDateTime &to); + +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 15fd48a9..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 @@ -129,7 +129,7 @@ ui/images/send.svg ui/images/sensors.svg ui/images/settings.svg - ui/images/share.svg + ui/images/things.svg ui/images/slideshow.svg ui/images/closable-move.svg ui/images/starred.svg @@ -234,5 +234,6 @@ ui/images/garage/garage-100.svg ui/images/navigationpad.svg ui/images/qrcode.svg + ui/images/energy.svg diff --git a/nymea-app/mainmenumodel.cpp b/nymea-app/mainmenumodel.cpp new file mode 100644 index 00000000..d18cc578 --- /dev/null +++ b/nymea-app/mainmenumodel.cpp @@ -0,0 +1,12 @@ +#include "mainmenumodel.h" + +MainMenuModel::MainMenuModel(QObject *parent) : QAbstractListModel(parent) +{ + +} + +int MainMenuModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_list.count(); +} diff --git a/nymea-app/mainmenumodel.h b/nymea-app/mainmenumodel.h new file mode 100644 index 00000000..f90cd3de --- /dev/null +++ b/nymea-app/mainmenumodel.h @@ -0,0 +1,24 @@ +#ifndef MAINMENUMODEL_H +#define MAINMENUMODEL_H + +#include + +class MainMenuItem: public QObject +{ + Q_OBJECT + +}; + +class MainMenuModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit MainMenuModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + +private: + QList m_list; +}; + +#endif // MAINMENUMODEL_H diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index a699195f..4be2cba9 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -13,6 +13,7 @@ linux:!android:!nozeroconf:LIBS += -lavahi-client -lavahi-common PRE_TARGETDEPS += ../libnymea-app HEADERS += \ + mainmenumodel.h \ platformintegration/generic/raspberrypihelper.h \ stylecontroller.h \ pushnotifications.h \ @@ -22,6 +23,7 @@ HEADERS += \ ruletemplates/messages.h SOURCES += main.cpp \ + mainmenumodel.cpp \ platformintegration/generic/raspberrypihelper.cpp \ stylecontroller.cpp \ pushnotifications.cpp \ diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 0a5a92aa..f1d2d196 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -10,8 +10,7 @@ ui/RootItem.qml ui/mainviews/ScenesView.qml ui/mainviews/FavoritesView.qml - ui/mainviews/DevicesPageDelegate.qml - ui/mainviews/DevicesPage.qml + ui/mainviews/ThingsView.qml ui/connection/ConnectPage.qml ui/connection/ManualConnectPage.qml ui/connection/ConnectingPage.qml @@ -51,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 @@ -108,6 +102,7 @@ ui/delegates/ParamDelegate.qml ui/delegates/ActionDelegate.qml ui/delegates/ThingDelegate.qml + ui/delegates/InterfaceTile.qml ui/system/LogViewerPage.qml ui/system/PluginsPage.qml ui/system/PluginParamsPage.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 @@ -165,7 +159,6 @@ ui/thingconfiguration/EditThingsPage.qml ui/thingconfiguration/ConfigureThingPage.qml ui/connection/CertificateDialog.qml - ui/experiences/garagegates/Main.qml ui/experiences/heating/Main.qml ui/fonts/Oswald-Bold.ttf ui/fonts/Oswald-ExtraLight.ttf @@ -218,5 +211,12 @@ ui/thingconfiguration/ThingClassDetailsPage.qml ui/components/ClosablesControlLarge.qml ui/devicepages/BarcodeScannerThingPage.qml + ui/mainviews/GaragesView.qml + 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/stylecontroller.cpp b/nymea-app/stylecontroller.cpp index 9a135b8d..1074226b 100644 --- a/nymea-app/stylecontroller.cpp +++ b/nymea-app/stylecontroller.cpp @@ -75,28 +75,6 @@ QStringList StyleController::allStyles() const return dir.entryList(QDir::Dirs); } -QString StyleController::currentExperience() const -{ - QSettings settings; - return settings.value("experience", "Default").toString(); -} - -void StyleController::setCurrentExperience(const QString ¤tExperience) -{ - QSettings settings; - if (settings.value("experience").toString() != currentExperience) { - settings.setValue("experience", currentExperience); - emit currentExperienceChanged(); - } -} - -QStringList StyleController::allExperiences() const -{ - QDir dir(":/ui/experiences"); - qDebug() << "experiences:" << dir.entryList(); - return QStringList() << "Default" << dir.entryList(); -} - void StyleController::setSystemFont(const QFont &font) { QApplication::setFont(font); diff --git a/nymea-app/stylecontroller.h b/nymea-app/stylecontroller.h index 14f50c81..70293ad3 100644 --- a/nymea-app/stylecontroller.h +++ b/nymea-app/stylecontroller.h @@ -39,9 +39,6 @@ class StyleController : public QObject Q_PROPERTY(QString currentStyle READ currentStyle WRITE setCurrentStyle NOTIFY currentStyleChanged) Q_PROPERTY(QStringList allStyles READ allStyles CONSTANT) - Q_PROPERTY(QString currentExperience READ currentExperience WRITE setCurrentExperience NOTIFY currentExperienceChanged) - Q_PROPERTY(QStringList allExperiences READ allExperiences CONSTANT) - public: explicit StyleController(QObject *parent = nullptr); @@ -50,17 +47,10 @@ public: QStringList allStyles() const; - QString currentExperience() const; - void setCurrentExperience(const QString ¤tExperience); - - QStringList allExperiences() const; - Q_INVOKABLE void setSystemFont(const QFont &font); signals: void currentStyleChanged(); - void currentExperienceChanged(); - }; #endif // STYLECONTROLLER_H diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 0bb0096c..3c63482f 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -33,6 +33,8 @@ import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.2 import QtQuick.Window 2.3 +import Qt.labs.settings 1.0 +import Qt.labs.folderlistmodel 2.2 import Nymea 1.0 import "components" import "delegates" @@ -42,7 +44,8 @@ Page { id: root header: FancyHeader { - title: swipeView.currentItem.title + id: mainHeader + title: filteredContentModel.data(swipeView.currentIndex, "displayName") leftButtonVisible: true leftButtonImageSource: { switch (engine.jsonRpcClient.currentConnection.bearerType) { @@ -65,10 +68,14 @@ Page { var dialog = connectionDialogComponent.createObject(root) dialog.open(); } - + onMenuOpenChanged: { + if (menuOpen && d.configOverlay) { + d.configOverlay.destroy() + } + } model: ListModel { - ListElement { iconSource: "../images/share.svg"; text: qsTr("Configure things"); page: "thingconfiguration/EditThingsPage.qml" } + ListElement { iconSource: "../images/things.svg"; text: qsTr("Configure things"); page: "thingconfiguration/EditThingsPage.qml" } ListElement { iconSource: "../images/magic.svg"; text: qsTr("Magic"); page: "MagicPage.qml" } ListElement { iconSource: "../images/stock_application.svg"; text: qsTr("App settings"); page: "appsettings/AppSettingsPage.qml" } ListElement { iconSource: "../images/settings.svg"; text: qsTr("System settings"); page: "SettingsPage.qml" } @@ -79,84 +86,6 @@ Page { } } - property int currentViewIndex: 0 - - property bool swipeViewReady: false - property bool tabsReady: false - - // FIXME: All this can go away when we require Controls 2.3 (Qt 5.10) or greater as TabBar got a major rework there. - // Ideally we'd just list the 3 items and set visible to false if the server version isn't good enough but TabBar - // has troubles dealing with that. For now, let's manually fill it and use a timer to initialize the currentIndex. - Component.onCompleted: { - // Fill SwipeView (The 2 static views things and scenes will already be there). - if (engine.jsonRpcClient.ensureServerVersion(1.6)) { - swipeView.insertItem(0, favoritesViewComponent.createObject(swipeView)) - } - var experienceView = null; - if (styleController.currentExperience != "Default") { - experienceView = experienceViewComponent.createObject(swipeView, {source: "experiences/" + styleController.currentExperience + "/Main.qml" }); - swipeView.insertItem(0, experienceView) - } - root.swipeViewReady = true; - - - var pi = 0; - if (experienceView) { - tabEntryComponent.createObject(tabBar, {text: experienceView.title, iconSource: experienceView.icon, pageIndex: pi++}) - } - if (engine.jsonRpcClient.ensureServerVersion(1.6)) { - tabEntryComponent.createObject(tabBar, {text: qsTr("Favorites"), iconSource: "../images/starred.svg", pageIndex: pi++}) - } - tabEntryComponent.createObject(tabBar, {text: qsTr("Things"), iconSource: "../images/share.svg", pageIndex: pi++}) - tabEntryComponent.createObject(tabBar, {text: qsTr("Scenes"), iconSource: "../images/slideshow.svg", pageIndex: pi++}) - if (engine.jsonRpcClient.ensureServerVersion(1.6)) { - tabEntryComponent.createObject(tabBar, {text: qsTr("Groups"), iconSource: "../images/view-grid-symbolic.svg", pageIndex: pi++}) - } - - root.tabsReady = true - } - - readonly property bool viewReady: swipeViewReady && tabsReady - onViewReadyChanged: { - if (tabSettings.currentMainViewIndex > swipeView.count) { - tabSettings.currentMainViewIndex = swipeView.count - 1; - } - - // Load current index from settings - currentViewIndex = tabSettings.currentMainViewIndex; - - // If setting is not initialized yet, init to "Things" page (might be 0 or 1, depending whether we have tags support) - if (currentViewIndex === -1) { - currentViewIndex = engine.jsonRpcClient.ensureServerVersion(1.6) ? 1 : 0 - } - - // and set up a binding to sync changes back to the settings - tabSettings.currentMainViewIndex = Qt.binding(function() { return root.currentViewIndex; }); - - // Tabbar gets a little confused if it's bound to it before the init happened, do it now - tabBar.currentIndex = Qt.binding(function() { return root.currentViewIndex; }); - } - - // FIXME: Currently we don't have any feedback for executeAction - // we don't want all the results, e.g. on looped calls like "all off" - // Connections { - // target: engine.deviceManager - // onExecuteActionReply: { - // var text = params["deviceError"] - // switch(text) { - // case "DeviceErrorNoError": - // return; - // case "DeviceErrorHardwareNotAvailable": - // text = qsTr("Could not execute action. The thing is not available"); - // break; - // } - - // var errorDialog = Qt.createComponent(Qt.resolvedUrl("components/ErrorDialog.qml")) - // var popup = errorDialog.createObject(root, {text: text}) - // popup.open() - // } - // } - Connections { target: engine.ruleManager onAddRuleReply: { @@ -170,6 +99,70 @@ Page { QtObject { id: d property var editRulePage: null + property var configOverlay: null + } + + Settings { + id: mainViewSettings + category: engine.jsonRpcClient.currentHost.uuid + property string mainMenuContent: "" + property var sortOrder: [] + property var filterList: ["things"] + property int currentIndex: 0 + } + + ListModel { + id: mainMenuBaseModel + // TODO: Should read this from disk somehow maybe? + ListElement { name: "things"; source: "ThingsView"; displayName: qsTr("Things"); icon: "things" } + ListElement { name: "favorites"; source: "FavoritesView"; displayName: qsTr("Favorites"); icon: "starred" } + ListElement { name: "groups"; source: "GroupsView"; displayName: qsTr("Groups"); icon: "view-grid-symbolic" } + 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 { + id: mainMenuModel + ListElement { name: "dummy"; source: "Dummy"; displayName: ""; icon: "" } + + Component.onCompleted: { + var configList = {} + var newList = {} + var newItems = 0 + for (var i = 0; i < mainMenuBaseModel.count; i++) { + var item = mainMenuBaseModel.get(i); + var idx = mainViewSettings.sortOrder.indexOf(item.name); + if (idx === -1) { + newList[newItems++] = item; + } else { + configList[idx] = item; + } + } + + clear(); + + for (idx in configList) { + item = configList[idx]; + mainMenuModel.append(item) + } + for (idx in newList) { + item = newList[idx]; + mainMenuModel.append(item) + } + + tabBar.currentIndex = Qt.binding(function() { return mainViewSettings.currentIndex; }) + swipeView.currentIndex = Qt.binding(function() { return tabBar.currentIndex; }) + mainViewSettings.currentIndex = Qt.binding(function() { return swipeView.currentIndex; }) + } + } + + SortFilterProxyModel { + id: filteredContentModel + sourceModel: mainMenuModel + filterList: mainViewSettings.filterList + filterRoleName: "name" } ColumnLayout { @@ -226,8 +219,8 @@ Page { } } - Item { + id: contentContainer Layout.fillWidth: true Layout.fillHeight: true clip: true @@ -235,131 +228,17 @@ Page { SwipeView { id: swipeView anchors.fill: parent - currentIndex: root.currentViewIndex + opacity: d.configOverlay === null ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } - onCurrentIndexChanged: { - root.currentViewIndex = currentIndex - } + Repeater { + model: d.configOverlay != null ? null : filteredContentModel - Component { - id: experienceViewComponent - Loader { + delegate: Loader { width: swipeView.width height: swipeView.height clip: true - readonly property string title: item ? item.title : "" - readonly property string icon: item ? item.icon : "" - } - } - - Component { - id: favoritesViewComponent - FavoritesView { - id: favoritesView - objectName: "favorites" - width: swipeView.width - height: swipeView.height - property string title: qsTr("My favorites") - - EmptyViewPlaceholder { - anchors { left: parent.left; right: parent.right; margins: app.margins } - anchors.verticalCenter: parent.verticalCenter - visible: favoritesView.count === 0 && !engine.deviceManager.fetchingData - title: qsTr("There are no favorite things yet.") - text: engine.deviceManager.devices.count === 0 ? - qsTr("It appears there are no things set up either yet. In order to use favorites you need to add some things first.") : - qsTr("Favorites allow you to keep track of your most important things when you have lots of them. Watch out for the star when interacting with things and use it to mark them as your favorites.") - imageSource: "images/starred.svg" - buttonVisible: engine.deviceManager.devices.count === 0 - buttonText: qsTr("Add a thing") - onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml")) - } - - } - } - - DevicesPage { - property string title: qsTr("My things") - width: swipeView.width - height: swipeView.height - model: InterfacesSortModel { - interfacesModel: InterfacesModel { - engine: _engine - devices: DevicesProxy { - engine: _engine - } - shownInterfaces: app.supportedInterfaces - showUncategorized: true - } - } - - EmptyViewPlaceholder { - anchors { left: parent.left; right: parent.right; margins: app.margins } - anchors.verticalCenter: parent.verticalCenter - visible: engine.deviceManager.devices.count === 0 && !engine.deviceManager.fetchingData - title: qsTr("Welcome to %1!").arg(app.systemName) - // Have that split in 2 because we need those strings separated in EditDevicesPage too and don't want translators to do them twice - text: qsTr("There are no things set up yet.") + "\n" + qsTr("In order for your %1 system to be useful, go ahead and add some things.").arg(app.systemName) - imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) - buttonText: qsTr("Add a thing") - onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml")) - } - } - - ScenesView { - id: scenesView - property string title: qsTr("My scenes"); - width: swipeView.width - height: swipeView.height - - EmptyViewPlaceholder { - anchors { left: parent.left; right: parent.right; margins: app.margins } - anchors.verticalCenter: parent.verticalCenter - visible: scenesView.count === 0 && !engine.deviceManager.fetchingData - title: qsTr("There are no scenes set up yet.") - text: engine.deviceManager.devices.count === 0 ? - qsTr("It appears there are no things set up either yet. In order to use scenes you need to add some things first.") : - qsTr("Scenes provide a useful way to control your things with just one click.") - imageSource: "images/slideshow.svg" - buttonText: engine.deviceManager.devices.count === 0 ? qsTr("Add a thing") : qsTr("Add a scene") - onButtonClicked: { - if (engine.deviceManager.devices.count === 0) { - pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml")) - } else { - var newRule = engine.ruleManager.createNewRule(); - d.editRulePage = pageStack.push(Qt.resolvedUrl("magic/EditRulePage.qml"), {rule: newRule }); - d.editRulePage.startAddAction(); - d.editRulePage.StackView.onRemoved.connect(function() { - newRule.destroy(); - }) - d.editRulePage.onAccept.connect(function() { - d.editRulePage.busy = true; - engine.ruleManager.addRule(d.editRulePage.rule); - }) - d.editRulePage.onCancel.connect(function() { - pageStack.pop(); - }) - } - } - } - } - - GroupsView { - id: groupsView - property string title: qsTr("My groups"); - width: swipeView.width - height: swipeView.height - - EmptyViewPlaceholder { - anchors { left: parent.left; right: parent.right; margins: app.margins } - anchors.verticalCenter: parent.verticalCenter - visible: groupsView.count == 0 && !engine.deviceManager.fetchingData && !engine.tagsManager.busy - title: qsTr("There are no groups set up yet.") - text: qsTr("Grouping things can be useful to control multiple devices at once, for example an entire room. Watch out for the group symbol when interacting with things and use it to add them to groups.") - imageSource: "images/view-grid-symbolic.svg" - buttonVisible: false -// buttonText: qsTr("Create a group") -// onButtonClicked: pageStack.push(Qt.resolvedUrl("thingconfiguration/NewThingPage.qml")) + source: "mainviews/" + model.source + ".qml" } } } @@ -383,19 +262,300 @@ Page { } } - footer: TabBar { - id: tabBar - Material.elevation: 3 - position: TabBar.Footer - implicitHeight: 70 + (app.landscape ? -20 : 0) + footer: Item { + readonly property bool shown: tabsRepeater.count > 1 || mainHeader.menuOpen || d.configOverlay + implicitHeight: shown ? 70 + (app.landscape ? -20 : 0) : 0 + Behavior on implicitHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }} + clip: true - Component { - id: tabEntryComponent - MainPageTabButton { - property int pageIndex: 0 -// height: tabBar.height - onClicked: root.currentViewIndex = pageIndex - alignment: app.landscape ? Qt.Horizontal : Qt.Vertical + TabBar { + id: tabBar + anchors.fill: parent + Material.elevation: 3 + position: TabBar.Footer + + visible: !mainHeader.menuOpen && !d.configOverlay + opacity: d.configOverlay === null ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + + Repeater { + id: tabsRepeater + model: d.configOverlay != null ? null : filteredContentModel + + delegate: MainPageTabButton { + alignment: app.landscape ? Qt.Horizontal : Qt.Vertical + height: tabBar.height + anchors.verticalCenter: parent.verticalCenter + text: model.displayName + iconSource: "../images/" + model.icon + ".svg" + + onPressAndHold: { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + d.configOverlay = configComponent.createObject(contentContainer) + mainHeader.menuOpen = false; + } + } + } + } + + MainPageTabButton { + anchors.fill: parent + alignment: app.landscape ? Qt.Horizontal : Qt.Vertical + text: d.configOverlay ? qsTr("Done") : qsTr("Configure") + iconSource: "../images/configure.svg" + opacity: visible ? 1 : 0 + visible: mainHeader.menuOpen || d.configOverlay + Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + + checked: false + checkable: false + + onClicked: { + if (d.configOverlay) { + d.configOverlay.destroy() + } else { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + d.configOverlay = configComponent.createObject(contentContainer) + mainHeader.menuOpen = false; + } + } + } + } + + Component { + id: configComponent + Item { + id: configOverlay + width: contentContainer.width + height: contentContainer.height + + NumberAnimation { + target: configOverlay + property: "scale" + duration: 200 + easing.type: Easing.InOutQuad + from: 2 + to: 1 + running: true + } + NumberAnimation { + target: configOverlay + property: "opacity" + duration: 200 + easing.type: Easing.InOutQuad + from: 0 + to: 1 + running: true + } + + ListView { + id: configListView + model: mainMenuModel + width: parent.width + height: parent.height / 2.5 + anchors.centerIn: parent + orientation: ListView.Horizontal + moveDisplaced: Transition { + NumberAnimation { properties: "x,y"; duration: 200 } + } + + property int delegateWidth: width / 2.5 + + property bool dragging: draggingIndex >= 0 + property int draggingIndex : -1 + + MouseArea { + id: dndArea + anchors.fill: parent + preventStealing: configListView.dragging + property int dragOffset: 0 + + onPressAndHold: { + mouse.accepted = true + var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x; + configListView.draggingIndex = configListView.indexAt(mouseXInListView, mouseY) + var item = mainMenuModel.get(configListView.draggingIndex) + dndItem.displayName = item.displayName + dndItem.icon = item.icon + var visualItem = configListView.itemAt(mouseXInListView, mouseY) + dndItem.isEnabled = visualItem.isEnabled + dndArea.dragOffset = configListView.mapToItem(visualItem, mouseX, mouseY).x + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackImpact) + } + onMouseYChanged: { + if (configListView.dragging) { + var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x; + var indexUnderMouse = configListView.indexAt(mouseXInListView - dndArea.dragOffset / 2, mouseY) + indexUnderMouse = Math.min(Math.max(0, indexUnderMouse), configListView.count - 1) + if (configListView.draggingIndex !== indexUnderMouse) { + PlatformHelper.vibrate(PlatformHelper.HapticsFeedbackSelection) + mainMenuModel.move(configListView.draggingIndex, indexUnderMouse, 1) + configListView.draggingIndex = indexUnderMouse; + } + } + } + onReleased: { + print("released!") + var mouseXInListView = configListView.contentItem.mapFromItem(dndArea, mouseX, mouseY).x; + var clickedIndex = configListView.indexAt(mouseXInListView, mouseY) + var item = mainMenuModel.get(clickedIndex) + var isEnabled = mainViewSettings.filterList.indexOf(item.name) >= 0; + if (!configListView.dragging) { + var newList = [] + for (var i = 0; i < mainMenuModel.count; i++) { + var entry = mainMenuModel.get(i).name; + if (entry === item.name) { + if (!isEnabled) { + newList.push(item.name) + } + } else { + if (mainViewSettings.filterList.indexOf(entry) >= 0) { + newList.push(entry) + } + } + } + if (newList.length === 0) { + newList.push("things") + } + + mainViewSettings.filterList = newList + } + configListView.draggingIndex = -1; + + var newSortOrder = [] + for (var i = 0; i < mainMenuModel.count; i++) { + newSortOrder.push(mainMenuModel.get(i).name) + } + mainViewSettings.sortOrder = newSortOrder; + } + Timer { + id: scroller + interval: 2 + repeat: true + running: direction != 0 + property int direction: { + if (!configListView.dragging) { + return 0; + } + return dndArea.mouseX < 50 ? -2 : dndArea.mouseX > dndArea.width - 50 ? 2 : 0 + } + onTriggered: { + configListView.contentX = Math.min(Math.max(0, configListView.contentX + direction), configListView.contentWidth - configListView.width) + } + } + } + + delegate: Item { + id: configDelegate + width: configListView.delegateWidth + height: configListView.height + property bool isEnabled: mainViewSettings.filterList.indexOf(model.name) >= 0 + visible: configListView.draggingIndex !== index + + Pane { + anchors.fill: parent + anchors.margins: app.margins / 2 + Material.elevation: 2 + + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + contentItem: ItemDelegate { + anchors.fill: parent + + padding: app.margins * 2 + contentItem: GridLayout { + columns: 1 + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ColorIcon { + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) * .8 + height: width + name: Qt.resolvedUrl("images/" + model.icon + ".svg") + color: configDelegate.isEnabled ? app.accentColor : keyColor + } + } + + + Label { + text: model.displayName + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font.pixelSize: app.largeFont + } + } + } + } + } + Item { + id: dndItem + width: configListView.delegateWidth + height: configListView.height + property bool isEnabled: false + property string displayName: "" + property string icon: "things" + visible: configListView.dragging + x: dndArea.mouseX - dndArea.dragOffset + onVisibleChanged: { + if (visible) { + dragStartAnimation.start(); + } + } + + NumberAnimation { + id: dragStartAnimation + target: dndItem + property: "scale" + from: 1 + to: 0.9 + duration: 200 + } + + Pane { + anchors.fill: parent + anchors.margins: app.margins / 2 + Material.elevation: 2 + + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + + contentItem: ItemDelegate { + anchors.fill: parent + + padding: app.margins * 2 + contentItem: GridLayout { + columns: 1 + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ColorIcon { + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) * .8 + height: width + name: Qt.resolvedUrl("images/" + dndItem.icon + ".svg") + color: dndItem.isEnabled ? app.accentColor : keyColor + } + } + + + Label { + text: dndItem.displayName + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font.pixelSize: app.largeFont + } + } + } + } + } } } } @@ -453,7 +613,7 @@ Page { font.pixelSize: app.smallFont elide: Text.ElideRight color: Material.color(Material.Grey) -// horizontalAlignment: Text.AlignHCenter + // horizontalAlignment: Text.AlignHCenter } Label { Layout.fillWidth: true @@ -461,7 +621,7 @@ Page { font.pixelSize: app.smallFont elide: Text.ElideRight color: Material.color(Material.Grey) -// horizontalAlignment: Text.AlignHCenter + // horizontalAlignment: Text.AlignHCenter } } ColorIcon { diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 2b2918bb..bec42a4b 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -33,6 +33,7 @@ import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.2 import Qt.labs.settings 1.0 +import Qt.labs.folderlistmodel 2.2 import QtQuick.Window 2.3 import Nymea 1.0 @@ -258,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": @@ -533,6 +534,11 @@ ApplicationWindow { onStateChanged: closeTimer.stop() } + FolderListModel { + id: availableMainViews + folder: "mainviews" + showFiles: false + } // NOTE: If using a Dialog, make sure closePolicy does not contain Dialog.CloseOnPressOutside // or the virtual keyboard will close when pressing it... diff --git a/nymea-app/ui/appsettings/LookAndFeelSettingsPage.qml b/nymea-app/ui/appsettings/LookAndFeelSettingsPage.qml index 8d0370b1..66d6a1d5 100644 --- a/nymea-app/ui/appsettings/LookAndFeelSettingsPage.qml +++ b/nymea-app/ui/appsettings/LookAndFeelSettingsPage.qml @@ -120,25 +120,6 @@ SettingsPageBase { onClicked: settings.showConnectionTabs = checked } - RowLayout { - Layout.leftMargin: app.margins - Layout.rightMargin: app.margins - visible: settings.showHiddenOptions - - Label { - Layout.fillWidth: true - text: qsTr("Experience mode") - } - - ComboBox { - currentIndex: model.indexOf(styleController.currentExperience) - model: styleController.allExperiences - onActivated: { - styleController.currentExperience = model[index] - } - } - } - SettingsPageSectionHeader { text: qsTr("Regional") } diff --git a/nymea-app/ui/components/ColorIcon.qml b/nymea-app/ui/components/ColorIcon.qml index be85552d..715069cd 100644 --- a/nymea-app/ui/components/ColorIcon.qml +++ b/nymea-app/ui/components/ColorIcon.qml @@ -48,7 +48,10 @@ Item { id: image anchors.fill: parent anchors.margins: parent ? parent.margins : 0 - source: width > 0 && height > 0 && icon.name ? icon.name : "" + source: width > 0 && height > 0 && icon.name ? + icon.name.endsWith(".svg") ? icon.name + : "qrc:/ui/images/" + icon.name + ".svg" + : "" sourceSize { width: width height: height diff --git a/nymea-app/ui/components/FancyHeader.qml b/nymea-app/ui/components/FancyHeader.qml index 3ed45383..59099639 100644 --- a/nymea-app/ui/components/FancyHeader.qml +++ b/nymea-app/ui/components/FancyHeader.qml @@ -35,7 +35,7 @@ import QtQuick.Controls.Material 2.1 ToolBar { id: root - height: 50 + (d.menuOpen ? app.iconSize * 3 + app.margins / 2 : 0) + height: 50 + (menuOpen ? app.iconSize * 3 + app.margins / 2 : 0) Behavior on height { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } } property string title @@ -47,16 +47,13 @@ ToolBar { signal clicked(int index); signal leftButtonClicked(); - QtObject { - id: d - property bool menuOpen: false - } + property bool menuOpen: false RowLayout { id: mainRow height: 50 width: parent.width - opacity: d.menuOpen ? 0 : 1 + opacity: menuOpen ? 0 : 1 Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } } HeaderButton { @@ -81,7 +78,7 @@ ToolBar { HeaderButton { id: menuButton imageSource: "../images/navigation-menu.svg" - onClicked: d.menuOpen = true + onClicked: menuOpen = true } } @@ -89,7 +86,7 @@ ToolBar { height: 50 anchors.bottom: menuPanel.top width: parent.width - opacity: d.menuOpen ? 1 : 0 + opacity: menuOpen ? 1 : 0 visible: opacity > 0 Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } } @@ -106,7 +103,7 @@ ToolBar { HeaderButton { imageSource:"../images/close.svg" - onClicked: d.menuOpen = false + onClicked: menuOpen = false } } @@ -118,7 +115,7 @@ ToolBar { width: Math.min(menuRow.childrenRect.width, parent.width) height: app.iconSize * 3 contentWidth: menuRow.childrenRect.width - opacity: d.menuOpen ? 1 : 0 + opacity: menuOpen ? 1 : 0 visible: opacity > 0 Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } } @@ -132,7 +129,7 @@ ToolBar { width: app.iconSize * 3 onClicked: { - d.menuOpen = false + menuOpen = false root.clicked(index) } diff --git a/nymea-app/ui/components/MainPageTabButton.qml b/nymea-app/ui/components/MainPageTabButton.qml index aee5adc3..27c01103 100644 --- a/nymea-app/ui/components/MainPageTabButton.qml +++ b/nymea-app/ui/components/MainPageTabButton.qml @@ -44,22 +44,27 @@ TabButton { opacity: 0.05 } - contentItem: GridLayout { - columns: root.alignment === Qt.Vertical ? 1 : 2 - rowSpacing: 4 - ColorIcon { - Layout.preferredWidth: app.iconSize - Layout.preferredHeight: app.iconSize - Layout.alignment: Qt.AlignHCenter - name: root.iconSource - color: root.checked ? app.accentColor : keyColor - } - Label { - Layout.fillWidth: root.alignment === Qt.Vertical - text: root.text - horizontalAlignment: Text.AlignHCenter - font.pixelSize: app.smallFont - color: root.checked ? app.accentColor : Material.foreground + contentItem: Item { + height: root.height + Grid { + anchors.centerIn: parent + columns: root.alignment == Qt.Vertical ? 1 : 2 + spacing: root.alignment == Qt.Horizontal ? app.margins : app.margins / 2 + horizontalItemAlignment: Grid.AlignHCenter + verticalItemAlignment: Grid.AlignVCenter + + ColorIcon { + width: app.iconSize + height: app.iconSize + name: root.iconSource + color: root.checked ? app.accentColor : keyColor + } + Label { + id: textLabel + text: root.text + font.pixelSize: app.smallFont + color: root.checked ? app.accentColor : Material.foreground + } } } } diff --git a/nymea-app/ui/mainviews/DevicesPage.qml b/nymea-app/ui/components/MainViewBase.qml similarity index 74% rename from nymea-app/ui/mainviews/DevicesPage.qml rename to nymea-app/ui/components/MainViewBase.qml index 1cc17923..a33bbbf8 100644 --- a/nymea-app/ui/mainviews/DevicesPage.qml +++ b/nymea-app/ui/components/MainViewBase.qml @@ -34,30 +34,13 @@ import QtQuick.Controls.Material 2.1 import QtQuick.Layouts 1.2 import Nymea 1.0 import "../components" +import "../delegates" MouseArea { id: root - property alias count: interfacesGridView.count - property alias model: interfacesGridView.model // Prevent scroll events to swipe left/right in case they fall through the grid preventStealing: true onWheel: wheel.accepted = true - GridView { - id: interfacesGridView - anchors.fill: parent - anchors.margins: app.margins / 2 - - readonly property int minTileWidth: 172 - readonly property int tilesPerRow: root.width / minTileWidth - - cellWidth: width / tilesPerRow - cellHeight: cellWidth - delegate: DevicesPageDelegate { - width: interfacesGridView.cellWidth - height: interfacesGridView.cellHeight - iface: Interfaces.findByName(model.name) - } - } } 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 new file mode 100644 index 00000000..540d40d4 --- /dev/null +++ b/nymea-app/ui/components/SmartMeterChart.qml @@ -0,0 +1,121 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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.1 +import QtCharts 2.2 +import Nymea 1.0 + +ChartView { + id: chart + backgroundColor: app.backgroundColor + theme: ChartView.ChartThemeLight + legend.labelColor: app.foregroundColor + legend.font.pixelSize: app.smallFont + legend.alignment: Qt.AlignRight + titleColor: app.foregroundColor + + property ThingsProxy meters: null + property int multiplier: 1 + + Connections { + target: meters + onCountChanged: chart.refresh() + } + + Component.onCompleted: { + chart.refresh() + } + + QtObject { + id: d + property var sliceMap: {} + } + + function refresh() { + pieSeries.clear(); + d.sliceMap = {} + for (var i = 0; i < meters.count; i++) { + var thing = meters.get(i); + 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) + } + var totalProducedStateType = thing.thingClass.stateTypes.findByName("totalEnergyProduced") + if (totalProducedStateType) { + var totalProducedState = thing.states.getState(totalProducedStateType.id) + value = value - (totalProducedState.value * chart.multiplier) + } + var slice = pieSeries.append(thing.name, Math.max(0, value)) + var color = app.accentColor + for (var j = 0; j < i; j+=2) { + if (i % 2 == 0) { + color = Qt.lighter(color, 1.2); + } else { + color = Qt.darker(color, 1.2) + } + } + slice.color = color + d.sliceMap[slice] = i + } + } + + PieSeries { + id: pieSeries + holeSize: 0.6 + size: 0.8 + + onClicked: { + print("clicked slice", slice, d.sliceMap[slice], meters.get(d.sliceMap[slice])) + pageStack.push("../devicepages/SmartMeterDevicePage.qml", {device: meters.get(d.sliceMap[slice])}) + } + } + + ColumnLayout { + x: chart.plotArea.x + (chart.plotArea.width * 0.5) - (width / 2) + y: chart.plotArea.y + (chart.plotArea.height * 0.5) - (height / 2) + + Label { + font.pixelSize: app.largeFont + Layout.alignment: Qt.AlignHCenter + text: Math.round(pieSeries.sum * 1000) / 1000 + } + + Label { + text: "KWh" + Layout.alignment: Qt.AlignHCenter + } + } +} + 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/mainviews/DevicesPageDelegate.qml b/nymea-app/ui/delegates/InterfaceTile.qml similarity index 96% rename from nymea-app/ui/mainviews/DevicesPageDelegate.qml rename to nymea-app/ui/delegates/InterfaceTile.qml index 1b6cdd36..9d936ca5 100644 --- a/nymea-app/ui/mainviews/DevicesPageDelegate.qml +++ b/nymea-app/ui/delegates/InterfaceTile.qml @@ -161,7 +161,7 @@ MainPageTile { case "media": return mediaControlComponent default: - console.warn("DevicesPageDelegate, inlineControl: Unhandled interface", iface.name) + console.warn("InterfaceTile, inlineControl: Unhandled interface", iface.name) } } @@ -227,7 +227,7 @@ MainPageTile { MediaControls { iconSize: app.iconSize * 1.2 - device: inlineMediaControl.currentDevice + thing: inlineMediaControl.currentDevice } } } @@ -267,7 +267,7 @@ MainPageTile { if (thing.thingClass.interfaces.indexOf("statefulgaragedoor") >= 0 || thing.thingClass.interfaces.indexOf("extendedstatefulgaragedoor") >= 0 || thing.thingClass.interfaces.indexOf("garagegate") >= 0) { statefulCount++; var stateType = thing.thingClass.stateTypes.findByName("state"); - if (stateType && device.states.getState(stateType.id).value !== "closed") { + if (stateType && thing.states.getState(stateType.id).value !== "closed") { count++; } } @@ -285,7 +285,7 @@ MainPageTile { return "" // return qsTr("%1 installed").arg(devicesProxy.count) } - console.warn("DevicesPageDelegate, inlineButtonControl: Unhandled interface", model.name) + console.warn("InterfaceTile, inlineButtonControl: Unhandled interface", model.name) } font.pixelSize: app.smallFont elide: Text.ElideRight @@ -329,7 +329,7 @@ MainPageTile { case "extendedshutter": return "../images/up.svg" default: - console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name) + console.warn("InterfaceTile", "inlineButtonControl image: Unhandled interface", iface.name) } return "" } @@ -370,7 +370,7 @@ MainPageTile { } break; default: - console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name) + console.warn("InterfaceTile:", "inlineButtonControl clicked: Unhandled interface", iface.name) } } } @@ -409,7 +409,7 @@ MainPageTile { case "extendedshutter": return "../images/media-playback-stop.svg" default: - console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name) + console.warn("InterfaceTile, inlineButtonControl image: Unhandled interface", iface.name) } return ""; } @@ -450,7 +450,7 @@ MainPageTile { } break; default: - console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name) + console.warn("InterfaceTile, inlineButtonControl clicked: Unhandled interface", iface.name) } } } @@ -500,7 +500,7 @@ MainPageTile { case "extendedshutter": return "../images/down.svg" default: - console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name) + console.warn("InterfaceTile, inlineButtonControl image: Unhandled interface", iface.name) } } } @@ -585,7 +585,7 @@ MainPageTile { } default: - console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name) + console.warn("InterfaceTile, inlineButtonControl clicked: Unhandled interface", iface.name) } } } 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/experiences/garagegates/Main.qml b/nymea-app/ui/experiences/garagegates/Main.qml deleted file mode 100644 index 11e91684..00000000 --- a/nymea-app/ui/experiences/garagegates/Main.qml +++ /dev/null @@ -1,128 +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.3 -import QtQuick.Layouts 1.2 -import QtQuick.Controls 2.2 -import "qrc:/ui/components" -import Nymea 1.0 - -Item { - id: root - readonly property string title: qsTr("Garage doors") - readonly property string icon: Qt.resolvedUrl("qrc:/ui/images/shutter/shutter-050.svg") - - DevicesProxy { - id: garagesFilterModel - engine: _engine - shownInterfaces: ["garagedoors"] - } - - EmptyViewPlaceholder { - anchors.centerIn: parent - width: parent.width - app.margins * 2 - text: qsTr("There are no garage doors set up yet.") - imageSource: "qrc:/ui/images/shutter/shutter-050.svg" - buttonText: qsTr("Set up now") - visible: garagesFilterModel.count === 0 - } - - SwipeView { - id: swipeView - anchors.fill: parent - - Repeater { - model: garagesFilterModel - - Item { - id: garageGateView - width: swipeView.width - height: swipeView.height - - readonly property Device device: garagesFilterModel.get(index) - - readonly property StateType openStateType: device.deviceClass.stateTypes.findByName("state") - readonly property State openState: openStateType ? device.states.getState(openStateType.id) : null - - readonly property StateType intermediatePositionStateType: device.deviceClass.stateTypes.findByName("intermediatePosition") - readonly property State intermediatePositionState: intermediatePositionStateType ? device.states.getState(intermediatePositionStateType.id) : null - - GridLayout { - id: layout - anchors.fill: parent - anchors.margins: app.margins - columns: app.landscape ? 2 : 1 - - Label { - id: label - text: garageGateView.device.name - font.pixelSize: app.largeFont - Layout.preferredWidth: layout.width - Layout.columnSpan: parent.columns - horizontalAlignment: Text.AlignHCenter - } - - Item { - Layout.fillHeight: true - Layout.fillWidth: true - Layout.minimumWidth: app.landscape ? layout.width / 2 : layout.width - - ColorIcon { - height: Math.min(parent.height, parent.width) - width: height - anchors.centerIn: parent - name: "qrc:/ui/images/shutter/shutter-" + currentImage + ".svg" - property string currentImage: garageGateView.openState.value === "closed" ? "100" : - garageGateView.openState.value === "open" && garageGateView.intermediatePositionState.value === false ? "000" : "050" - } - } - - Item { - Layout.fillWidth: true - Layout.preferredHeight: controls.implicitHeight - Layout.minimumWidth: app.landscape ? layout.width / 2 : layout.width - - ShutterControls { - id: controls - device: garageGateView.device - spacing: (parent.width - app.iconSize*2*children.length) / (children.length - 1) - } - } - } - } - } - } - - PageIndicator { - anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter } - count: garagesFilterModel.count - currentIndex: swipeView.currentIndex - } -} diff --git a/nymea-app/ui/grouping/GroupInterfacesPage.qml b/nymea-app/ui/grouping/GroupInterfacesPage.qml index 87f158e9..d89f419b 100644 --- a/nymea-app/ui/grouping/GroupInterfacesPage.qml +++ b/nymea-app/ui/grouping/GroupInterfacesPage.qml @@ -71,12 +71,12 @@ Page { cellWidth: width / tilesPerRow cellHeight: cellWidth -// delegate: DevicesPageDelegate { +// delegate: InterfaceTile { // width: interfacesGridView.cellWidth // height: interfacesGridView.cellHeight // } - delegate: DevicesPageDelegate { + delegate: InterfaceTile { width: interfacesGridView.cellWidth height: interfacesGridView.cellHeight iface: Interfaces.findByName(model.name) 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/energy.svg b/nymea-app/ui/images/energy.svg new file mode 100644 index 00000000..888be089 --- /dev/null +++ b/nymea-app/ui/images/energy.svg @@ -0,0 +1,23 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + 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/images/share.svg b/nymea-app/ui/images/things.svg similarity index 100% rename from nymea-app/ui/images/share.svg rename to nymea-app/ui/images/things.svg diff --git a/nymea-app/ui/mainviews/EnergyView.qml b/nymea-app/ui/mainviews/EnergyView.qml new file mode 100644 index 00000000..0c836634 --- /dev/null +++ b/nymea-app/ui/mainviews/EnergyView.qml @@ -0,0 +1,373 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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: consumers + engine: _engine + shownInterfaces: ["smartmeterconsumer"] + } + ThingsProxy { + id: producers + engine: _engine + shownInterfaces: ["smartmeterproducer"] + } + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + visible: !engine.thingManager.fetchingData && consumers.count == 0 + title: qsTr("There are no energy meters installed.") + text: qsTr("To get an overview of your current energy usage, install some energy meters.") + imageSource: "../images/smartmeter.svg" + buttonText: qsTr("Add things") + } + + Flickable { + anchors.fill: parent + topMargin: app.margins + contentHeight: energyGrid.childrenRect.height + + GridLayout { + id: energyGrid + width: parent.width + visible: consumers.count > 0 + columns: root.width > 600 ? 2 : 1 + rowSpacing: 0 + + SmartMeterChart { + Layout.preferredWidth: energyGrid.width / energyGrid.columns + Layout.preferredHeight: width * .7 + meters: consumers + 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 + titleColor: app.foregroundColor + title: qsTr("Power usage history") + + property var startTime: xAxis.min + property var endTime: xAxis.max + + property int sampleRate: XYSeriesAdapter.SampleRateMinute + + property int busyModels: 0 + + BusyIndicator { + anchors.centerIn: parent + visible: chartView.busyModels > 0 + running: visible + } + + 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 + onBusyChanged: { + if (busy) { + chartView.busyModels++ + } else { + chartView.busyModels-- + } + } + } + property XYSeriesAdapter adapter: XYSeriesAdapter { + id: seriesAdapter + logsModel: logsModel + sampleRate: chartView.sampleRate + xySeries: upperSeries + } + Connections { + target: xAxis + onMinChanged: seriesAdapter.ensureSamples(xAxis.min, xAxis.max) + onMaxChanged: seriesAdapter.ensureSamples(xAxis.min, xAxis.max) + } + 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") + seriesAdapter.ensureSamples(xAxis.min, xAxis.max) + 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; + } + } + } + + 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: "%d" + labelsColor: app.foregroundColor + 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 * 6) + 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 + meters: producers + title: qsTr("Total produced energy") + visible: producers.count > 0 + multiplier: -1 + } + } + } +} diff --git a/nymea-app/ui/mainviews/FavoritesView.qml b/nymea-app/ui/mainviews/FavoritesView.qml index 9e87aff7..5695f373 100644 --- a/nymea-app/ui/mainviews/FavoritesView.qml +++ b/nymea-app/ui/mainviews/FavoritesView.qml @@ -36,15 +36,10 @@ import Nymea 1.0 import "../components" import "../delegates" -MouseArea { +MainViewBase { id: root property bool editMode: false - readonly property int count: tagsProxy.count - - // Prevent scroll events to swipe left/right in case they fall through the grid - preventStealing: true - onWheel: wheel.accepted = true TagsProxyModel { id: tagsProxy @@ -167,4 +162,19 @@ MouseArea { } } } + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + visible: gridView.count === 0 && !engine.deviceManager.fetchingData + title: qsTr("There are no favorite things yet.") + text: engine.deviceManager.devices.count === 0 ? + qsTr("It appears there are no things set up either yet. In order to use favorites you need to add some things first.") : + qsTr("Favorites allow you to keep track of your most important things when you have lots of them. Watch out for the star when interacting with things and use it to mark them as your favorites.") + imageSource: "../images/starred.svg" + buttonVisible: engine.deviceManager.devices.count === 0 + buttonText: qsTr("Add a thing") + onButtonClicked: pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml")) + } + } diff --git a/nymea-app/ui/mainviews/GaragesView.qml b/nymea-app/ui/mainviews/GaragesView.qml new file mode 100644 index 00000000..add60278 --- /dev/null +++ b/nymea-app/ui/mainviews/GaragesView.qml @@ -0,0 +1,240 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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.3 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.2 +import "../components" +import Nymea 1.0 + +MainViewBase { + id: root + + readonly property bool landscape: width > height + + DevicesProxy { + id: garagesFilterModel + engine: _engine + shownInterfaces: ["garagedoor", "garagegate"] + } + + EmptyViewPlaceholder { + anchors.centerIn: parent + width: parent.width - app.margins * 2 + text: qsTr("There are no garage doors set up yet.") + imageSource: "qrc:/ui/images/garage/garage-100.svg" + buttonText: qsTr("Set up now") + visible: garagesFilterModel.count === 0 && !engine.thingManager.fetchingData + onButtonClicked: pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml")) + } + + SwipeView { + id: swipeView + anchors.fill: parent + + Repeater { + model: garagesFilterModel + + Item { + id: garageGateView + width: swipeView.width + height: swipeView.height + + readonly property Device thing: garagesFilterModel.get(index) + + + readonly property bool isImpulseBased: thing.thingClass.interfaces.indexOf("impulsegaragedoor") >= 0 + readonly property bool isStateful: thing.thingClass.interfaces.indexOf("statefulgaragedoor") >= 0 + readonly property bool isExtended: thing.thingClass.interfaces.indexOf("extendedstatefulgaragedoor") >= 0 + + // Stateful garagedoor + readonly property StateType stateStateType: thing.thingClass.stateTypes.findByName("state") + readonly property State stateState: stateStateType ? thing.states.getState(stateStateType.id) : null + + // Extended stateful garagedoor + readonly property StateType percentageStateType: thing.thingClass.stateTypes.findByName("percentage") + readonly property State percentageState: percentageStateType ? thing.states.getState(percentageStateType.id) : null + + + // Backward compatiblity with old garagegate interface + readonly property StateType intermediatePositionStateType: thing.thingClass.stateTypes.findByName("intermediatePosition") + readonly property var intermediatePositionState: intermediatePositionStateType ? device.states.getState(intermediatePositionStateType.id) : null + + // Some garages may also implement the light interface + readonly property var lightStateType: thing.thingClass.stateTypes.findByName("power") + readonly property var lightState: lightStateType ? thing.states.getState(lightStateType.id) : null + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: app.margins + anchors.bottomMargin: app.margins + + Label { + Layout.fillWidth: true + font.pixelSize: app.largeFont + text: garageGateView.thing.name + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + + GridLayout { + columns: root.landscape ? 2 : 1 + + ColorIcon { + id: shutterImage + Layout.preferredWidth: root.landscape ? + Math.min(parent.width - shutterControlsContainer.minimumWidth, parent.height) - app.margins + : Math.min(Math.min(parent.width, 500), parent.height - shutterControlsContainer.minimumHeight) + Layout.preferredHeight: width + Layout.alignment: Qt.AlignHCenter + property string currentImage: { + if (garageGateView.isExtended) { + return app.pad(Math.round(garageGateView.percentageState.value / 10), 2) + "0" + } + if (garageGateView.intermediatePositionStateType) { + return garageGateView.stateState.value === "closed" ? "100" + : garageGateView.intermediatePositionState.value === false ? "000" : "050" + } + return "100" + } + name: "../images/garage/garage-" + currentImage + ".svg" + + Item { + id: arrows + anchors.centerIn: parent + width: app.iconSize * 2 + height: parent.height * .6 + clip: true + visible: garageGateView.stateStateType && (garageGateView.stateState.value === "opening" || garageGateView.stateState.value === "closing") + property bool up: garageGateView.stateState && garageGateView.stateState.value === "opening" + + // NumberAnimation doesn't reload to/from while it's running. If we switch from closing to opening or vice versa + // we need to somehow stop and start the animation + property bool animationHack: true + onAnimationHackChanged: { + if (!animationHack) hackTimer.start(); + } + Timer { id: hackTimer; interval: 1; onTriggered: arrows.animationHack = true } + Connections { target: garageGateView.stateState; onValueChanged: arrows.animationHack = false } + + NumberAnimation { + target: arrowColumn + property: "y" + duration: 500 + easing.type: Easing.Linear + from: arrows.up ? app.iconSize : -app.iconSize + to: arrows.up ? -app.iconSize : app.iconSize + loops: Animation.Infinite + running: arrows.animationHack && garageGateView.stateState && (garageGateView.stateState.value === "opening" || garageGateView.stateState.value === "closing") + } + + Column { + id: arrowColumn + width: parent.width + + Repeater { + model: arrows.height / app.iconSize + 1 + ColorIcon { + name: arrows.up ? "../images/up.svg" : "../images/down.svg" + width: parent.width + height: width + color: app.accentColor + } + } + } + } + } + + Item { + id: shutterControlsContainer + Layout.fillWidth: true + Layout.margins: app.margins * 2 + Layout.fillHeight: true + property int minimumWidth: app.iconSize * 2.5 * (garageGateView.lightState ? 4 : 3) + property int minimumHeight: app.iconSize * 2.5 + + ItemDelegate { + height: app.iconSize * 2 + width: height + anchors.centerIn: parent + visible: garageGateView.isImpulseBased + ColorIcon { + anchors.fill: parent + name: "../images/closable-move.svg" + anchors.margins: app.margins + } + onClicked: { + var actionTypeId = garageGateView.thing.thingClass.actionTypes.findByName("triggerImpulse").id + print("Triggering impulse", actionTypeId) + engine.thingManager.executeAction(garageGateView.thing.id, actionTypeId) + } + } + + ShutterControls { + id: shutterControls + device: garageGateView.thing + anchors.centerIn: parent + spacing: (parent.width - app.iconSize*2*children.length) / (children.length - 1) + visible: !garageGateView.isImpulseBased + + ItemDelegate { + width: app.iconSize * 2 + height: width + visible: garageGateView.lightStateType !== null + + ColorIcon { + anchors.fill: parent + anchors.margins: app.margins + name: "../images/light-" + (garageGateView.lightState && garageGateView.lightState.value === true ? "on" : "off") + ".svg" + color: garageGateView.lightState && garageGateView.lightState.value === true ? Material.accent : keyColor + } + onClicked: { + var params = []; + var param = {}; + param["paramTypeId"] = garageGateView.lightStateType.id; + param["value"] = !garageGateView.lightState.value; + params.push(param) + engine.deviceManager.executeAction(garageGateView.device.id, garageGateView.lightStateType.id, params) + } + } + } + } + } + } + } + } + } + + PageIndicator { + anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter } + count: garagesFilterModel.count + currentIndex: swipeView.currentIndex + } +} diff --git a/nymea-app/ui/mainviews/GroupsView.qml b/nymea-app/ui/mainviews/GroupsView.qml index 01f1a7d3..ecb203e0 100644 --- a/nymea-app/ui/mainviews/GroupsView.qml +++ b/nymea-app/ui/mainviews/GroupsView.qml @@ -35,12 +35,8 @@ import Nymea 1.0 import QtQuick.Controls.Material 2.2 import "../components" -MouseArea { +MainViewBase { id: root - preventStealing: true - onWheel: wheel.accepted = true - - readonly property int count: groupsGridView.count GridView { id: groupsGridView @@ -488,7 +484,18 @@ MouseArea { } // 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 } } + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + visible: groupsGridView.count == 0 && !engine.deviceManager.fetchingData && !engine.tagsManager.busy + title: qsTr("There are no groups set up yet.") + text: qsTr("Grouping things can be useful to control multiple devices at once, for example an entire room. Watch out for the group symbol when interacting with things and use it to add them to groups.") + imageSource: "../images/view-grid-symbolic.svg" + buttonVisible: false + } + } 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/mainviews/ScenesView.qml b/nymea-app/ui/mainviews/ScenesView.qml index a4b09e2a..d8235a32 100644 --- a/nymea-app/ui/mainviews/ScenesView.qml +++ b/nymea-app/ui/mainviews/ScenesView.qml @@ -35,15 +35,9 @@ import Nymea 1.0 import QtQuick.Controls.Material 2.2 import "../components" -MouseArea { +MainViewBase { id: root - readonly property int count: interfacesGridView.count - - // Prevent scroll events to swipe left/right in case they fall through the grid - preventStealing: true - onWheel: wheel.accepted = true - GridView { id: interfacesGridView anchors.fill: parent @@ -81,4 +75,37 @@ MouseArea { } } } + + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + visible: interfacesGridView.count === 0 && !engine.deviceManager.fetchingData + title: qsTr("There are no scenes set up yet.") + text: engine.deviceManager.devices.count === 0 ? + qsTr("It appears there are no things set up either yet. In order to use scenes you need to add some things first.") : + qsTr("Scenes provide a useful way to control your things with just one click.") + imageSource: "../images/slideshow.svg" + buttonText: engine.deviceManager.devices.count === 0 ? qsTr("Add a thing") : qsTr("Add a scene") + onButtonClicked: { + if (engine.deviceManager.devices.count === 0) { + pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml")) + } else { + var newRule = engine.ruleManager.createNewRule(); + d.editRulePage = pageStack.push(Qt.resolvedUrl("../magic/EditRulePage.qml"), {rule: newRule }); + d.editRulePage.startAddAction(); + d.editRulePage.StackView.onRemoved.connect(function() { + newRule.destroy(); + }) + d.editRulePage.onAccept.connect(function() { + d.editRulePage.busy = true; + engine.ruleManager.addRule(d.editRulePage.rule); + }) + d.editRulePage.onCancel.connect(function() { + pageStack.pop(); + }) + } + } + } + } diff --git a/nymea-app/ui/mainviews/ThingsView.qml b/nymea-app/ui/mainviews/ThingsView.qml new file mode 100644 index 00000000..72da5b29 --- /dev/null +++ b/nymea-app/ui/mainviews/ThingsView.qml @@ -0,0 +1,83 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 Nymea 1.0 +import "../components" +import "../delegates" + +MainViewBase { + id: root + + InterfacesSortModel { + id: mainModel + interfacesModel: InterfacesModel { + engine: _engine + devices: DevicesProxy { + engine: _engine + } + shownInterfaces: app.supportedInterfaces + showUncategorized: true + } + } + + GridView { + id: interfacesGridView + anchors.fill: parent + anchors.margins: app.margins / 2 + model: mainModel + + readonly property int minTileWidth: 172 + readonly property int tilesPerRow: root.width / minTileWidth + + cellWidth: width / tilesPerRow + cellHeight: cellWidth + delegate: InterfaceTile { + width: interfacesGridView.cellWidth + height: interfacesGridView.cellHeight + iface: Interfaces.findByName(model.name) + } + } + + EmptyViewPlaceholder { + anchors { left: parent.left; right: parent.right; margins: app.margins } + anchors.verticalCenter: parent.verticalCenter + visible: engine.deviceManager.devices.count === 0 && !engine.deviceManager.fetchingData + title: qsTr("Welcome to %1!").arg(app.systemName) + // Have that split in 2 because we need those strings separated in EditDevicesPage too and don't want translators to do them twice + text: qsTr("There are no things set up yet.") + "\n" + qsTr("In order for your %1 system to be useful, go ahead and add some things.").arg(app.systemName) + imageSource: "qrc:/styles/%1/logo.svg".arg(styleController.currentStyle) + buttonText: qsTr("Add a thing") + onButtonClicked: pageStack.push(Qt.resolvedUrl("../thingconfiguration/NewThingPage.qml")) + } +} 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 diff --git a/nymea-app/ui/thingconfiguration/SetupWizard.qml b/nymea-app/ui/thingconfiguration/SetupWizard.qml index 61dbc57c..83c2170e 100644 --- a/nymea-app/ui/thingconfiguration/SetupWizard.qml +++ b/nymea-app/ui/thingconfiguration/SetupWizard.qml @@ -309,7 +309,7 @@ Page { } BusyIndicator { running: visible - anchors.horizontalCenter: parent.horizontalCenter + Layout.alignment: Qt.AlignHCenter } }