Merge PR #405: Make the main view more customizable
This commit is contained in:
commit
e4d0392bca
@ -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 ¶ms)
|
|||||||
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 ¶ms)
|
|||||||
|
|
||||||
void DeviceManager::getIOConnectionsResponse(const QVariantMap ¶ms)
|
void DeviceManager::getIOConnectionsResponse(const QVariantMap ¶ms)
|
||||||
{
|
{
|
||||||
qDebug() << "Get IO connections response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
|
// qDebug() << "Get IO connections response" << qUtf8Printable(QJsonDocument::fromVariant(params).toJson());
|
||||||
|
|
||||||
foreach (const QVariant &connectionVariant, params.value("params").toMap().value("ioConnections").toList()) {
|
foreach (const QVariant &connectionVariant, params.value("params").toMap().value("ioConnections").toList()) {
|
||||||
QVariantMap connectionMap = connectionVariant.toMap();
|
QVariantMap connectionMap = connectionVariant.toMap();
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -592,7 +592,7 @@ void JsonRpcClient::helloReply(const QVariantMap ¶ms)
|
|||||||
|
|
||||||
qDebug() << "Handshake reply:" << "Protocol version:" << protoVersionString << "InitRequired:" << m_initialSetupRequired << "AuthRequired:" << m_authenticationRequired << "PushButtonAvailable:" << m_pushButtonAuthAvailable;;
|
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();
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 \
|
||||||
|
|||||||
203
libnymea-app/models/barseriesadapter.cpp
Normal file
203
libnymea-app/models/barseriesadapter.cpp
Normal 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;
|
||||||
|
}
|
||||||
66
libnymea-app/models/barseriesadapter.h
Normal file
66
libnymea-app/models/barseriesadapter.h
Normal 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
|
||||||
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
62
libnymea-app/models/sortfilterproxymodel.cpp
Normal file
62
libnymea-app/models/sortfilterproxymodel.cpp
Normal 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);
|
||||||
|
}
|
||||||
37
libnymea-app/models/sortfilterproxymodel.h
Normal file
37
libnymea-app/models/sortfilterproxymodel.h
Normal 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
|
||||||
@ -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();
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
192
libnymea-app/models/xyseriesadapter.cpp
Normal file
192
libnymea-app/models/xyseriesadapter.cpp
Normal 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;
|
||||||
|
}
|
||||||
89
libnymea-app/models/xyseriesadapter.h
Normal file
89
libnymea-app/models/xyseriesadapter.h
Normal 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
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: \""));
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 ¶ms)
|
int Device::executeAction(const QString &actionName, const QVariantList ¶ms)
|
||||||
{
|
{
|
||||||
ActionType *actionType = m_deviceClass->actionTypes()->findByName(actionName);
|
ActionType *actionType = m_thingClass->actionTypes()->findByName(actionName);
|
||||||
|
|
||||||
QVariantList finalParams;
|
QVariantList finalParams;
|
||||||
foreach (const QVariant ¶mVariant, params) {
|
foreach (const QVariant ¶mVariant, params) {
|
||||||
@ -209,30 +218,30 @@ int Device::executeAction(const QString &actionName, const QVariantList ¶ms)
|
|||||||
return m_deviceManager->executeAction(m_id, actionType->id(), finalParams);
|
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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
12
nymea-app/mainmenumodel.cpp
Normal file
12
nymea-app/mainmenumodel.cpp
Normal 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
24
nymea-app/mainmenumodel.h
Normal 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
|
||||||
@ -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 \
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 ¤tExperience)
|
|
||||||
{
|
|
||||||
QSettings settings;
|
|
||||||
if (settings.value("experience").toString() != currentExperience) {
|
|
||||||
settings.setValue("experience", currentExperience);
|
|
||||||
emit currentExperienceChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList StyleController::allExperiences() const
|
|
||||||
{
|
|
||||||
QDir dir(":/ui/experiences");
|
|
||||||
qDebug() << "experiences:" << dir.entryList();
|
|
||||||
return QStringList() << "Default" << dir.entryList();
|
|
||||||
}
|
|
||||||
|
|
||||||
void StyleController::setSystemFont(const QFont &font)
|
void StyleController::setSystemFont(const QFont &font)
|
||||||
{
|
{
|
||||||
QApplication::setFont(font);
|
QApplication::setFont(font);
|
||||||
|
|||||||
@ -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 ¤tExperience);
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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...
|
||||||
|
|||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|||||||
101
nymea-app/ui/components/MediaBrowser.qml
Normal file
101
nymea-app/ui/components/MediaBrowser.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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"
|
||||||
|
|||||||
145
nymea-app/ui/components/ShuffleRepeatVolumeControl.qml
Normal file
145
nymea-app/ui/components/ShuffleRepeatVolumeControl.qml
Normal 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}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
nymea-app/ui/components/SmartMeterChart.qml
Normal file
121
nymea-app/ui/components/SmartMeterChart.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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()) {
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
../mediaplayer-app-symbolic.svg
|
../media.svg
|
||||||
23
nymea-app/ui/images/energy.svg
Normal file
23
nymea-app/ui/images/energy.svg
Normal 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 |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
373
nymea-app/ui/mainviews/EnergyView.qml
Normal file
373
nymea-app/ui/mainviews/EnergyView.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
240
nymea-app/ui/mainviews/GaragesView.qml
Normal file
240
nymea-app/ui/mainviews/GaragesView.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
186
nymea-app/ui/mainviews/MediaView.qml
Normal file
186
nymea-app/ui/mainviews/MediaView.qml
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
83
nymea-app/ui/mainviews/ThingsView.qml
Normal file
83
nymea-app/ui/mainviews/ThingsView.qml
Normal 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -309,7 +309,7 @@ Page {
|
|||||||
}
|
}
|
||||||
BusyIndicator {
|
BusyIndicator {
|
||||||
running: visible
|
running: visible
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user