Merge PR #405: Make the main view more customizable

This commit is contained in:
Jenkins nymea 2020-08-27 16:12:06 +02:00
commit e4d0392bca
72 changed files with 3029 additions and 1897 deletions

View File

@ -76,7 +76,7 @@ void DeviceManager::init()
qWarning() << "received an event from a device we don't know..." << deviceId << event; qWarning() << "received an event from a device we don't know..." << deviceId << event;
return; 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()); dev->eventTriggered(eventTypeId.toString(), event.value("params").toMap());
emit eventTriggered(deviceId.toString(), 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()); qWarning() << "received an event from a device we don't know..." << deviceId << qUtf8Printable(QJsonDocument::fromVariant(data).toJson());
return; 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()); dev->eventTriggered(eventTypeId.toString(), event.value("params").toMap());
} else if (notification == "Integrations.IOConnectionAdded") { } else if (notification == "Integrations.IOConnectionAdded") {
QVariantMap connectionMap = data.value("params").toMap().value("ioConnection").toMap(); QVariantMap connectionMap = data.value("params").toMap().value("ioConnection").toMap();
@ -343,7 +343,7 @@ void DeviceManager::getConfiguredDevicesResponse(const QVariantMap &params)
QVariantList stateVariantList = deviceVariant.toMap().value("states").toList(); QVariantList stateVariantList = deviceVariant.toMap().value("states").toList();
foreach (const QVariant &stateMap, stateVariantList) { foreach (const QVariant &stateMap, stateVariantList) {
QString stateTypeId = stateMap.toMap().value("stateTypeId").toString(); QString stateTypeId = stateMap.toMap().value("stateTypeId").toString();
StateType *st = device->deviceClass()->stateTypes()->getStateType(stateTypeId); StateType *st = device->thingClass()->stateTypes()->getStateType(stateTypeId);
if (!st) { if (!st) {
qWarning() << "Can't find a statetype for this state"; qWarning() << "Can't find a statetype for this state";
continue; continue;
@ -750,7 +750,7 @@ void DeviceManager::executeBrowserItemActionResponse(const QVariantMap &params)
void DeviceManager::getIOConnectionsResponse(const QVariantMap &params) void DeviceManager::getIOConnectionsResponse(const QVariantMap &params)
{ {
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()) { foreach (const QVariant &connectionVariant, params.value("params").toMap().value("ioConnections").toList()) {
QVariantMap connectionMap = connectionVariant.toMap(); QVariantMap connectionMap = connectionVariant.toMap();

View File

@ -72,24 +72,24 @@ QVariant Devices::data(const QModelIndex &index, int role) const
if (index.row() < 0 || index.row() >= m_devices.count()) if (index.row() < 0 || index.row() >= m_devices.count())
return QVariant(); return QVariant();
Device *device = m_devices.at(index.row()); Device *thing = m_devices.at(index.row());
switch (role) { switch (role) {
case RoleName: case RoleName:
return device->name(); return thing->name();
case RoleId: case RoleId:
return device->id().toString(); return thing->id().toString();
case RoleDeviceClass: case RoleDeviceClass:
return device->deviceClassId().toString(); return thing->deviceClassId().toString();
case RoleParentDeviceId: case RoleParentDeviceId:
return device->parentDeviceId().toString(); return thing->parentDeviceId().toString();
case RoleSetupStatus: case RoleSetupStatus:
return device->setupStatus(); return thing->setupStatus();
case RoleSetupDisplayMessage: case RoleSetupDisplayMessage:
return device->setupDisplayMessage(); return thing->setupDisplayMessage();
case RoleInterfaces: case RoleInterfaces:
return device->deviceClass()->interfaces(); return thing->thingClass()->interfaces();
case RoleBaseInterface: case RoleBaseInterface:
return device->deviceClass()->baseInterface(); return thing->thingClass()->baseInterface();
} }
return QVariant(); return QVariant();
} }

View File

@ -592,7 +592,7 @@ void JsonRpcClient::helloReply(const QVariantMap &params)
qDebug() << "Handshake reply:" << "Protocol version:" << protoVersionString << "InitRequired:" << m_initialSetupRequired << "AuthRequired:" << m_authenticationRequired << "PushButtonAvailable:" << m_pushButtonAuthAvailable;; 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) { if (m_jsonRpcVersion < minimumRequiredVersion) {
qWarning() << "Nymea core doesn't support minimum required version. Required:" << minimumRequiredVersion << "Found:" << m_jsonRpcVersion; qWarning() << "Nymea core doesn't support minimum required version. Required:" << minimumRequiredVersion << "Found:" << m_jsonRpcVersion;
m_connection->disconnect(); m_connection->disconnect();

View File

@ -65,7 +65,8 @@
#include "types/browseritem.h" #include "types/browseritem.h"
#include "models/logsmodel.h" #include "models/logsmodel.h"
#include "models/logsmodelng.h" #include "models/logsmodelng.h"
#include "models/valuelogsproxymodel.h" #include "models/barseriesadapter.h"
#include "models/xyseriesadapter.h"
#include "models/interfacesproxy.h" #include "models/interfacesproxy.h"
#include "configuration/nymeaconfiguration.h" #include "configuration/nymeaconfiguration.h"
#include "configuration/serverconfiguration.h" #include "configuration/serverconfiguration.h"
@ -92,6 +93,7 @@
#include "ruletemplates/ruleactionparamtemplate.h" #include "ruletemplates/ruleactionparamtemplate.h"
#include "connection/awsclient.h" #include "connection/awsclient.h"
#include "models/devicemodel.h" #include "models/devicemodel.h"
#include "models/sortfilterproxymodel.h"
#include "system/systemcontroller.h" #include "system/systemcontroller.h"
#include "types/package.h" #include "types/package.h"
#include "types/packages.h" #include "types/packages.h"
@ -177,8 +179,11 @@ void registerQmlTypes() {
qmlRegisterType<VendorsProxy>(uri, 1, 0, "VendorsProxy"); qmlRegisterType<VendorsProxy>(uri, 1, 0, "VendorsProxy");
qmlRegisterUncreatableType<Device>(uri, 1, 0, "Device", "Can't create this in QML. Get it from the Devices."); qmlRegisterUncreatableType<Device>(uri, 1, 0, "Device", "Can't create this in QML. Get it from the Devices.");
qmlRegisterUncreatableType<Device>(uri, 1, 0, "Thing", "Can't create this in QML. Get it from the Things.");
qmlRegisterUncreatableType<Devices>(uri, 1, 0, "Devices", "Can't create this in QML. Get it from the DeviceManager."); qmlRegisterUncreatableType<Devices>(uri, 1, 0, "Devices", "Can't create this in QML. Get it from the DeviceManager.");
qmlRegisterUncreatableType<Devices>(uri, 1, 0, "Things", "Can't create this in QML. Get it from the ThingManager.");
qmlRegisterType<DevicesProxy>(uri, 1, 0, "DevicesProxy"); qmlRegisterType<DevicesProxy>(uri, 1, 0, "DevicesProxy");
qmlRegisterType<DevicesProxy>(uri, 1, 0, "ThingsProxy");
qmlRegisterType<InterfacesModel>(uri, 1, 0, "InterfacesModel"); qmlRegisterType<InterfacesModel>(uri, 1, 0, "InterfacesModel");
qmlRegisterType<InterfacesSortModel>(uri, 1, 0, "InterfacesSortModel"); qmlRegisterType<InterfacesSortModel>(uri, 1, 0, "InterfacesSortModel");
@ -242,8 +247,9 @@ void registerQmlTypes() {
qmlRegisterType<LogsModel>(uri, 1, 0, "LogsModel"); qmlRegisterType<LogsModel>(uri, 1, 0, "LogsModel");
qmlRegisterType<LogsModelNg>(uri, 1, 0, "LogsModelNg"); qmlRegisterType<LogsModelNg>(uri, 1, 0, "LogsModelNg");
qmlRegisterType<ValueLogsProxyModel>(uri, 1, 0, "ValueLogsProxyModel");
qmlRegisterUncreatableType<LogEntry>(uri, 1, 0, "LogEntry", "Get them from LogsModel"); qmlRegisterUncreatableType<LogEntry>(uri, 1, 0, "LogEntry", "Get them from LogsModel");
qmlRegisterType<BarSeriesAdapter>(uri, 1, 0, "BarSeriesAdapter");
qmlRegisterType<XYSeriesAdapter>(uri, 1, 0, "XYSeriesAdapter");
qmlRegisterUncreatableType<TagsManager>(uri, 1, 0, "TagsManager", "Get it from Engine"); qmlRegisterUncreatableType<TagsManager>(uri, 1, 0, "TagsManager", "Get it from Engine");
qmlRegisterUncreatableType<Tags>(uri, 1, 0, "Tags", "Get it from TagsManager"); qmlRegisterUncreatableType<Tags>(uri, 1, 0, "Tags", "Get it from TagsManager");
@ -311,6 +317,8 @@ void registerQmlTypes() {
qmlRegisterUncreatableType<IOConnection>(uri, 1, 0, "IOConnection", "Get it from IOConnections"); qmlRegisterUncreatableType<IOConnection>(uri, 1, 0, "IOConnection", "Get it from IOConnections");
qmlRegisterType<IOInputConnectionWatcher>(uri, 1, 0, "IOInputConnectionWatcher"); qmlRegisterType<IOInputConnectionWatcher>(uri, 1, 0, "IOInputConnectionWatcher");
qmlRegisterType<IOOutputConnectionWatcher>(uri, 1, 0, "IOOutputConnectionWatcher"); qmlRegisterType<IOOutputConnectionWatcher>(uri, 1, 0, "IOOutputConnectionWatcher");
qmlRegisterType<SortFilterProxyModel>(uri, 1, 0, "SortFilterProxyModel");
} }
#endif // LIBNYMEAAPPCORE_H #endif // LIBNYMEAAPPCORE_H

View File

@ -27,6 +27,9 @@ INCLUDEPATH += $$top_srcdir/QtZeroConf
SOURCES += \ SOURCES += \
configuration/networkmanager.cpp \ configuration/networkmanager.cpp \
engine.cpp \ engine.cpp \
models/barseriesadapter.cpp \
models/sortfilterproxymodel.cpp \
models/xyseriesadapter.cpp \
ruletemplates/calendaritemtemplate.cpp \ ruletemplates/calendaritemtemplate.cpp \
ruletemplates/timedescriptortemplate.cpp \ ruletemplates/timedescriptortemplate.cpp \
ruletemplates/timeeventitemtemplate.cpp \ ruletemplates/timeeventitemtemplate.cpp \
@ -127,7 +130,6 @@ SOURCES += \
rulemanager.cpp \ rulemanager.cpp \
models/rulesfiltermodel.cpp \ models/rulesfiltermodel.cpp \
models/logsmodel.cpp \ models/logsmodel.cpp \
models/valuelogsproxymodel.cpp \
logmanager.cpp \ logmanager.cpp \
wifisetup/bluetoothdevice.cpp \ wifisetup/bluetoothdevice.cpp \
wifisetup/bluetoothdeviceinfo.cpp \ wifisetup/bluetoothdeviceinfo.cpp \
@ -162,6 +164,9 @@ SOURCES += \
HEADERS += \ HEADERS += \
configuration/networkmanager.h \ configuration/networkmanager.h \
engine.h \ engine.h \
models/barseriesadapter.h \
models/sortfilterproxymodel.h \
models/xyseriesadapter.h \
ruletemplates/calendaritemtemplate.h \ ruletemplates/calendaritemtemplate.h \
ruletemplates/timedescriptortemplate.h \ ruletemplates/timedescriptortemplate.h \
ruletemplates/timeeventitemtemplate.h \ ruletemplates/timeeventitemtemplate.h \
@ -263,7 +268,6 @@ HEADERS += \
rulemanager.h \ rulemanager.h \
models/rulesfiltermodel.h \ models/rulesfiltermodel.h \
models/logsmodel.h \ models/logsmodel.h \
models/valuelogsproxymodel.h \
logmanager.h \ logmanager.h \
wifisetup/bluetoothdevice.h \ wifisetup/bluetoothdevice.h \
wifisetup/bluetoothdeviceinfo.h \ wifisetup/bluetoothdeviceinfo.h \

View File

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

View File

@ -0,0 +1,66 @@
#ifndef BARSERIESADAPTER_H
#define BARSERIESADAPTER_H
#include "logsmodel.h"
#include <QObject>
#include <QBarSeries>
#include <QBarSet>
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<LogEntry*> entries;
qreal value() const;
};
LogsModel *m_logsModel = nullptr;
QtCharts::QAbstractBarSeries *m_barSeries = nullptr;
QtCharts::QBarSet *m_set = nullptr;
Interval m_interval = IntervalMinutes;
QList<TimeSlot> m_timeslots;
};
#endif // BARSERIESADAPTER_H

View File

@ -49,35 +49,35 @@ QVariant DeviceModel::data(const QModelIndex &index, int role) const
return m_list.at(index.row()); return m_list.at(index.row());
} }
if (role == RoleType) { 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) { if (stateType) {
return TypeStateType; 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) { if (actionType) {
return TypeActionType; 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) { if (eventType) {
return TypeEventType; return TypeEventType;
} }
} }
if (role == RoleDisplayName) { 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) { if (stateType) {
return stateType->displayName(); 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) { if (actionType) {
return actionType->displayName(); 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) { if (eventType) {
return eventType->displayName(); return eventType->displayName();
} }
} }
if (role == RoleWritable) { 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 actionType != nullptr;
} }
return QVariant(); return QVariant();
@ -169,22 +169,22 @@ void DeviceModel::updateList()
beginResetModel(); beginResetModel();
m_list.clear(); m_list.clear();
if (m_showStates) { if (m_showStates) {
for (int i = 0; i < m_device->deviceClass()->stateTypes()->rowCount(); i++) { for (int i = 0; i < m_device->thingClass()->stateTypes()->rowCount(); i++) {
m_list.append(m_device->deviceClass()->stateTypes()->get(i)->id()); m_list.append(m_device->thingClass()->stateTypes()->get(i)->id());
} }
} }
if (m_showActions) { if (m_showActions) {
for (int i = 0; i < m_device->deviceClass()->actionTypes()->rowCount(); i++) { for (int i = 0; i < m_device->thingClass()->actionTypes()->rowCount(); i++) {
if (!m_list.contains(m_device->deviceClass()->actionTypes()->get(i)->id())) { if (!m_list.contains(m_device->thingClass()->actionTypes()->get(i)->id())) {
m_list.append(m_device->deviceClass()->actionTypes()->get(i)->id()); m_list.append(m_device->thingClass()->actionTypes()->get(i)->id());
} }
} }
} }
if (m_showEvents) { if (m_showEvents) {
for (int i = 0; i < m_device->deviceClass()->eventTypes()->rowCount(); i++) { for (int i = 0; i < m_device->thingClass()->eventTypes()->rowCount(); i++) {
if (!m_list.contains(m_device->deviceClass()->eventTypes()->get(i)->id())) { if (!m_list.contains(m_device->thingClass()->eventTypes()->get(i)->id())) {
m_list.append(m_device->deviceClass()->eventTypes()->get(i)->id()); m_list.append(m_device->thingClass()->eventTypes()->get(i)->id());
} }
} }
} }

View File

@ -103,11 +103,11 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source
bool found = false; bool found = false;
for (int i = 0; i < m_devicesFilter->rowCount(); i++) { for (int i = 0; i < m_devicesFilter->rowCount(); i++) {
Device *d = m_devicesFilter->get(i); Device *d = m_devicesFilter->get(i);
if (!d->deviceClass()) { if (!d->thingClass()) {
qWarning() << "Cannot find DeviceClass for device:" << d->id() << d->name(); qWarning() << "Cannot find DeviceClass for device:" << d->id() << d->name();
return false; return false;
} }
if (d->deviceClass()->interfaces().contains(interfaceName)) { if (d->thingClass()->interfaces().contains(interfaceName)) {
found = true; found = true;
break; break;
} }
@ -121,11 +121,11 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source
bool found = false; bool found = false;
for (int i = 0; i < m_devicesProxyFilter->rowCount(); i++) { for (int i = 0; i < m_devicesProxyFilter->rowCount(); i++) {
Device *d = m_devicesProxyFilter->get(i); Device *d = m_devicesProxyFilter->get(i);
if (!d->deviceClass()) { if (!d->thingClass()) {
qWarning() << "Cannot find DeviceClass for device:" << d->id() << d->name(); qWarning() << "Cannot find ThingClass for thing:" << d->id() << d->name();
return false; return false;
} }
if (d->deviceClass()->interfaces().contains(interfaceName)) { if (d->thingClass()->interfaces().contains(interfaceName)) {
found = true; found = true;
break; break;
} }

View File

@ -29,14 +29,18 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "logsmodel.h" #include "logsmodel.h"
#include <QDateTime>
#include <QDebug>
#include <QMetaEnum>
#include <QJsonDocument>
#include "engine.h" #include "engine.h"
#include "types/logentry.h"
#include "logmanager.h" #include "logmanager.h"
#include <QMetaEnum>
LogsModel::LogsModel(QObject *parent) : QAbstractListModel(parent) LogsModel::LogsModel(QObject *parent) : QAbstractListModel(parent)
{ {
} }
Engine *LogsModel::engine() const 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 int LogsModel::rowCount(const QModelIndex &parent) const
{ {
Q_UNUSED(parent) Q_UNUSED(parent)
@ -72,6 +71,7 @@ QVariant LogsModel::data(const QModelIndex &index, int role) const
case RoleValue: case RoleValue:
return m_list.at(index.row())->value(); return m_list.at(index.row())->value();
case RoleThingId: case RoleThingId:
case RoleDeviceId:
return m_list.at(index.row())->thingId(); return m_list.at(index.row())->thingId();
case RoleTypeId: case RoleTypeId:
return m_list.at(index.row())->typeId(); return m_list.at(index.row())->typeId();
@ -88,13 +88,19 @@ QHash<int, QByteArray> LogsModel::roleNames() const
QHash<int, QByteArray> roles; QHash<int, QByteArray> roles;
roles.insert(RoleTimestamp, "timestamp"); roles.insert(RoleTimestamp, "timestamp");
roles.insert(RoleValue, "value"); roles.insert(RoleValue, "value");
roles.insert(RoleThingId, "deviceId"); roles.insert(RoleThingId, "thingId");
roles.insert(RoleDeviceId, "deviceId");
roles.insert(RoleTypeId, "typeId"); roles.insert(RoleTypeId, "typeId");
roles.insert(RoleSource, "source"); roles.insert(RoleSource, "source");
roles.insert(RoleLoggingEventType, "loggingEventType"); roles.insert(RoleLoggingEventType, "loggingEventType");
return roles; return roles;
} }
bool LogsModel::busy() const
{
return m_busy;
}
bool LogsModel::live() const bool LogsModel::live() const
{ {
return m_live; 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) { if (m_thingId != thingId) {
m_deviceId = deviceId; m_thingId = thingId;
emit deviceIdChanged(); emit thingIdChanged();
} }
} }
QStringList LogsModel::typeIds() const 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) void LogsModel::setTypeIds(const QStringList &typeIds)
{ {
if (m_typeIds != typeIds) { QList<QUuid> fixedTypeIds;
m_typeIds = typeIds; foreach (const QString &id, typeIds) {
fixedTypeIds.append(QUuid(id));
}
if (m_typeIds != fixedTypeIds) {
m_typeIds = fixedTypeIds;
emit typeIdsChanged(); 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 LogEntry *LogsModel::get(int index) const
{ {
if (index >= 0 && index < m_list.count()) { if (index >= 0 && index < m_list.count()) {
@ -168,163 +205,177 @@ LogEntry *LogsModel::get(int index) const
return nullptr; 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) void LogsModel::logsReply(const QVariantMap &data)
{ {
// qDebug() << "logs reply" << data; int offset = data.value("params").toMap().value("offset").toInt();
beginResetModel(); int count = data.value("params").toMap().value("count").toInt();
qDeleteAll(m_list);
m_list.clear();
// qDebug() << qUtf8Printable(QJsonDocument::fromVariant(data).toJson());
QList<LogEntry*> newBlock;
QList<QVariant> logEntries = data.value("params").toMap().value("logEntries").toList(); QList<QVariant> logEntries = data.value("params").toMap().value("logEntries").toList();
foreach (const QVariant &logEntryVariant, logEntries) { foreach (const QVariant &logEntryVariant, logEntries) {
QVariantMap entryMap = logEntryVariant.toMap(); QVariantMap entryMap = logEntryVariant.toMap();
QDateTime timeStamp = QDateTime::fromMSecsSinceEpoch(entryMap.value("timestamp").toLongLong()); 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(); QString typeId = entryMap.value("typeId").toString();
QMetaEnum sourceEnum = QMetaEnum::fromType<LogEntry::LoggingSource>(); QMetaEnum sourceEnum = QMetaEnum::fromType<LogEntry::LoggingSource>();
LogEntry::LoggingSource loggingSource = (LogEntry::LoggingSource)sourceEnum.keyToValue(entryMap.value("source").toByteArray()); LogEntry::LoggingSource loggingSource = static_cast<LogEntry::LoggingSource>(sourceEnum.keyToValue(entryMap.value("source").toByteArray()));
QMetaEnum loggingEventTypeEnum = QMetaEnum::fromType<LogEntry::LoggingEventType>(); QMetaEnum loggingEventTypeEnum = QMetaEnum::fromType<LogEntry::LoggingEventType>();
LogEntry::LoggingEventType loggingEventType = (LogEntry::LoggingEventType)loggingEventTypeEnum.keyToValue(entryMap.value("eventType").toByteArray()); LogEntry::LoggingEventType loggingEventType = static_cast<LogEntry::LoggingEventType>(loggingEventTypeEnum.keyToValue(entryMap.value("eventType").toByteArray()));
QVariant value = loggingEventType == LogEntry::LoggingEventTypeActiveChange ? entryMap.value("active").toBool() : entryMap.value("value"); QVariant value = loggingEventType == LogEntry::LoggingEventTypeActiveChange ? entryMap.value("active").toBool() : entryMap.value("value");
LogEntry *entry = new LogEntry(timeStamp, value, deviceId, typeId, loggingSource, loggingEventType, this); LogEntry *entry = new LogEntry(timeStamp, value, thingId, typeId, loggingSource, loggingEventType, this);
m_list.append(entry); newBlock.append(entry);
} }
endResetModel(); // qDebug() << "Received logs from" << offset << "to" << offset + count << "Actual count:" << newBlock.count();
emit countChanged();
m_busy = false; if (count < m_blockSize) {
emit busyChanged(); m_canFetchMore = false;
} }
void LogsModel::fetchEarlierReply(const QVariantMap &data) if (newBlock.isEmpty()) {
{ m_busyInternal = false;
// qDebug() << "logs reply" << data; m_busy = false;
emit busyChanged();
QList<QVariant> logEntries = data.value("params").toMap().value("logEntries").toList(); return;
QList<LogEntry*> newEntries; }
foreach (const QVariant &logEntryVariant, logEntries) {
QVariantMap entryMap = logEntryVariant.toMap(); beginInsertRows(QModelIndex(), offset, offset + newBlock.count() - 1);
QDateTime timeStamp = QDateTime::fromMSecsSinceEpoch(entryMap.value("timestamp").toLongLong()); for (int i = 0; i < newBlock.count(); i++) {
QString deviceId = entryMap.value("deviceId").toString(); LogEntry *entry = newBlock.at(i);
QString typeId = entryMap.value("typeId").toString(); m_list.insert(offset + i, entry);
QMetaEnum sourceEnum = QMetaEnum::fromType<LogEntry::LoggingSource>(); emit logEntryAdded(entry);
LogEntry::LoggingSource loggingSource = (LogEntry::LoggingSource)sourceEnum.keyToValue(entryMap.value("source").toByteArray());
QMetaEnum loggingEventTypeEnum = QMetaEnum::fromType<LogEntry::LoggingEventType>();
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);
} }
beginInsertRows(QModelIndex(), 0, newEntries.count() - 1);
newEntries.append(m_list);
m_list = newEntries;
endInsertRows(); endInsertRows();
emit countChanged(); emit countChanged();
m_busy = false; m_busyInternal = false;
emit busyChanged();
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) void LogsModel::newLogEntryReceived(const QVariantMap &data)
{ {
// qDebug() << "***** model NG" << data << m_live;
if (!m_live) { if (!m_live) {
return; return;
} }
QVariantMap entryMap = data; QVariantMap entryMap = data;
QString deviceId = entryMap.value("deviceId").toString(); QUuid thingId;
if (!m_deviceId.isNull() && deviceId != m_deviceId) { 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; return;
} }
QString typeId = entryMap.value("typeId").toString(); QUuid typeId = entryMap.value("typeId").toUuid();
if (!m_typeIds.isEmpty() && !m_typeIds.contains(typeId)) { if (!m_typeIds.isEmpty() && !m_typeIds.contains(typeId)) {
return; return;
} }
beginInsertRows(QModelIndex(), m_list.count(), m_list.count()); beginInsertRows(QModelIndex(), 0, 0);
QDateTime timeStamp = QDateTime::fromMSecsSinceEpoch(entryMap.value("timestamp").toLongLong()); QDateTime timeStamp = QDateTime::fromMSecsSinceEpoch(entryMap.value("timestamp").toLongLong());
QMetaEnum sourceEnum = QMetaEnum::fromType<LogEntry::LoggingSource>(); QMetaEnum sourceEnum = QMetaEnum::fromType<LogEntry::LoggingSource>();
LogEntry::LoggingSource loggingSource = (LogEntry::LoggingSource)sourceEnum.keyToValue(entryMap.value("source").toByteArray()); LogEntry::LoggingSource loggingSource = static_cast<LogEntry::LoggingSource>(sourceEnum.keyToValue(entryMap.value("source").toByteArray()));
QMetaEnum loggingEventTypeEnum = QMetaEnum::fromType<LogEntry::LoggingEventType>(); QMetaEnum loggingEventTypeEnum = QMetaEnum::fromType<LogEntry::LoggingEventType>();
LogEntry::LoggingEventType loggingEventType = (LogEntry::LoggingEventType)loggingEventTypeEnum.keyToValue(entryMap.value("eventType").toByteArray()); LogEntry::LoggingEventType loggingEventType = static_cast<LogEntry::LoggingEventType>(loggingEventTypeEnum.keyToValue(entryMap.value("eventType").toByteArray()));
QVariant value = loggingEventType == LogEntry::LoggingEventTypeActiveChange ? entryMap.value("active").toBool() : entryMap.value("value"); QVariant value = loggingEventType == LogEntry::LoggingEventTypeActiveChange ? entryMap.value("active").toBool() : entryMap.value("value");
LogEntry *entry = new LogEntry(timeStamp, value, deviceId, typeId, loggingSource, loggingEventType, this); LogEntry *entry = new LogEntry(timeStamp, value, thingId, typeId, loggingSource, loggingEventType, this);
m_list.append(entry); m_list.prepend(entry);
endInsertRows(); endInsertRows();
emit countChanged(); emit countChanged();
emit logEntryAdded(entry);
} }

View File

@ -32,26 +32,26 @@
#define LOGSMODEL_H #define LOGSMODEL_H
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QQmlParserStatus>
#include "jsonrpc/jsonhandler.h" #include "jsonrpc/jsonhandler.h"
#include "types/logentry.h" #include "types/logentry.h"
class Engine; class Engine;
class LogsModel : public QAbstractListModel class LogsModel : public QAbstractListModel, public QQmlParserStatus
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged) 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(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(QStringList typeIds READ typeIds WRITE setTypeIds NOTIFY typeIdsChanged)
Q_PROPERTY(QDateTime startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged) Q_PROPERTY(QDateTime startTime READ startTime WRITE setStartTime NOTIFY startTimeChanged)
Q_PROPERTY(QDateTime endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged) Q_PROPERTY(QDateTime endTime READ endTime WRITE setEndTime NOTIFY endTimeChanged)
// Q_PROPERTY(int paginationCount READ paginationCount WRITE setPaginationCount NOTIFY paginationCountChanged) Q_PROPERTY(QDateTime viewStartTime READ viewStartTime WRITE setViewStartTime NOTIFY viewStartTimeChanged)
Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged)
public: public:
enum Roles { enum Roles {
@ -72,12 +72,16 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> 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; bool live() const;
void setLive(bool live); void setLive(bool live);
QString deviceId() const; QUuid thingId() const;
void setDeviceId(const QString &deviceId); void setThingId(const QUuid &deviceId);
QStringList typeIds() const; QStringList typeIds() const;
void setTypeIds(const QStringList &typeIds); void setTypeIds(const QStringList &typeIds);
@ -88,44 +92,45 @@ public:
QDateTime endTime() const; QDateTime endTime() const;
void setEndTime(const QDateTime &endTime); void setEndTime(const QDateTime &endTime);
// int paginationCount() const; QDateTime viewStartTime() const;
// void setPaginationCount(int paginationCount); void setViewStartTime(const QDateTime &viewStartTime);
Q_INVOKABLE LogEntry* get(int index) const; Q_INVOKABLE LogEntry* get(int index) const;
Q_INVOKABLE void notificationReceived(const QVariantMap &data);
signals: signals:
void engineChanged(); void engineChanged();
void busyChanged(); void busyChanged();
void liveChanged(); void liveChanged();
void countChanged(); void countChanged();
void deviceIdChanged(); void thingIdChanged();
void typeIdsChanged(); void typeIdsChanged();
void startTimeChanged(); void startTimeChanged();
void endTimeChanged(); void endTimeChanged();
// void paginationCountChanged(); void viewStartTimeChanged();
public slots: void logEntryAdded(LogEntry *entry);
virtual void update();
virtual void fetchEarlier(int hours);
// virtual void fetchLater(int hours);
private slots: private slots:
virtual void logsReply(const QVariantMap &data); virtual void logsReply(const QVariantMap &data);
virtual void fetchEarlierReply(const QVariantMap &data);
void newLogEntryReceived(const QVariantMap &data); void newLogEntryReceived(const QVariantMap &data);
protected: protected:
Engine *m_engine = nullptr; Engine *m_engine = nullptr;
QList<LogEntry*> m_list; QList<LogEntry*> m_list;
QString m_deviceId; QUuid m_thingId;
QStringList m_typeIds; QList<QUuid> m_typeIds;
QDateTime m_startTime = QDateTime::currentDateTime().addDays(-1); QDateTime m_startTime;
QDateTime m_endTime = QDateTime::currentDateTime(); QDateTime m_endTime;
QDateTime m_viewStartTime;
bool m_busy = false; bool m_busy = false;
bool m_live = false; bool m_live = false;
int m_blockSize = 100;
bool m_busyInternal = false;
bool m_canFetchMore = true;
}; };

View File

@ -278,7 +278,7 @@ void LogsModelNg::logsReply(const QVariantMap &data)
continue; continue;
} }
StateType *entryStateType = dev->deviceClass()->stateTypes()->getStateType(entry->typeId()); StateType *entryStateType = dev->thingClass()->stateTypes()->getStateType(entry->typeId());
if (m_graphSeries) { if (m_graphSeries) {
if (entryStateType->type().toLower() == "bool") { if (entryStateType->type().toLower() == "bool") {
@ -453,9 +453,9 @@ void LogsModelNg::newLogEntryReceived(const QVariantMap &data)
Device *dev = m_engine->thingManager()->devices()->getDevice(entry->thingId()); 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 // 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) { if (m_graphSeries->count() > 1) {
m_graphSeries->removePoints(0, 2); m_graphSeries->removePoints(0, 2);

View File

@ -0,0 +1,62 @@
#include "sortfilterproxymodel.h"
#include <QtDebug>
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);
}

View File

@ -0,0 +1,37 @@
#ifndef SORTFILTERPROXYMODEL_H
#define SORTFILTERPROXYMODEL_H
#include <QSortFilterProxyModel>
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

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "valuelogsproxymodel.h"
#include <QDebug>
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<int, QList<QVariant> > entries;
QList<QVariant> 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<QVariant> 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<QVariant> 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();
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef VALUELOGSPROXYMODEL_H
#define VALUELOGSPROXYMODEL_H
#include <QAbstractListModel>
#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

View File

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

View File

@ -0,0 +1,89 @@
#ifndef XYSERIESADAPTER_H
#define XYSERIESADAPTER_H
#include "logsmodel.h"
#include <QObject>
#include <QXYSeries>
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<LogEntry*> 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<Sample*> m_samples;
QDateTime m_newestSample;
QDateTime m_oldestSample;
qreal m_maxValue = 0;
qreal m_minValue = 0;
};
#endif // XYSERIESADAPTER_H

View File

@ -302,7 +302,7 @@ bool RuleTemplatesFilterModel::filterAcceptsRow(int source_row, const QModelInde
bool found = false; bool found = false;
for (int i = 0; i < m_filterDevicesProxy->rowCount(); i++) { for (int i = 0; i < m_filterDevicesProxy->rowCount(); i++) {
// qDebug() << "Checking device:" << m_filterDevicesProxy->get(i)->deviceClass()->interfaces(); // 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; found = true;
break; break;
} }

View File

@ -169,7 +169,7 @@ void CodeCompletion::update()
if (thingIdExp.exactMatch(blockText)) { if (thingIdExp.exactMatch(blockText)) {
for (int i = 0; i < m_engine->deviceManager()->devices()->rowCount(); i++) { for (int i = 0; i < m_engine->deviceManager()->devices()->rowCount(); i++) {
Device *dev = m_engine->deviceManager()->devices()->get(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: \"")); blockText.remove(QRegExp(".*thingId: \""));
m_model->update(entries); m_model->update(entries);
@ -182,7 +182,7 @@ void CodeCompletion::update()
if (deviceIdExp.exactMatch(blockText)) { if (deviceIdExp.exactMatch(blockText)) {
for (int i = 0; i < m_engine->deviceManager()->devices()->rowCount(); i++) { for (int i = 0; i < m_engine->deviceManager()->devices()->rowCount(); i++) {
Device *dev = m_engine->deviceManager()->devices()->get(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: \"")); blockText.remove(QRegExp(".*deviceId: \""));
m_model->update(entries); m_model->update(entries);
@ -209,8 +209,8 @@ void CodeCompletion::update()
return; return;
} }
for (int i = 0; i < device->deviceClass()->stateTypes()->rowCount(); i++) { for (int i = 0; i < device->thingClass()->stateTypes()->rowCount(); i++) {
StateType *stateType = device->deviceClass()->stateTypes()->get(i); StateType *stateType = device->thingClass()->stateTypes()->get(i);
entries.append(CompletionModel::Entry(stateType->id().toString() + "\" // " + stateType->name(), stateType->name(), "stateType")); entries.append(CompletionModel::Entry(stateType->id().toString() + "\" // " + stateType->name(), stateType->name(), "stateType"));
} }
blockText.remove(QRegExp(".*stateTypeId: \"")); blockText.remove(QRegExp(".*stateTypeId: \""));
@ -241,8 +241,8 @@ void CodeCompletion::update()
} }
qDebug() << "Device is" << device->name(); qDebug() << "Device is" << device->name();
for (int i = 0; i < device->deviceClass()->stateTypes()->rowCount(); i++) { for (int i = 0; i < device->thingClass()->stateTypes()->rowCount(); i++) {
StateType *stateType = device->deviceClass()->stateTypes()->get(i); StateType *stateType = device->thingClass()->stateTypes()->get(i);
entries.append(CompletionModel::Entry(stateType->name() + "\"", stateType->name(), "stateType")); entries.append(CompletionModel::Entry(stateType->name() + "\"", stateType->name(), "stateType"));
} }
blockText.remove(QRegExp(".*stateName: \"")); blockText.remove(QRegExp(".*stateName: \""));
@ -270,8 +270,8 @@ void CodeCompletion::update()
return; return;
} }
for (int i = 0; i < device->deviceClass()->actionTypes()->rowCount(); i++) { for (int i = 0; i < device->thingClass()->actionTypes()->rowCount(); i++) {
ActionType *actionType = device->deviceClass()->actionTypes()->get(i); ActionType *actionType = device->thingClass()->actionTypes()->get(i);
entries.append(CompletionModel::Entry(actionType->id().toString() + "\" // " + actionType->name(), actionType->name(), "actionType")); entries.append(CompletionModel::Entry(actionType->id().toString() + "\" // " + actionType->name(), actionType->name(), "actionType"));
} }
blockText.remove(QRegExp(".*actionTypeId: \"")); blockText.remove(QRegExp(".*actionTypeId: \""));
@ -299,8 +299,8 @@ void CodeCompletion::update()
return; return;
} }
for (int i = 0; i < device->deviceClass()->actionTypes()->rowCount(); i++) { for (int i = 0; i < device->thingClass()->actionTypes()->rowCount(); i++) {
ActionType *actionType = device->deviceClass()->actionTypes()->get(i); ActionType *actionType = device->thingClass()->actionTypes()->get(i);
entries.append(CompletionModel::Entry(actionType->name() + "\"", actionType->name(), "actionType")); entries.append(CompletionModel::Entry(actionType->name() + "\"", actionType->name(), "actionType"));
} }
blockText.remove(QRegExp(".*actionName: \"")); blockText.remove(QRegExp(".*actionName: \""));
@ -328,8 +328,8 @@ void CodeCompletion::update()
return; return;
} }
for (int i = 0; i < device->deviceClass()->eventTypes()->rowCount(); i++) { for (int i = 0; i < device->thingClass()->eventTypes()->rowCount(); i++) {
EventType *eventType = device->deviceClass()->eventTypes()->get(i); EventType *eventType = device->thingClass()->eventTypes()->get(i);
entries.append(CompletionModel::Entry(eventType->id().toString() + "\" // " + eventType->name(), eventType->name(), "eventType")); entries.append(CompletionModel::Entry(eventType->id().toString() + "\" // " + eventType->name(), eventType->name(), "eventType"));
} }
blockText.remove(QRegExp(".*eventTypeId: \"")); blockText.remove(QRegExp(".*eventTypeId: \""));
@ -358,8 +358,8 @@ void CodeCompletion::update()
return; return;
} }
for (int i = 0; i < device->deviceClass()->eventTypes()->rowCount(); i++) { for (int i = 0; i < device->thingClass()->eventTypes()->rowCount(); i++) {
EventType *eventType = device->deviceClass()->eventTypes()->get(i); EventType *eventType = device->thingClass()->eventTypes()->get(i);
entries.append(CompletionModel::Entry(eventType->name() + "\"", eventType->name(), "eventType")); entries.append(CompletionModel::Entry(eventType->name() + "\"", eventType->name(), "eventType"));
} }
blockText.remove(QRegExp(".*eventName: \"")); blockText.remove(QRegExp(".*eventName: \""));

View File

@ -389,9 +389,15 @@ void SystemController::notificationReceived(const QVariantMap &data)
emit powerManagementAvailableChanged(); emit powerManagementAvailableChanged();
emit updateManagementAvailableChanged(); emit updateManagementAvailableChanged();
} else if (notification == "System.TimeConfigurationChanged") { } 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 = 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 serverTimeChanged();
emit serverTimeZoneChanged(); emit serverTimeZoneChanged();
m_automaticTimeAvailable = data.value("params").toMap().value("automaticTimeAvailable").toBool(); m_automaticTimeAvailable = data.value("params").toMap().value("automaticTimeAvailable").toBool();

View File

@ -82,7 +82,7 @@ int ThingGroup::executeAction(const QString &actionName, const QVariantList &par
if (device->setupStatus() != Device::DeviceSetupStatusComplete) { if (device->setupStatus() != Device::DeviceSetupStatusComplete) {
continue; continue;
} }
ActionType *actionType = device->deviceClass()->actionTypes()->findByName(actionName); ActionType *actionType = device->thingClass()->actionTypes()->findByName(actionName);
if (!actionType) { if (!actionType) {
continue; continue;
} }
@ -110,8 +110,8 @@ int ThingGroup::executeAction(const QString &actionName, const QVariantList &par
void ThingGroup::syncStates() void ThingGroup::syncStates()
{ {
for (int i = 0; i < deviceClass()->stateTypes()->rowCount(); i++) { for (int i = 0; i < thingClass()->stateTypes()->rowCount(); i++) {
StateType *stateType = deviceClass()->stateTypes()->get(i); StateType *stateType = thingClass()->stateTypes()->get(i);
State *state = states()->getState(stateType->id()); State *state = states()->getState(stateType->id());
qDebug() << "syncing state" << stateType->name(); qDebug() << "syncing state" << stateType->name();
@ -121,13 +121,13 @@ void ThingGroup::syncStates()
for (int j = 0; j < m_devices->rowCount(); j++) { for (int j = 0; j < m_devices->rowCount(); j++) {
Device *d = m_devices->get(j); Device *d = m_devices->get(j);
// Skip things that don't have the required state // 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) { if (!ds) {
continue; continue;
} }
// Skip disconnected things // Skip disconnected things
StateType *connectedStateType = d->deviceClass()->stateTypes()->findByName("connected"); StateType *connectedStateType = d->thingClass()->stateTypes()->findByName("connected");
if (connectedStateType) { if (connectedStateType) {
if (!d->stateValue(connectedStateType->id()).toBool()) { if (!d->stateValue(connectedStateType->id()).toBool()) {
continue; continue;

View File

@ -34,11 +34,11 @@
#include <QDebug> #include <QDebug>
Device::Device(DeviceManager *deviceManager, DeviceClass *deviceClass, const QUuid &parentDeviceId, QObject *parent) : Device::Device(DeviceManager *deviceManager, DeviceClass *thingClass, const QUuid &parentId, QObject *parent) :
QObject(parent), QObject(parent),
m_deviceManager(deviceManager), m_deviceManager(deviceManager),
m_parentDeviceId(parentDeviceId), m_parentId(parentId),
m_deviceClass(deviceClass) m_thingClass(thingClass)
{ {
} }
@ -65,22 +65,22 @@ void Device::setId(const QUuid &id)
QUuid Device::deviceClassId() const QUuid Device::deviceClassId() const
{ {
return m_deviceClass->id(); return m_thingClass->id();
} }
QUuid Device::thingClassId() const QUuid Device::thingClassId() const
{ {
return m_deviceClass->id(); return m_thingClass->id();
} }
QUuid Device::parentDeviceId() const QUuid Device::parentDeviceId() const
{ {
return m_parentDeviceId; return m_parentId;
} }
bool Device::isChild() const bool Device::isChild() const
{ {
return !m_parentDeviceId.isNull(); return !m_parentId.isNull();
} }
Device::DeviceSetupStatus Device::setupStatus() const 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 DeviceClass *Device::thingClass() const
{ {
return m_deviceClass; return m_thingClass;
} }
bool Device::hasState(const QUuid &stateTypeId) 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 &params) int Device::executeAction(const QString &actionName, const QVariantList &params)
{ {
ActionType *actionType = m_deviceClass->actionTypes()->findByName(actionName); ActionType *actionType = m_thingClass->actionTypes()->findByName(actionName);
QVariantList finalParams; QVariantList finalParams;
foreach (const QVariant &paramVariant, params) { foreach (const QVariant &paramVariant, params) {
@ -209,30 +218,30 @@ int Device::executeAction(const QString &actionName, const QVariantList &params)
return m_deviceManager->executeAction(m_id, actionType->id(), finalParams); 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; dbg.nospace() << "Thing: " << thing->name() << " (" << thing->id().toString() << ") Class:" << thing->thingClass()->name() << " (" << thing->thingClassId().toString() << ")" << endl;
for (int i = 0; i < device->deviceClass()->paramTypes()->rowCount(); i++) { for (int i = 0; i < thing->thingClass()->paramTypes()->rowCount(); i++) {
ParamType *pt = device->deviceClass()->paramTypes()->get(i); ParamType *pt = thing->thingClass()->paramTypes()->get(i);
Param *p = device->params()->getParam(pt->id().toString()); Param *p = thing->params()->getParam(pt->id().toString());
if (p) { if (p) {
dbg << " Param " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << p->value() << endl; dbg << " Param " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << p->value() << endl;
} else { } else {
dbg << " Param " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << "*** Unknown value ***" << endl; dbg << " Param " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << "*** Unknown value ***" << endl;
} }
} }
for (int i = 0; i < device->deviceClass()->settingsTypes()->rowCount(); i++) { for (int i = 0; i < thing->thingClass()->settingsTypes()->rowCount(); i++) {
ParamType *pt = device->deviceClass()->settingsTypes()->get(i); ParamType *pt = thing->thingClass()->settingsTypes()->get(i);
Param *p = device->settings()->getParam(pt->id().toString()); Param *p = thing->settings()->getParam(pt->id().toString());
if (p) { if (p) {
dbg << " Setting " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << p->value() << endl; dbg << " Setting " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << p->value() << endl;
} else { } else {
dbg << " Setting " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << "*** Unknown value ***" << endl; dbg << " Setting " << i << ": " << pt->id().toString() << ": " << pt->name() << " = " << "*** Unknown value ***" << endl;
} }
} }
for (int i = 0; i < device->deviceClass()->stateTypes()->rowCount(); i++) { for (int i = 0; i < thing->thingClass()->stateTypes()->rowCount(); i++) {
StateType *st = device->deviceClass()->stateTypes()->get(i); StateType *st = thing->thingClass()->stateTypes()->get(i);
State *s = device->states()->getState(st->id()); State *s = thing->states()->getState(st->id());
dbg << " State " << i << ": " << st->id() << ": " << st->name() << " = " << s->value() << endl; dbg << " State " << i << ": " << st->id() << ": " << st->name() << " = " << s->value() << endl;
} }
return dbg; return dbg;

View File

@ -55,7 +55,7 @@ class Device : public QObject
Q_PROPERTY(Params *params READ params NOTIFY paramsChanged) Q_PROPERTY(Params *params READ params NOTIFY paramsChanged)
Q_PROPERTY(Params *settings READ settings NOTIFY settingsChanged) Q_PROPERTY(Params *settings READ settings NOTIFY settingsChanged)
Q_PROPERTY(States *states READ states NOTIFY statesChanged) 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) Q_PROPERTY(DeviceClass *thingClass READ thingClass CONSTANT)
public: public:
@ -67,7 +67,7 @@ public:
}; };
Q_ENUM(DeviceSetupStatus) 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; QUuid id() const;
void setId(const QUuid &id); void setId(const QUuid &id);
@ -93,10 +93,11 @@ public:
States *states() const; States *states() const;
void setStates(States *states); void setStates(States *states);
DeviceClass *deviceClass() const;
DeviceClass *thingClass() const; DeviceClass *thingClass() const;
Q_INVOKABLE bool hasState(const QUuid &stateTypeId); 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); Q_INVOKABLE QVariant stateValue(const QUuid &stateTypeId);
void setStateValue(const QUuid &stateTypeId, const QVariant &value); void setStateValue(const QUuid &stateTypeId, const QVariant &value);
@ -117,15 +118,15 @@ protected:
DeviceManager *m_deviceManager = nullptr; DeviceManager *m_deviceManager = nullptr;
QString m_name; QString m_name;
QUuid m_id; QUuid m_id;
QUuid m_parentDeviceId; QUuid m_parentId;
DeviceSetupStatus m_setupStatus = DeviceSetupStatusNone; DeviceSetupStatus m_setupStatus = DeviceSetupStatusNone;
QString m_setupDisplayMessage; QString m_setupDisplayMessage;
Params *m_params = nullptr; Params *m_params = nullptr;
Params *m_settings = nullptr; Params *m_settings = nullptr;
States *m_states = 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 #endif // DEVICE_H

View File

@ -99,7 +99,7 @@
<file>ui/images/media-seek-forward.svg</file> <file>ui/images/media-seek-forward.svg</file>
<file>ui/images/media-skip-backward.svg</file> <file>ui/images/media-skip-backward.svg</file>
<file>ui/images/media-skip-forward.svg</file> <file>ui/images/media-skip-forward.svg</file>
<file>ui/images/mediaplayer-app-symbolic.svg</file> <file>ui/images/media.svg</file>
<file>ui/images/navigation-menu.svg</file> <file>ui/images/navigation-menu.svg</file>
<file>ui/images/network-secure.svg</file> <file>ui/images/network-secure.svg</file>
<file>ui/images/network-vpn.svg</file> <file>ui/images/network-vpn.svg</file>
@ -129,7 +129,7 @@
<file>ui/images/send.svg</file> <file>ui/images/send.svg</file>
<file>ui/images/sensors.svg</file> <file>ui/images/sensors.svg</file>
<file>ui/images/settings.svg</file> <file>ui/images/settings.svg</file>
<file>ui/images/share.svg</file> <file>ui/images/things.svg</file>
<file>ui/images/slideshow.svg</file> <file>ui/images/slideshow.svg</file>
<file>ui/images/closable-move.svg</file> <file>ui/images/closable-move.svg</file>
<file>ui/images/starred.svg</file> <file>ui/images/starred.svg</file>
@ -234,5 +234,6 @@
<file>ui/images/garage/garage-100.svg</file> <file>ui/images/garage/garage-100.svg</file>
<file>ui/images/navigationpad.svg</file> <file>ui/images/navigationpad.svg</file>
<file>ui/images/qrcode.svg</file> <file>ui/images/qrcode.svg</file>
<file>ui/images/energy.svg</file>
</qresource> </qresource>
</RCC> </RCC>

View File

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

24
nymea-app/mainmenumodel.h Normal file
View File

@ -0,0 +1,24 @@
#ifndef MAINMENUMODEL_H
#define MAINMENUMODEL_H
#include <QAbstractListModel>
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<MainMenuItem*> m_list;
};
#endif // MAINMENUMODEL_H

View File

@ -13,6 +13,7 @@ linux:!android:!nozeroconf:LIBS += -lavahi-client -lavahi-common
PRE_TARGETDEPS += ../libnymea-app PRE_TARGETDEPS += ../libnymea-app
HEADERS += \ HEADERS += \
mainmenumodel.h \
platformintegration/generic/raspberrypihelper.h \ platformintegration/generic/raspberrypihelper.h \
stylecontroller.h \ stylecontroller.h \
pushnotifications.h \ pushnotifications.h \
@ -22,6 +23,7 @@ HEADERS += \
ruletemplates/messages.h ruletemplates/messages.h
SOURCES += main.cpp \ SOURCES += main.cpp \
mainmenumodel.cpp \
platformintegration/generic/raspberrypihelper.cpp \ platformintegration/generic/raspberrypihelper.cpp \
stylecontroller.cpp \ stylecontroller.cpp \
pushnotifications.cpp \ pushnotifications.cpp \

View File

@ -10,8 +10,7 @@
<file>ui/RootItem.qml</file> <file>ui/RootItem.qml</file>
<file>ui/mainviews/ScenesView.qml</file> <file>ui/mainviews/ScenesView.qml</file>
<file>ui/mainviews/FavoritesView.qml</file> <file>ui/mainviews/FavoritesView.qml</file>
<file>ui/mainviews/DevicesPageDelegate.qml</file> <file>ui/mainviews/ThingsView.qml</file>
<file>ui/mainviews/DevicesPage.qml</file>
<file>ui/connection/ConnectPage.qml</file> <file>ui/connection/ConnectPage.qml</file>
<file>ui/connection/ManualConnectPage.qml</file> <file>ui/connection/ManualConnectPage.qml</file>
<file>ui/connection/ConnectingPage.qml</file> <file>ui/connection/ConnectingPage.qml</file>
@ -51,18 +50,13 @@
<file>ui/customviews/CustomViewBase.qml</file> <file>ui/customviews/CustomViewBase.qml</file>
<file>ui/customviews/WeatherView.qml</file> <file>ui/customviews/WeatherView.qml</file>
<file>ui/customviews/MediaControllerView.qml</file> <file>ui/customviews/MediaControllerView.qml</file>
<file>ui/customviews/SensorView.qml</file>
<file>ui/customviews/NotificationsView.qml</file> <file>ui/customviews/NotificationsView.qml</file>
<file>ui/customviews/ExtendedVolumeController.qml</file> <file>ui/customviews/ExtendedVolumeController.qml</file>
<file>ui/devicepages/MediaDevicePage.qml</file> <file>ui/devicepages/MediaDevicePage.qml</file>
<file>ui/devicepages/ButtonDevicePage.qml</file> <file>ui/devicepages/ButtonDevicePage.qml</file>
<file>ui/devicepages/GenericDevicePage.qml</file> <file>ui/devicepages/GenericDevicePage.qml</file>
<file>ui/devicepages/WeatherDevicePagePre110.qml</file>
<file>ui/devicepages/WeatherDevicePagePost110.qml</file>
<file>ui/devicepages/WeatherDevicePage.qml</file> <file>ui/devicepages/WeatherDevicePage.qml</file>
<file>ui/devicepages/SensorDevicePage.qml</file> <file>ui/devicepages/SensorDevicePage.qml</file>
<file>ui/devicepages/SensorDevicePagePre110.qml</file>
<file>ui/devicepages/SensorDevicePagePost110.qml</file>
<file>ui/devicepages/DevicePageBase.qml</file> <file>ui/devicepages/DevicePageBase.qml</file>
<file>ui/devicepages/InputTriggerDevicePage.qml</file> <file>ui/devicepages/InputTriggerDevicePage.qml</file>
<file>ui/devicepages/StateLogPage.qml</file> <file>ui/devicepages/StateLogPage.qml</file>
@ -108,6 +102,7 @@
<file>ui/delegates/ParamDelegate.qml</file> <file>ui/delegates/ParamDelegate.qml</file>
<file>ui/delegates/ActionDelegate.qml</file> <file>ui/delegates/ActionDelegate.qml</file>
<file>ui/delegates/ThingDelegate.qml</file> <file>ui/delegates/ThingDelegate.qml</file>
<file>ui/delegates/InterfaceTile.qml</file>
<file>ui/system/LogViewerPage.qml</file> <file>ui/system/LogViewerPage.qml</file>
<file>ui/system/PluginsPage.qml</file> <file>ui/system/PluginsPage.qml</file>
<file>ui/system/PluginParamsPage.qml</file> <file>ui/system/PluginParamsPage.qml</file>
@ -134,7 +129,6 @@
<file>../LICENSE.OFL</file> <file>../LICENSE.OFL</file>
<file>../LICENSE.OpenSSL</file> <file>../LICENSE.OpenSSL</file>
<file>../LICENSE.LGPL3</file> <file>../LICENSE.LGPL3</file>
<file>ui/customviews/GenericTypeGraphPre110.qml</file>
<file>ui/customviews/GenericTypeGraph.qml</file> <file>ui/customviews/GenericTypeGraph.qml</file>
<file>ui/devicepages/SmartMeterDevicePage.qml</file> <file>ui/devicepages/SmartMeterDevicePage.qml</file>
<file>ui/devicelistpages/SmartMeterDeviceListPage.qml</file> <file>ui/devicelistpages/SmartMeterDeviceListPage.qml</file>
@ -165,7 +159,6 @@
<file>ui/thingconfiguration/EditThingsPage.qml</file> <file>ui/thingconfiguration/EditThingsPage.qml</file>
<file>ui/thingconfiguration/ConfigureThingPage.qml</file> <file>ui/thingconfiguration/ConfigureThingPage.qml</file>
<file>ui/connection/CertificateDialog.qml</file> <file>ui/connection/CertificateDialog.qml</file>
<file>ui/experiences/garagegates/Main.qml</file>
<file>ui/experiences/heating/Main.qml</file> <file>ui/experiences/heating/Main.qml</file>
<file>ui/fonts/Oswald-Bold.ttf</file> <file>ui/fonts/Oswald-Bold.ttf</file>
<file>ui/fonts/Oswald-ExtraLight.ttf</file> <file>ui/fonts/Oswald-ExtraLight.ttf</file>
@ -218,5 +211,12 @@
<file>ui/thingconfiguration/ThingClassDetailsPage.qml</file> <file>ui/thingconfiguration/ThingClassDetailsPage.qml</file>
<file>ui/components/ClosablesControlLarge.qml</file> <file>ui/components/ClosablesControlLarge.qml</file>
<file>ui/devicepages/BarcodeScannerThingPage.qml</file> <file>ui/devicepages/BarcodeScannerThingPage.qml</file>
<file>ui/mainviews/GaragesView.qml</file>
<file>ui/mainviews/EnergyView.qml</file>
<file>ui/components/MainViewBase.qml</file>
<file>ui/components/SmartMeterChart.qml</file>
<file>ui/mainviews/MediaView.qml</file>
<file>ui/components/ShuffleRepeatVolumeControl.qml</file>
<file>ui/components/MediaBrowser.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -75,28 +75,6 @@ QStringList StyleController::allStyles() const
return dir.entryList(QDir::Dirs); return dir.entryList(QDir::Dirs);
} }
QString StyleController::currentExperience() const
{
QSettings settings;
return settings.value("experience", "Default").toString();
}
void StyleController::setCurrentExperience(const QString &currentExperience)
{
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) void StyleController::setSystemFont(const QFont &font)
{ {
QApplication::setFont(font); QApplication::setFont(font);

View File

@ -39,9 +39,6 @@ class StyleController : public QObject
Q_PROPERTY(QString currentStyle READ currentStyle WRITE setCurrentStyle NOTIFY currentStyleChanged) Q_PROPERTY(QString currentStyle READ currentStyle WRITE setCurrentStyle NOTIFY currentStyleChanged)
Q_PROPERTY(QStringList allStyles READ allStyles CONSTANT) 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: public:
explicit StyleController(QObject *parent = nullptr); explicit StyleController(QObject *parent = nullptr);
@ -50,17 +47,10 @@ public:
QStringList allStyles() const; QStringList allStyles() const;
QString currentExperience() const;
void setCurrentExperience(const QString &currentExperience);
QStringList allExperiences() const;
Q_INVOKABLE void setSystemFont(const QFont &font); Q_INVOKABLE void setSystemFont(const QFont &font);
signals: signals:
void currentStyleChanged(); void currentStyleChanged();
void currentExperienceChanged();
}; };
#endif // STYLECONTROLLER_H #endif // STYLECONTROLLER_H

View File

@ -33,6 +33,8 @@ import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.1 import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtQuick.Window 2.3 import QtQuick.Window 2.3
import Qt.labs.settings 1.0
import Qt.labs.folderlistmodel 2.2
import Nymea 1.0 import Nymea 1.0
import "components" import "components"
import "delegates" import "delegates"
@ -42,7 +44,8 @@ Page {
id: root id: root
header: FancyHeader { header: FancyHeader {
title: swipeView.currentItem.title id: mainHeader
title: filteredContentModel.data(swipeView.currentIndex, "displayName")
leftButtonVisible: true leftButtonVisible: true
leftButtonImageSource: { leftButtonImageSource: {
switch (engine.jsonRpcClient.currentConnection.bearerType) { switch (engine.jsonRpcClient.currentConnection.bearerType) {
@ -65,10 +68,14 @@ Page {
var dialog = connectionDialogComponent.createObject(root) var dialog = connectionDialogComponent.createObject(root)
dialog.open(); dialog.open();
} }
onMenuOpenChanged: {
if (menuOpen && d.configOverlay) {
d.configOverlay.destroy()
}
}
model: ListModel { 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/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/stock_application.svg"; text: qsTr("App settings"); page: "appsettings/AppSettingsPage.qml" }
ListElement { iconSource: "../images/settings.svg"; text: qsTr("System settings"); page: "SettingsPage.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 { Connections {
target: engine.ruleManager target: engine.ruleManager
onAddRuleReply: { onAddRuleReply: {
@ -170,6 +99,70 @@ Page {
QtObject { QtObject {
id: d id: d
property var editRulePage: null 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 { ColumnLayout {
@ -226,8 +219,8 @@ Page {
} }
} }
Item { Item {
id: contentContainer
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
clip: true clip: true
@ -235,131 +228,17 @@ Page {
SwipeView { SwipeView {
id: swipeView id: swipeView
anchors.fill: parent anchors.fill: parent
currentIndex: root.currentViewIndex opacity: d.configOverlay === null ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
onCurrentIndexChanged: { Repeater {
root.currentViewIndex = currentIndex model: d.configOverlay != null ? null : filteredContentModel
}
Component { delegate: Loader {
id: experienceViewComponent
Loader {
width: swipeView.width width: swipeView.width
height: swipeView.height height: swipeView.height
clip: true clip: true
readonly property string title: item ? item.title : "" source: "mainviews/" + model.source + ".qml"
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"))
} }
} }
} }
@ -383,19 +262,300 @@ Page {
} }
} }
footer: TabBar { footer: Item {
id: tabBar readonly property bool shown: tabsRepeater.count > 1 || mainHeader.menuOpen || d.configOverlay
Material.elevation: 3 implicitHeight: shown ? 70 + (app.landscape ? -20 : 0) : 0
position: TabBar.Footer Behavior on implicitHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }}
implicitHeight: 70 + (app.landscape ? -20 : 0) clip: true
Component { TabBar {
id: tabEntryComponent id: tabBar
MainPageTabButton { anchors.fill: parent
property int pageIndex: 0 Material.elevation: 3
// height: tabBar.height position: TabBar.Footer
onClicked: root.currentViewIndex = pageIndex
alignment: app.landscape ? Qt.Horizontal : Qt.Vertical 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 font.pixelSize: app.smallFont
elide: Text.ElideRight elide: Text.ElideRight
color: Material.color(Material.Grey) color: Material.color(Material.Grey)
// horizontalAlignment: Text.AlignHCenter // horizontalAlignment: Text.AlignHCenter
} }
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
@ -461,7 +621,7 @@ Page {
font.pixelSize: app.smallFont font.pixelSize: app.smallFont
elide: Text.ElideRight elide: Text.ElideRight
color: Material.color(Material.Grey) color: Material.color(Material.Grey)
// horizontalAlignment: Text.AlignHCenter // horizontalAlignment: Text.AlignHCenter
} }
} }
ColorIcon { ColorIcon {

View File

@ -33,6 +33,7 @@ import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.1 import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import Qt.labs.settings 1.0 import Qt.labs.settings 1.0
import Qt.labs.folderlistmodel 2.2
import QtQuick.Window 2.3 import QtQuick.Window 2.3
import Nymea 1.0 import Nymea 1.0
@ -258,7 +259,7 @@ ApplicationWindow {
case "mediacontroller": case "mediacontroller":
case "extendedmediacontroller": case "extendedmediacontroller":
case "mediaplayer": case "mediaplayer":
return Qt.resolvedUrl("images/mediaplayer-app-symbolic.svg") return Qt.resolvedUrl("images/media.svg")
case "powersocket": case "powersocket":
return Qt.resolvedUrl("images/powersocket.svg") return Qt.resolvedUrl("images/powersocket.svg")
case "button": case "button":
@ -533,6 +534,11 @@ ApplicationWindow {
onStateChanged: closeTimer.stop() onStateChanged: closeTimer.stop()
} }
FolderListModel {
id: availableMainViews
folder: "mainviews"
showFiles: false
}
// NOTE: If using a Dialog, make sure closePolicy does not contain Dialog.CloseOnPressOutside // NOTE: If using a Dialog, make sure closePolicy does not contain Dialog.CloseOnPressOutside
// or the virtual keyboard will close when pressing it... // or the virtual keyboard will close when pressing it...

View File

@ -120,25 +120,6 @@ SettingsPageBase {
onClicked: settings.showConnectionTabs = checked 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 { SettingsPageSectionHeader {
text: qsTr("Regional") text: qsTr("Regional")
} }

View File

@ -48,7 +48,10 @@ Item {
id: image id: image
anchors.fill: parent anchors.fill: parent
anchors.margins: parent ? parent.margins : 0 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 { sourceSize {
width: width width: width
height: height height: height

View File

@ -35,7 +35,7 @@ import QtQuick.Controls.Material 2.1
ToolBar { ToolBar {
id: root 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 } } Behavior on height { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
property string title property string title
@ -47,16 +47,13 @@ ToolBar {
signal clicked(int index); signal clicked(int index);
signal leftButtonClicked(); signal leftButtonClicked();
QtObject { property bool menuOpen: false
id: d
property bool menuOpen: false
}
RowLayout { RowLayout {
id: mainRow id: mainRow
height: 50 height: 50
width: parent.width width: parent.width
opacity: d.menuOpen ? 0 : 1 opacity: menuOpen ? 0 : 1
Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } } Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
HeaderButton { HeaderButton {
@ -81,7 +78,7 @@ ToolBar {
HeaderButton { HeaderButton {
id: menuButton id: menuButton
imageSource: "../images/navigation-menu.svg" imageSource: "../images/navigation-menu.svg"
onClicked: d.menuOpen = true onClicked: menuOpen = true
} }
} }
@ -89,7 +86,7 @@ ToolBar {
height: 50 height: 50
anchors.bottom: menuPanel.top anchors.bottom: menuPanel.top
width: parent.width width: parent.width
opacity: d.menuOpen ? 1 : 0 opacity: menuOpen ? 1 : 0
visible: opacity > 0 visible: opacity > 0
Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } } Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
@ -106,7 +103,7 @@ ToolBar {
HeaderButton { HeaderButton {
imageSource:"../images/close.svg" imageSource:"../images/close.svg"
onClicked: d.menuOpen = false onClicked: menuOpen = false
} }
} }
@ -118,7 +115,7 @@ ToolBar {
width: Math.min(menuRow.childrenRect.width, parent.width) width: Math.min(menuRow.childrenRect.width, parent.width)
height: app.iconSize * 3 height: app.iconSize * 3
contentWidth: menuRow.childrenRect.width contentWidth: menuRow.childrenRect.width
opacity: d.menuOpen ? 1 : 0 opacity: menuOpen ? 1 : 0
visible: opacity > 0 visible: opacity > 0
Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } } Behavior on opacity { NumberAnimation { easing.type: Easing.InOutQuad; duration: 200 } }
@ -132,7 +129,7 @@ ToolBar {
width: app.iconSize * 3 width: app.iconSize * 3
onClicked: { onClicked: {
d.menuOpen = false menuOpen = false
root.clicked(index) root.clicked(index)
} }

View File

@ -44,22 +44,27 @@ TabButton {
opacity: 0.05 opacity: 0.05
} }
contentItem: GridLayout { contentItem: Item {
columns: root.alignment === Qt.Vertical ? 1 : 2 height: root.height
rowSpacing: 4 Grid {
ColorIcon { anchors.centerIn: parent
Layout.preferredWidth: app.iconSize columns: root.alignment == Qt.Vertical ? 1 : 2
Layout.preferredHeight: app.iconSize spacing: root.alignment == Qt.Horizontal ? app.margins : app.margins / 2
Layout.alignment: Qt.AlignHCenter horizontalItemAlignment: Grid.AlignHCenter
name: root.iconSource verticalItemAlignment: Grid.AlignVCenter
color: root.checked ? app.accentColor : keyColor
} ColorIcon {
Label { width: app.iconSize
Layout.fillWidth: root.alignment === Qt.Vertical height: app.iconSize
text: root.text name: root.iconSource
horizontalAlignment: Text.AlignHCenter color: root.checked ? app.accentColor : keyColor
font.pixelSize: app.smallFont }
color: root.checked ? app.accentColor : Material.foreground Label {
id: textLabel
text: root.text
font.pixelSize: app.smallFont
color: root.checked ? app.accentColor : Material.foreground
}
} }
} }
} }

View File

@ -34,30 +34,13 @@ import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import Nymea 1.0 import Nymea 1.0
import "../components" import "../components"
import "../delegates"
MouseArea { MouseArea {
id: root 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 // Prevent scroll events to swipe left/right in case they fall through the grid
preventStealing: true preventStealing: true
onWheel: wheel.accepted = 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)
}
}
} }

View File

@ -36,13 +36,13 @@ import Nymea 1.0
Item { Item {
id: root id: root
property Device device: null property Thing thing: null
readonly property StateType artworkStateType: device ? device.deviceClass.stateTypes.findByName("artwork") : null readonly property StateType artworkStateType: thing ? thing.thingClass.stateTypes.findByName("artwork") : null
readonly property State artworkState: artworkStateType ? device.states.getState(artworkStateType.id) : 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 StateType playerTypeStateType: thing ? thing.thingClass.stateTypes.findByName("playerType") : null
readonly property State playerTypeState: playerTypeStateType ? device.states.getState(playerTypeStateType.id) : null readonly property State playerTypeState: playerTypeStateType ? thing.states.getState(playerTypeStateType.id) : null
Pane { Pane {
Material.elevation: 2 Material.elevation: 2

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.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
}
}
}
}
}

View File

@ -38,15 +38,18 @@ RowLayout {
id: root id: root
implicitHeight: iconSize + app.margins implicitHeight: iconSize + app.margins
property Device device: null property Thing thing: null
property int iconSize: app.iconSize * 1.5 property int iconSize: app.iconSize * 1.5
readonly property StateType playbackStateType: device ? device.deviceClass.stateTypes.findByName("playbackStatus") : null readonly property StateType playbackStateType: thing ? thing.thingClass.stateTypes.findByName("playbackStatus") : null
readonly property State playbackState: playbackStateType ? device.states.getState(playbackStateType.id) : null readonly property State playbackState: playbackStateType ? thing.states.getState(playbackStateType.id) : null
function executeAction(actionName, params) { function executeAction(actionName, params) {
var actionTypeId = device.deviceClass.actionTypes.findByName(actionName).id; if (params === undefined) {
engine.deviceManager.executeAction(device.id, actionTypeId, params) params = []
}
var actionTypeId = thing.thingClass.actionTypes.findByName(actionName).id;
engine.thingManager.executeAction(thing.id, actionTypeId, params)
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
@ -68,7 +71,7 @@ RowLayout {
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
ProgressButton { ProgressButton {
Layout.preferredHeight: root,iconSize Layout.preferredHeight: root.iconSize
Layout.preferredWidth: height Layout.preferredWidth: height
imageSource: root.playbackState && root.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg" imageSource: root.playbackState && root.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg"
longpressImageSource: "../images/media-playback-stop.svg" longpressImageSource: "../images/media-playback-stop.svg"

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.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}]);
}
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.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
}
}
}

View File

@ -36,6 +36,8 @@ Item {
implicitHeight: slider.implicitHeight implicitHeight: slider.implicitHeight
implicitWidth: slider.implicitWidth implicitWidth: slider.implicitWidth
property alias orientation: slider.orientation
property real value: 0 property real value: 0
property alias from: slider.from property alias from: slider.from
property alias to: slider.to property alias to: slider.to
@ -49,6 +51,7 @@ Item {
Slider { Slider {
id: slider id: slider
anchors.left: parent.left; anchors.right: parent.right anchors.left: parent.left; anchors.right: parent.right
anchors.top: parent.top; anchors.bottom: parent.bottom
from: 0 from: 0
to: 100 to: 100
property var lastSentTime: new Date() property var lastSentTime: new Date()

View File

@ -143,8 +143,8 @@ Item {
} }
} }
min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05)) min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05))
onMinChanged: print("min set to", min) onMinChanged: applyNiceNumbers();
onMaxChanged: print("max set to", min) onMaxChanged: applyNiceNumbers();
labelsFont.pixelSize: app.smallFont labelsFont.pixelSize: app.smallFont
labelFormat: { labelFormat: {
switch (root.stateType.type.toLowerCase()) { switch (root.stateType.type.toLowerCase()) {

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import QtQuick.Layouts 1.1
import Nymea 1.0
import "../components"
import "../customviews"
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
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.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)
}
}
}

View File

@ -161,7 +161,7 @@ MainPageTile {
case "media": case "media":
return mediaControlComponent return mediaControlComponent
default: default:
console.warn("DevicesPageDelegate, inlineControl: Unhandled interface", iface.name) console.warn("InterfaceTile, inlineControl: Unhandled interface", iface.name)
} }
} }
@ -227,7 +227,7 @@ MainPageTile {
MediaControls { MediaControls {
iconSize: app.iconSize * 1.2 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) { if (thing.thingClass.interfaces.indexOf("statefulgaragedoor") >= 0 || thing.thingClass.interfaces.indexOf("extendedstatefulgaragedoor") >= 0 || thing.thingClass.interfaces.indexOf("garagegate") >= 0) {
statefulCount++; statefulCount++;
var stateType = thing.thingClass.stateTypes.findByName("state"); 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++; count++;
} }
} }
@ -285,7 +285,7 @@ MainPageTile {
return "" return ""
// return qsTr("%1 installed").arg(devicesProxy.count) // 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 font.pixelSize: app.smallFont
elide: Text.ElideRight elide: Text.ElideRight
@ -329,7 +329,7 @@ MainPageTile {
case "extendedshutter": case "extendedshutter":
return "../images/up.svg" return "../images/up.svg"
default: default:
console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name) console.warn("InterfaceTile", "inlineButtonControl image: Unhandled interface", iface.name)
} }
return "" return ""
} }
@ -370,7 +370,7 @@ MainPageTile {
} }
break; break;
default: 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": case "extendedshutter":
return "../images/media-playback-stop.svg" return "../images/media-playback-stop.svg"
default: default:
console.warn("DevicesPageDelegate, inlineButtonControl image: Unhandled interface", iface.name) console.warn("InterfaceTile, inlineButtonControl image: Unhandled interface", iface.name)
} }
return ""; return "";
} }
@ -450,7 +450,7 @@ MainPageTile {
} }
break; break;
default: 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": case "extendedshutter":
return "../images/down.svg" return "../images/down.svg"
default: 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: default:
console.warn("DevicesPageDelegate, inlineButtonControl clicked: Unhandled interface", iface.name) console.warn("InterfaceTile, inlineButtonControl clicked: Unhandled interface", iface.name)
} }
} }
} }

View File

@ -145,7 +145,7 @@ DeviceListPageBase {
} }
MediaControls { MediaControls {
visible: itemDelegate.deviceClass.interfaces.indexOf("mediacontroller") >= 0 visible: itemDelegate.deviceClass.interfaces.indexOf("mediacontroller") >= 0
device: itemDelegate.device thing: itemDelegate.device
} }
} }
Item { Item {

View File

@ -141,12 +141,7 @@ Page {
return; return;
} }
var source; var source = Qt.resolvedUrl("../customviews/GenericTypeGraph.qml");
if (engine.jsonRpcClient.ensureServerVersion("1.10")) {
source = Qt.resolvedUrl("../customviews/GenericTypeGraph.qml");
} else {
source = Qt.resolvedUrl("../customviews/GenericTypeGraphPre110.qml");
}
setSource(source, {device: root.device, stateType: stateType}) setSource(source, {device: root.device, stateType: stateType})
} }
} }

View File

@ -173,7 +173,7 @@ DevicePageBase {
MediaArtworkImage { MediaArtworkImage {
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: parent.width / parent.columns Layout.preferredWidth: parent.width / parent.columns
device: root.device thing: root.device
} }
ColumnLayout { ColumnLayout {
@ -208,7 +208,7 @@ DevicePageBase {
} }
MediaControls { MediaControls {
device: root.device thing: root.device
iconSize: app.iconSize * 2 iconSize: app.iconSize * 2
} }
} }
@ -220,63 +220,8 @@ DevicePageBase {
Component { Component {
id: browserComponent id: browserComponent
Item { MediaBrowser {
thing: root.device
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
}
}
}
}
} }
} }
@ -287,7 +232,6 @@ DevicePageBase {
swipeView.currentIndex--; swipeView.currentIndex--;
} }
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: app.margins anchors.margins: app.margins
@ -300,49 +244,13 @@ DevicePageBase {
MediaControls { MediaControls {
Layout.fillWidth: true 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 { footer: Pane {
Material.elevation: 1 Material.elevation: 1
height: 52 height: 52
@ -374,64 +282,11 @@ DevicePageBase {
onClicked: swipeView.currentIndex-- onClicked: swipeView.currentIndex--
} }
} }
Item { ShuffleRepeatVolumeControl {
Layout.fillWidth: true; Layout.fillHeight: true Layout.fillWidth: true
visible: root.deviceClass.interfaces.indexOf("shufflerepeat") >= 0 thing: root.device
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()
}
}
} }
Item { Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: swipeView.count > 1 && swipeView.currentIndex < swipeView.count - 1 ? parent.width / 4 : 0 Layout.preferredWidth: swipeView.count > 1 && swipeView.currentIndex < swipeView.count - 1 ? parent.width / 4 : 0

View File

@ -38,16 +38,160 @@ import "../customviews"
DevicePageBase { DevicePageBase {
id: root id: root
Loader { Flickable {
anchors.fill: parent id: listView
Component.onCompleted: { anchors { fill: parent }
var src interactive: contentHeight > height
if (engine.jsonRpcClient.ensureServerVersion("1.10")) { contentHeight: contentGrid.implicitHeight
src = "SensorDevicePagePost110.qml"
} else { GridLayout {
src = "SensorDevicePagePre110.qml" 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})
} }
} }
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.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
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.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
}
}

View File

@ -38,16 +38,55 @@ import "../customviews"
DevicePageBase { DevicePageBase {
id: root id: root
Loader { Flickable {
anchors.fill: parent anchors.fill: parent
Component.onCompleted: { clip: true
var src contentHeight: contentColumn.implicitHeight
if (engine.jsonRpcClient.ensureServerVersion("1.10")) {
src = "WeatherDevicePagePost110.qml" ColumnLayout {
} else { id: contentColumn
src = "WeatherDevicePagePre110.qml" 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})
} }
} }
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.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")
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.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"
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.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
}
}

View File

@ -71,12 +71,12 @@ Page {
cellWidth: width / tilesPerRow cellWidth: width / tilesPerRow
cellHeight: cellWidth cellHeight: cellWidth
// delegate: DevicesPageDelegate { // delegate: InterfaceTile {
// width: interfacesGridView.cellWidth // width: interfacesGridView.cellWidth
// height: interfacesGridView.cellHeight // height: interfacesGridView.cellHeight
// } // }
delegate: DevicesPageDelegate { delegate: InterfaceTile {
width: interfacesGridView.cellWidth width: interfacesGridView.cellWidth
height: interfacesGridView.cellHeight height: interfacesGridView.cellHeight
iface: Interfaces.findByName(model.name) iface: Interfaces.findByName(model.name)

View File

@ -1 +1 @@
../mediaplayer-app-symbolic.svg ../media.svg

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg id="svg4874" width="96" height="96" version="1.1" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata id="metadata4879">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer1" transform="translate(67.857 -78.505)">
<g id="g4845" transform="matrix(0 -1 -1 0 373.51 516.51)">
<g id="g4778" transform="matrix(-.9996 0 0 1 575.94 -611)">
<g id="g4780" transform="matrix(-1 0 0 1 576 611)">
<rect id="rect4782" transform="scale(-1,1)" x="-438" y="345.36" width="96.038" height="96" style="color:#000000;fill:none"/>
<path id="path4212" d="m341.96 389 56.75 24v-15.273h39.288l-56.75-24v15.273z" style="fill:#808080"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.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
}
}
}
}

View File

@ -36,15 +36,10 @@ import Nymea 1.0
import "../components" import "../components"
import "../delegates" import "../delegates"
MouseArea { MainViewBase {
id: root id: root
property bool editMode: false 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 { TagsProxyModel {
id: tagsProxy 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"))
}
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.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
}
}

View File

@ -35,12 +35,8 @@ import Nymea 1.0
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.2
import "../components" import "../components"
MouseArea { MainViewBase {
id: root id: root
preventStealing: true
onWheel: wheel.accepted = true
readonly property int count: groupsGridView.count
GridView { GridView {
id: groupsGridView id: groupsGridView
@ -488,7 +484,18 @@ MouseArea {
} }
// involve count in the statement to make the binding re-evaluate when the group is changed // 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
}
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.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
}
}

View File

@ -35,15 +35,9 @@ import Nymea 1.0
import QtQuick.Controls.Material 2.2 import QtQuick.Controls.Material 2.2
import "../components" import "../components"
MouseArea { MainViewBase {
id: root 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 { GridView {
id: interfacesGridView id: interfacesGridView
anchors.fill: parent 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();
})
}
}
}
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
import QtQuick 2.8
import QtQuick.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"))
}
}

View File

@ -56,23 +56,6 @@ Page {
LogsModel { LogsModel {
id: logsModel id: logsModel
engine: _engine 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 live: true
} }
@ -83,13 +66,11 @@ Page {
ListView { ListView {
id: listView id: listView
model: engine.jsonRpcClient.ensureServerVersion("1.10") ? logsModelNg : logsModel model: logsModel
anchors.fill: parent anchors.fill: parent
clip: true clip: true
headerPositioning: ListView.OverlayHeader headerPositioning: ListView.OverlayHeader
Component.onCompleted: model.update()
onDraggingChanged: { onDraggingChanged: {
if (dragging) { if (dragging) {
root.autoScroll = false; root.autoScroll = false;
@ -103,14 +84,6 @@ Page {
visible: listView.model.busy visible: listView.model.busy
} }
onContentYChanged: {
if (!engine.jsonRpcClient.ensureServerVersion("1.10")) {
if (!logsModel.busy && contentY - originY < 5 * height) {
logsModel.fetchEarlier(1)
}
}
}
delegate: ItemDelegate { delegate: ItemDelegate {
id: delegate id: delegate
width: parent.width width: parent.width

View File

@ -309,7 +309,7 @@ Page {
} }
BusyIndicator { BusyIndicator {
running: visible running: visible
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter
} }
} }