Merge PR #276: Grouping

This commit is contained in:
Jenkins nymea 2019-11-30 18:22:08 +01:00
commit 2955690e02
34 changed files with 1774 additions and 421 deletions

View File

@ -23,6 +23,7 @@
#include "devicesproxy.h"
#include "engine.h"
#include "tagsmanager.h"
#include "types/tag.h"
DevicesProxy::DevicesProxy(QObject *parent) :
QSortFilterProxyModel(parent)
@ -93,7 +94,7 @@ QString DevicesProxy::filterTagId() const
void DevicesProxy::setFilterTagId(const QString &filterTag)
{
if (m_filterTagId != filterTagId()) {
if (m_filterTagId != filterTag) {
m_filterTagId = filterTag;
emit filterTagIdChanged();
invalidateFilter();
@ -101,6 +102,21 @@ void DevicesProxy::setFilterTagId(const QString &filterTag)
}
}
QString DevicesProxy::filterTagValue() const
{
return m_filterTagValue;
}
void DevicesProxy::setFilterTagValue(const QString &tagValue)
{
if (m_filterTagValue != tagValue) {
m_filterTagValue = tagValue;
emit filterTagValueChanged();
invalidateFilter();
emit countChanged();
}
}
QString DevicesProxy::filterDeviceClassId() const
{
return m_filterDeviceClassId;
@ -112,6 +128,22 @@ void DevicesProxy::setFilterDeviceClassId(const QString &filterDeviceClassId)
m_filterDeviceClassId = filterDeviceClassId;
emit filterDeviceClassIdChanged();
invalidateFilter();
emit countChanged();
}
}
QString DevicesProxy::filterDeviceId() const
{
return m_filterDeviceId;
}
void DevicesProxy::setFilterDeviceId(const QString &filterDeviceId)
{
if (m_filterDeviceId != filterDeviceId) {
m_filterDeviceId = filterDeviceId;
emit filterDeviceIdChanged();
invalidateFilter();
emit countChanged();
}
}
@ -156,7 +188,7 @@ void DevicesProxy::setNameFilter(const QString &nameFilter)
m_nameFilter = nameFilter;
emit nameFilterChanged();
invalidateFilter();
countChanged();
emit countChanged();
}
}
@ -201,6 +233,7 @@ void DevicesProxy::setGroupByInterface(bool groupByInterface)
m_groupByInterface = groupByInterface;
emit groupByInterfaceChanged();
invalidate();
emit countChanged();
}
}
@ -254,12 +287,21 @@ bool DevicesProxy::filterAcceptsRow(int source_row, const QModelIndex &source_pa
{
Device *device = getInternal(source_row);
if (!m_filterTagId.isEmpty()) {
if (!m_engine->tagsManager()->tags()->findDeviceTag(device->id().toString(), m_filterTagId)) {
Tag *tag = m_engine->tagsManager()->tags()->findDeviceTag(device->id().toString(), m_filterTagId);
if (!tag) {
return false;
}
if (!m_filterTagValue.isEmpty() && tag->value() != m_filterTagValue) {
return false;
}
}
if (!m_filterDeviceClassId.isEmpty()) {
if (device->deviceClassId() != m_filterDeviceClassId) {
if (device->deviceClassId() != QUuid(m_filterDeviceClassId)) {
return false;
}
}
if (!m_filterDeviceId.isEmpty()) {
if (device->id() != QUuid(m_filterDeviceId)) {
return false;
}
}

View File

@ -38,7 +38,9 @@ class DevicesProxy : public QSortFilterProxyModel
Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged)
Q_PROPERTY(DevicesProxy *parentProxy READ parentProxy WRITE setParentProxy NOTIFY parentProxyChanged)
Q_PROPERTY(QString filterTagId READ filterTagId WRITE setFilterTagId NOTIFY filterTagIdChanged)
Q_PROPERTY(QString filterTagValue READ filterTagValue WRITE setFilterTagValue NOTIFY filterTagValueChanged)
Q_PROPERTY(QString filterDeviceClassId READ filterDeviceClassId WRITE setFilterDeviceClassId NOTIFY filterDeviceClassIdChanged)
Q_PROPERTY(QString filterDeviceId READ filterDeviceId WRITE setFilterDeviceId NOTIFY filterDeviceIdChanged)
Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged)
Q_PROPERTY(QStringList hiddenInterfaces READ hiddenInterfaces WRITE setHiddenInterfaces NOTIFY hiddenInterfacesChanged)
Q_PROPERTY(QString nameFilter READ nameFilter WRITE setNameFilter NOTIFY nameFilterChanged)
@ -63,9 +65,15 @@ public:
QString filterTagId() const;
void setFilterTagId(const QString &filterTag);
QString filterTagValue() const;
void setFilterTagValue(const QString &tagValue);
QString filterDeviceClassId() const;
void setFilterDeviceClassId(const QString &filterDeviceClassId);
QString filterDeviceId() const;
void setFilterDeviceId(const QString &filterDeviceId);
QStringList shownInterfaces() const;
void setShownInterfaces(const QStringList &shownInterfaces);
@ -91,7 +99,9 @@ signals:
void engineChanged();
void parentProxyChanged();
void filterTagIdChanged();
void filterTagValueChanged();
void filterDeviceClassIdChanged();
void filterDeviceIdChanged();
void shownInterfacesChanged();
void hiddenInterfacesChanged();
void nameFilterChanged();
@ -106,7 +116,9 @@ private:
Engine *m_engine = nullptr;
DevicesProxy *m_parentProxy = nullptr;
QString m_filterTagId;
QString m_filterTagValue;
QString m_filterDeviceClassId;
QString m_filterDeviceId;
QStringList m_shownInterfaces;
QStringList m_hiddenInterfaces;
QString m_nameFilter;

View File

@ -137,6 +137,7 @@ void Engine::onConnectedChanged()
qDebug() << "Engine: connected changed:" << m_jsonRpcClient->connected();
m_deviceManager->clear();
m_ruleManager->clear();
m_tagsManager->clear();
if (m_jsonRpcClient->connected()) {
qDebug() << "Engine: inital setup required:" << m_jsonRpcClient->initialSetupRequired() << "auth required:" << m_jsonRpcClient->authenticationRequired();
if (!m_jsonRpcClient->initialSetupRequired() && !m_jsonRpcClient->authenticationRequired()) {

View File

@ -48,6 +48,7 @@
#include "models/wirelessaccesspointsproxy.h"
#include "tagsmanager.h"
#include "models/tagsproxymodel.h"
#include "models/taglistmodel.h"
#include "types/tag.h"
#include "ruletemplates/ruletemplates.h"
#include "ruletemplates/ruletemplate.h"
@ -189,6 +190,7 @@ void registerQmlTypes() {
qmlRegisterUncreatableType<Tags>(uri, 1, 0, "Tags", "Get it from TagsManager");
qmlRegisterUncreatableType<Tag>(uri, 1, 0, "Tag", "Get it from Tags");
qmlRegisterType<TagsProxyModel>(uri, 1, 0, "TagsProxyModel");
qmlRegisterType<TagListModel>(uri, 1, 0, "TagListModel");
qmlRegisterType<NetworkManagerController>(uri, 1, 0, "NetworkManagerController");
qmlRegisterType<BluetoothDiscovery>(uri, 1, 0, "BluetoothDiscovery");

View File

@ -47,6 +47,7 @@ SOURCES += \
deviceclassesproxy.cpp \
devicediscovery.cpp \
models/packagesfiltermodel.cpp \
models/taglistmodel.cpp \
vendorsproxy.cpp \
pluginsproxy.cpp \
interfacesmodel.cpp \
@ -109,6 +110,7 @@ HEADERS += \
deviceclassesproxy.h \
devicediscovery.h \
models/packagesfiltermodel.h \
models/taglistmodel.h \
vendorsproxy.h \
pluginsproxy.h \
interfacesmodel.h \

View File

@ -5,6 +5,7 @@
#include "types/device.h"
#include "devices.h"
#include "devicesproxy.h"
InterfacesProxy::InterfacesProxy(QObject *parent): QSortFilterProxyModel(parent)
{
@ -23,6 +24,7 @@ void InterfacesProxy::setShowEvents(bool showEvents)
m_showEvents = showEvents;
emit showEventsChanged();
invalidateFilter();
void countChanged();
}
}
@ -37,6 +39,7 @@ void InterfacesProxy::setShowActions(bool showActions)
m_showActions = showActions;
emit showActionsChanged();
invalidateFilter();
void countChanged();
}
}
@ -51,6 +54,7 @@ void InterfacesProxy::setShowStates(bool showStates)
m_showStates = showStates;
emit showStatesChanged();
invalidateFilter();
void countChanged();
}
}
@ -58,7 +62,6 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source
{
Q_UNUSED(source_parent)
QString interfaceName = m_interfaces->get(source_row)->name();
qDebug() << "filterAcceptsRow" << interfaceName << m_shownInterfaces;
if (!m_shownInterfaces.isEmpty()) {
if (!m_shownInterfaces.contains(interfaceName)) {
return false;
@ -83,6 +86,24 @@ bool InterfacesProxy::filterAcceptsRow(int source_row, const QModelIndex &source
return false;
}
}
if (m_devicesProxyFilter != nullptr) {
// TODO: This could be improved *a lot* by caching interfaces in the devices model...
bool found = false;
for (int i = 0; i < m_devicesProxyFilter->rowCount(); i++) {
Device *d = m_devicesProxyFilter->get(i);
if (!d->deviceClass()) {
qWarning() << "Cannot find DeviceClass for device:" << d->id() << d->name();
return false;
}
if (d->deviceClass()->interfaces().contains(interfaceName)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
Interface* iface = m_interfaces->get(source_row);
if (m_showEvents) {

View File

@ -4,15 +4,18 @@
#include <QSortFilterProxyModel>
class Devices;
class DevicesProxy;
class Interface;
class Interfaces;
class InterfacesProxy: public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_PROPERTY(QStringList shownInterfaces READ shownInterfaces WRITE setShownInterfaces NOTIFY shownInterfacesChanged)
Q_PROPERTY(Devices* devicesFilter READ devicesFilter WRITE setDevicesFilter NOTIFY devicesFilterChanged)
Q_PROPERTY(DevicesProxy* devicesProxyFilter READ devicesProxyFilter WRITE setDevicesProxyFilter NOTIFY devicesProxyFilterChanged)
Q_PROPERTY(bool showEvents READ showEvents WRITE setShowEvents NOTIFY showEventsChanged)
Q_PROPERTY(bool showActions READ showActions WRITE setShowActions NOTIFY showActionsChanged)
Q_PROPERTY(bool showStates READ showStates WRITE setShowStates NOTIFY showStatesChanged)
@ -26,6 +29,9 @@ public:
Devices* devicesFilter() const { return m_devicesFilter; }
void setDevicesFilter(Devices *devices) { m_devicesFilter = devices; emit devicesFilterChanged(); invalidateFilter(); }
DevicesProxy* devicesProxyFilter() const { return m_devicesProxyFilter; }
void setDevicesProxyFilter(DevicesProxy *devicesProxy) { m_devicesProxyFilter = devicesProxy; emit devicesProxyFilterChanged(); invalidateFilter(); }
bool showEvents() const;
void setShowEvents(bool showEvents);
@ -42,14 +48,18 @@ public:
signals:
void shownInterfacesChanged();
void devicesFilterChanged();
void devicesProxyFilterChanged();
void showEventsChanged();
void showActionsChanged();
void showStatesChanged();
void countChanged();
private:
Interfaces *m_interfaces = nullptr;
QStringList m_shownInterfaces;
Devices* m_devicesFilter = nullptr;
DevicesProxy* m_devicesProxyFilter = nullptr;
bool m_showEvents = false;
bool m_showActions = false;
bool m_showStates = false;

View File

@ -0,0 +1,100 @@
#include "taglistmodel.h"
#include "tagsproxymodel.h"
#include "types/tag.h"
#include <QDebug>
TagListModel::TagListModel(QObject *parent) : QAbstractListModel(parent)
{
}
TagsProxyModel *TagListModel::tagsProxy() const
{
return m_tagsProxy;
}
void TagListModel::setTagsProxy(TagsProxyModel *tagsProxy)
{
if (m_tagsProxy != tagsProxy) {
m_tagsProxy = tagsProxy;
emit tagsProxyChanged();
connect(tagsProxy, &TagsProxyModel::countChanged, this, &TagListModel::update);
update();
}
}
int TagListModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_list.count();
}
QVariant TagListModel::data(const QModelIndex &index, int role) const
{
switch (role) {
case RoleTagId:
return m_list.at(index.row())->tagId();
case RoleValue:
return m_list.at(index.row())->value();
}
return QVariant();
}
QHash<int, QByteArray> TagListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles.insert(RoleTagId, "tagId");
roles.insert(RoleValue, "value");
return roles;
}
bool TagListModel::containsId(const QString &tagId)
{
foreach (Tag* t, m_list) {
if (t->tagId() == tagId) {
return true;
}
}
return false;
}
bool TagListModel::containsValue(const QString &tagValue)
{
foreach (Tag* t, m_list) {
if (t->value() == tagValue) {
return true;
}
}
return false;
}
void TagListModel::update()
{
beginResetModel();
qDeleteAll(m_list);
m_list.clear();
for (int i = 0; i < m_tagsProxy->rowCount(); i++) {
Tag *tag = m_tagsProxy->get(i);
bool found = false;
foreach (Tag* existingTag, m_list) {
if (tag->tagId() == existingTag->tagId() && tag->value() == existingTag->value()) {
found = true;
break;
}
}
if (!found) {
Tag *t = new Tag(tag->tagId(), tag->value(), this);
m_list.append(t);
}
}
qDebug() << "Model populated" << m_list.count() << this;
endResetModel();
emit countChanged();
}

View File

@ -0,0 +1,44 @@
#ifndef TAGLISTMODEL_H
#define TAGLISTMODEL_H
#include <QAbstractListModel>
class TagsProxyModel;
class Tag;
class TagListModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
Q_PROPERTY(TagsProxyModel* tagsProxy READ tagsProxy WRITE setTagsProxy NOTIFY tagsProxyChanged)
public:
enum Roles {
RoleTagId,
RoleValue
};
explicit TagListModel(QObject *parent = nullptr);
TagsProxyModel* tagsProxy() const;
void setTagsProxy(TagsProxyModel* tagsProxy);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE bool containsId(const QString &tagId);
Q_INVOKABLE bool containsValue(const QString &tagValue);
signals:
void countChanged();
void tagsProxyChanged();
private slots:
void update();
private:
TagsProxyModel *m_tagsProxy = nullptr;
QList<Tag*> m_list;
};
#endif // TAGLISTMODEL_H

View File

@ -94,17 +94,18 @@ bool TagsProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_
Q_UNUSED(source_parent)
Tag *tag = m_tags->get(source_row);
if (!m_filterTagId.isEmpty()) {
if (tag->tagId() != m_filterTagId) {
QRegExp exp(m_filterTagId);
if (!exp.exactMatch(tag->tagId())) {
return false;
}
}
if (!m_filterDeviceId.isEmpty()) {
if (tag->deviceId() != m_filterDeviceId) {
if (QUuid(tag->deviceId()) != QUuid(m_filterDeviceId)) {
return false;
}
}
if (!m_filterRuleId.isEmpty()) {
if (tag->ruleId() != m_filterRuleId) {
if (QUuid(tag->ruleId()) != QUuid(m_filterRuleId)) {
return false;
}
}
@ -115,7 +116,7 @@ bool TagsProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex
{
QString leftValue = m_tags->get(source_left.row())->value();
QString rightValue = m_tags->get(source_right.row())->value();
bool okLeft, okRight;;
bool okLeft, okRight;
qlonglong leftAsNumber = leftValue.toLongLong(&okLeft);
qlonglong rightAsNumber = rightValue.toLongLong(&okRight);
if (okLeft && okRight) {

View File

@ -42,6 +42,7 @@ signals:
void filterTagIdChanged();
void filterDeviceIdChanged();
void filterRuleIdChanged();
void groupSameTagsChanged();
void countChanged();
private:

View File

@ -17,10 +17,22 @@ QString TagsManager::nameSpace() const
void TagsManager::init()
{
m_busy = true;
emit busyChanged();
m_tags->clear();
m_jsonClient->sendCommand("Tags.GetTags", this, "getTagsReply");
}
void TagsManager::clear()
{
m_tags->clear();
}
bool TagsManager::busy() const
{
return m_busy;
}
Tags *TagsManager::tags() const
{
return m_tags;
@ -83,13 +95,16 @@ void TagsManager::handleTagsNotification(const QVariantMap &params)
QString notification = params.value("notification").toString();
if (notification == "Tags.TagAdded") {
addTagInternal(tagMap);
Tag *tag = unpackTag(tagMap);
if (tag) {
m_tags->addTag(tag);
}
} else if (notification == "Tags.TagRemoved") {
for (int i = 0; i < m_tags->rowCount(); i++) {
Tag* tag = m_tags->get(i);
if (tagMap.value("deviceId").toString() == tag->deviceId() &&
tagMap.value("ruleId").toString() == tag->ruleId() &&
if (tagMap.value("deviceId").toUuid() == tag->deviceId() &&
tagMap.value("ruleId").toUuid() == tag->ruleId() &&
tagMap.value("tagId").toString() == tag->tagId()) {
m_tags->removeTag(tag);
return;
@ -99,8 +114,8 @@ void TagsManager::handleTagsNotification(const QVariantMap &params)
qDebug() << "tag value changed";
for (int i = 0; i < m_tags->rowCount(); i++) {
Tag* tag = m_tags->get(i);
if (tagMap.value("deviceId").toString() == tag->deviceId() &&
tagMap.value("ruleId").toString() == tag->ruleId() &&
if (tagMap.value("deviceId").toUuid() == tag->deviceId() &&
tagMap.value("ruleId").toUuid() == tag->ruleId() &&
tagMap.value("tagId").toString() == tag->tagId()) {
qDebug() << "Found tag";
tag->setValue(tagMap.value("value").toString());
@ -111,10 +126,17 @@ void TagsManager::handleTagsNotification(const QVariantMap &params)
void TagsManager::getTagsReply(const QVariantMap &params)
{
QList<Tag*> tags;
foreach (const QVariant &tagVariant, params.value("params").toMap().value("tags").toList()) {
addTagInternal(tagVariant.toMap());
Tag *tag = unpackTag(tagVariant.toMap());
if (tag) {
tags.append(tag);
}
}
emit tagsChanged();
m_tags->addTags(tags);
m_busy = false;
emit busyChanged();
}
void TagsManager::addTagReply(const QVariantMap &params)
@ -127,7 +149,7 @@ void TagsManager::removeTagReply(const QVariantMap &params)
qDebug() << "RemoveTag reply" << params;
}
void TagsManager::addTagInternal(const QVariantMap &tagMap)
Tag* TagsManager::unpackTag(const QVariantMap &tagMap)
{
QString deviceId = tagMap.value("deviceId").toString();
QString ruleId = tagMap.value("ruleId").toString();
@ -143,8 +165,8 @@ void TagsManager::addTagInternal(const QVariantMap &tagMap)
} else {
qWarning() << "Invalid tag. Neither deviceId nor ruleId are set. Skipping...";
tag->deleteLater();
return;
return nullptr;
}
// qDebug() << "adding tag" << tag->tagId() << tag->value();
m_tags->addTag(tag);
return tag;
}

View File

@ -9,13 +9,16 @@
class TagsManager : public JsonHandler
{
Q_OBJECT
Q_PROPERTY(Tags* tags READ tags NOTIFY tagsChanged)
Q_PROPERTY(Tags* tags READ tags CONSTANT)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public:
explicit TagsManager(JsonRpcClient *jsonClient, QObject *parent = nullptr);
QString nameSpace() const override;
void init();
void clear();
bool busy() const;
Tags* tags() const;
@ -25,7 +28,7 @@ public:
Q_INVOKABLE void untagRule(const QString &ruleId, const QString &tagId);
signals:
void tagsChanged();
void busyChanged();
private slots:
void handleTagsNotification(const QVariantMap &params);
@ -34,11 +37,12 @@ private slots:
void removeTagReply(const QVariantMap &params);
private:
void addTagInternal(const QVariantMap &tagMap);
Tag *unpackTag(const QVariantMap &tagMap);
JsonRpcClient *m_jsonClient = nullptr;
Tags *m_tags = nullptr;
bool m_busy = false;
};
#endif // TAGSMANAGER_H

View File

@ -89,6 +89,11 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent)
tr("Daylight"),
tr("Daylight changed"));
addInterface("lightsensor", tr("Light intensity sensors"));
addStateType("lightsensor", "lightIntensity", QVariant::Bool, false,
tr("Light intensity"),
tr("Light intensity changed"));
addInterface("evcharger", tr("EV charger"));
addStateType("evcharger", "power", QVariant::Bool, true,
tr("Charging"),
@ -115,6 +120,13 @@ Interfaces::Interfaces(QObject *parent) : QAbstractListModel(parent)
tr("Playback status"),
tr("Playback status changed"),
tr("Set playback status"));
addInterface("mediacontroller", tr("Media controllers"));
addActionType("mediacontroller", "play", tr("Start playback"), new ParamTypes());
addActionType("mediacontroller", "stop", tr("Stop playback"), new ParamTypes());
addActionType("mediacontroller", "pause", tr("Pause playback"), new ParamTypes());
addActionType("mediacontroller", "skipBack", tr("Skip back"), new ParamTypes());
addActionType("mediacontroller", "skipNext", tr("Skip next"), new ParamTypes());
}
int Interfaces::rowCount(const QModelIndex &parent) const
@ -144,6 +156,9 @@ QHash<int, QByteArray> Interfaces::roleNames() const
Interface *Interfaces::get(int index) const
{
if (index < 0 || index >= m_list.count()) {
return nullptr;
}
return m_list.at(index);
}

View File

@ -10,22 +10,22 @@ Tag::Tag(const QString &tagId, const QString &value, QObject *parent):
}
QString Tag::deviceId() const
QUuid Tag::deviceId() const
{
return m_deviceId;
}
void Tag::setDeviceId(const QString &deviceId)
void Tag::setDeviceId(const QUuid &deviceId)
{
m_deviceId = deviceId;
}
QString Tag::ruleId() const
QUuid Tag::ruleId() const
{
return m_ruleId;
}
void Tag::setRuleId(const QString &ruleId)
void Tag::setRuleId(const QUuid &ruleId)
{
m_ruleId = ruleId;
}

View File

@ -2,23 +2,24 @@
#define TAG_H
#include <QObject>
#include <QUuid>
class Tag : public QObject
{
Q_OBJECT
Q_PROPERTY(QString deviceId READ deviceId CONSTANT)
Q_PROPERTY(QString ruleId READ ruleId CONSTANT)
Q_PROPERTY(QUuid deviceId READ deviceId CONSTANT)
Q_PROPERTY(QUuid ruleId READ ruleId CONSTANT)
Q_PROPERTY(QString tagId READ tagId CONSTANT)
Q_PROPERTY(QString value READ value NOTIFY valueChanged)
public:
explicit Tag(const QString &tagId, const QString &value, QObject *parent = nullptr);
QString deviceId() const;
void setDeviceId(const QString &deviceId);
QUuid deviceId() const;
void setDeviceId(const QUuid &deviceId);
QString ruleId() const;
void setRuleId(const QString &ruleId);
QUuid ruleId() const;
void setRuleId(const QUuid &ruleId);
QString tagId() const;
@ -29,8 +30,8 @@ signals:
void valueChanged();
private:
QString m_deviceId;
QString m_ruleId;
QUuid m_deviceId;
QUuid m_ruleId;
QString m_tagId;
QString m_value;
};

View File

@ -46,6 +46,19 @@ void Tags::addTag(Tag *tag)
beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
m_list.append(tag);
endInsertRows();
qDebug() << "tags count changed";
emit countChanged();
}
void Tags::addTags(QList<Tag *> tags)
{
beginInsertRows(QModelIndex(), m_list.count(), m_list.count() + tags.count() - 1);
foreach (Tag *tag, tags) {
tag->setParent(this);
connect(tag, &Tag::valueChanged, this, &Tags::tagValueChanged);
}
m_list.append(tags);
endInsertRows();
emit countChanged();
}
@ -68,7 +81,7 @@ Tag *Tags::get(int index) const
return m_list.at(index);
}
Tag *Tags::findDeviceTag(const QString &deviceId, const QString &tagId) const
Tag *Tags::findDeviceTag(const QUuid &deviceId, const QString &tagId) const
{
foreach (Tag *tag, m_list) {
if (tag->deviceId() == deviceId && tag->tagId() == tagId) {

View File

@ -25,11 +25,12 @@ public:
QHash<int, QByteArray> roleNames() const override;
void addTag(Tag *tag);
void addTags(QList<Tag*> tags);
void removeTag(Tag *tag);
Tag* get(int index) const;
Q_INVOKABLE Tag* findDeviceTag(const QString &deviceId, const QString &tagId) const;
Q_INVOKABLE Tag* findDeviceTag(const QUuid &deviceId, const QString &tagId) const;
Q_INVOKABLE Tag* findRuleTag(const QString &ruleId, const QString &tagId) const;
void clear();

View File

@ -212,5 +212,6 @@
<file>ui/images/browser/MediaBrowserIconNapster.svg</file>
<file>ui/images/browser/MediaBrowserIconSoundCloud.svg</file>
<file>ui/images/browser/MediaBrowserIconDeezer.svg</file>
<file>ui/images/view-grid-symbolic.svg</file>
</qresource>
</RCC>

View File

@ -197,5 +197,8 @@
<file>ui/magic/NewMagicPage.qml</file>
<file>ui/components/WebViewWrapper.qml</file>
<file>ui/magic/NewScenePage.qml</file>
<file>ui/mainviews/GroupsView.qml</file>
<file>ui/grouping/GroupPage.qml</file>
<file>ui/delegates/ThingTile.qml</file>
</qresource>
</RCC>

View File

@ -1339,6 +1339,22 @@
<source>Logs</source>
<translation>Protokoll</translation>
</message>
<message>
<source>Grouping</source>
<translation>Gruppen</translation>
</message>
<message>
<source>New group</source>
<translation>Neue Gruppe</translation>
</message>
<message>
<source>OK</source>
<translation>OK</translation>
</message>
<message>
<source>Groups for %1</source>
<translation>Gruppen für %1</translation>
</message>
</context>
<context>
<name>DeviceRulesPage</name>
@ -2204,6 +2220,13 @@ Bitte versuche es erneut.</translation>
<translation>Entschuldigung, es gibt noch nicht genügend Daten um hier ein Diagramm anzuzeigen!</translation>
</message>
</context>
<context>
<name>GroupsView</name>
<message>
<source>Lighting</source>
<translation>Beleuchtung</translation>
</message>
</context>
<context>
<name>HeatingDevicePage</name>
<message>
@ -2515,6 +2538,42 @@ Bitte versuche es erneut.</translation>
<source>Set playback status</source>
<translation>Setze Wiedergabestatus</translation>
</message>
<message>
<source>Light intensity sensors</source>
<translation>Lichtstärkesensoren</translation>
</message>
<message>
<source>Light intensity</source>
<translation>Lichtstärke</translation>
</message>
<message>
<source>Light intensity changed</source>
<translation>Lichtstärke geändert</translation>
</message>
<message>
<source>Media controllers</source>
<translation>Mediensteuerungen</translation>
</message>
<message>
<source>Start playback</source>
<translation>Wiedergabe starten</translation>
</message>
<message>
<source>Stop playback</source>
<translation>Wiedergabe stoppen</translation>
</message>
<message>
<source>Pause playback</source>
<translation>Wiedergabe pausieren</translation>
</message>
<message>
<source>Skip back</source>
<translation>Zurück springen</translation>
</message>
<message>
<source>Skip next</source>
<translation>Weiter springen</translation>
</message>
</context>
<context>
<name>InterfacesModels</name>
@ -2893,7 +2952,7 @@ Bitte versuche es erneut.</translation>
</message>
<message>
<source>There are no scenes set up yet</source>
<translation>Es sind noch keine Szenen eingerichet</translation>
<translation type="vanished">Es sind noch keine Szenen eingerichet</translation>
</message>
<message>
<source>It appears there are no things set up either yet. In order to use scenes you need to add some things first.</source>
@ -2950,6 +3009,26 @@ Bitte versuche es erneut.</translation>
<source>In order for your %1 system to be useful, go ahead and add some things.</source>
<translation>Damit dein %1 System sich nützlich machen kann, füge einige &quot;Things&quot; hinzu.</translation>
</message>
<message>
<source>Groups</source>
<translation>Gruppen</translation>
</message>
<message>
<source>There are no scenes set up yet.</source>
<translation>Es sind noch keine Szenen eingerichtet.</translation>
</message>
<message>
<source>My groups</source>
<translation>Meine Gruppen</translation>
</message>
<message>
<source>There are no groups set up yet.</source>
<translation>Es sind noch keine Gruppen eingerichtet.</translation>
</message>
<message>
<source>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.</source>
<translation>&quot;Things&quot; in Gruppen zu verwalten ist hilfreich um mehrere Geräte gleichzeitig zu schalten, z.B. einen ganzen Raum. Halte Ausschau nach dem Gruppensymbol wenn du mit &quot;Things&quot; interagierst und benutze es um sie Gruppen hinzuzufügen.</translation>
</message>
</context>
<context>
<name>ManualConnectPage</name>

View File

@ -903,6 +903,22 @@
<source>Logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Grouping</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>OK</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Groups for %1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DeviceRulesPage</name>
@ -1648,6 +1664,13 @@ Please try again.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>GroupsView</name>
<message>
<source>Lighting</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>HeatingDevicePage</name>
<message>
@ -1944,6 +1967,42 @@ Please try again.</source>
<source>Set playback status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Light intensity sensors</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Light intensity</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Light intensity changed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Media controllers</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Start playback</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Stop playback</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pause playback</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Skip back</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Skip next</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LightsDeviceListPage</name>
@ -2225,10 +2284,6 @@ Please try again.</source>
<source>My scenes</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>There are no scenes set up yet</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>It appears there are no things set up either yet. In order to use scenes you need to add some things first.</source>
<translation type="unfinished"></translation>
@ -2276,6 +2331,26 @@ Please try again.</source>
<source>In order for your %1 system to be useful, go ahead and add some things.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Groups</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>There are no scenes set up yet.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>My groups</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>There are no groups set up yet.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>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.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ManualConnectPage</name>

View File

@ -903,6 +903,22 @@
<source>Logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Grouping</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>OK</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Groups for %1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DeviceRulesPage</name>
@ -1648,6 +1664,13 @@ Please try again.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>GroupsView</name>
<message>
<source>Lighting</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>HeatingDevicePage</name>
<message>
@ -1944,6 +1967,42 @@ Please try again.</source>
<source>Set playback status</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Light intensity sensors</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Light intensity</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Light intensity changed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Media controllers</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Start playback</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Stop playback</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pause playback</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Skip back</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Skip next</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LightsDeviceListPage</name>
@ -2225,10 +2284,6 @@ Please try again.</source>
<source>My scenes</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>There are no scenes set up yet</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>It appears there are no things set up either yet. In order to use scenes you need to add some things first.</source>
<translation type="unfinished"></translation>
@ -2276,6 +2331,26 @@ Please try again.</source>
<source>In order for your %1 system to be useful, go ahead and add some things.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Groups</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>There are no scenes set up yet.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>My groups</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>There are no groups set up yet.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>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.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ManualConnectPage</name>

View File

@ -932,6 +932,22 @@
<source>Logs</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Grouping</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>OK</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Groups for %1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DeviceRulesPage</name>
@ -1684,6 +1700,13 @@ Please try again.</source>
<translation>, !</translation>
</message>
</context>
<context>
<name>GroupsView</name>
<message>
<source>Lighting</source>
<translation type="unfinished">()</translation>
</message>
</context>
<context>
<name>HeatingDevicePage</name>
<message>
@ -1984,6 +2007,42 @@ Please try again.</source>
<source>Set playback status</source>
<translation> </translation>
</message>
<message>
<source>Light intensity sensors</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Light intensity</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Light intensity changed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Media controllers</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Start playback</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Stop playback</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pause playback</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Skip back</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Skip next</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LightsDeviceListPage</name>
@ -2267,7 +2326,7 @@ Please try again.</source>
</message>
<message>
<source>There are no scenes set up yet</source>
<translation> () </translation>
<translation type="vanished"> () </translation>
</message>
<message>
<source>It appears there are no things set up either yet. In order to use scenes you need to add some things first.</source>
@ -2315,6 +2374,26 @@ Please try again.</source>
<source>In order for your %1 system to be useful, go ahead and add some things.</source>
<translation>%1 .</translation>
</message>
<message>
<source>Groups</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>There are no scenes set up yet.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>My groups</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>There are no groups set up yet.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>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.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ManualConnectPage</name>

View File

@ -79,6 +79,10 @@ Page {
}
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
}
@ -265,7 +269,7 @@ Page {
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")
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.")
@ -292,6 +296,25 @@ Page {
}
}
}
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
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"))
}
}
}
ColumnLayout {

View File

@ -86,7 +86,7 @@ Item {
Component.onCompleted: ready = true
anchors.fill: parent
anchors.margins: parent.margins
anchors.margins: parent ? parent.margins : 0
source: ready && width > 0 && height > 0 && icon.name ? icon.name : ""
sourceSize {
width: width
@ -100,7 +100,7 @@ Item {
id: colorizedImage
anchors.fill: parent
anchors.margins: parent.margins
anchors.margins: parent ? parent.margins : 0
visible: active && image.status == Image.Ready
// Whether or not a color has been set.

View File

@ -0,0 +1,376 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Nymea 1.0
import "../components"
MainPageTile {
id: root
text: device.name.toUpperCase()
iconName: app.interfacesToIcon(deviceClass.interfaces)
iconColor: app.accentColor
batteryCritical: batteryCriticalState && batteryCriticalState.value === true
disconnected: connectedState && connectedState.value === false
backgroundImage: artworkState && artworkState.value.length > 0 ? artworkState.value : ""
property Device device: null
readonly property DeviceClass deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null
readonly property State connectedState: deviceClass.interfaces.indexOf("connectable") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("connected").id) : null
readonly property State batteryCriticalState: deviceClass.interfaces.indexOf("battery") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("batteryCritical").id) : null
readonly property State artworkState: deviceClass.interfaces.indexOf("mediametadataprovider") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("artwork").id) : null
contentItem: Loader {
id: loader
anchors.fill: parent
sourceComponent: {
if (root.deviceClass.interfaces.indexOf("closable") >= 0) {
return closableComponent;
}
if (root.deviceClass.interfaces.indexOf("power") >= 0) {
return lightsComponent;
}
if (root.deviceClass.interfaces.indexOf("sensor") >= 0) {
return sensorsComponent;
}
if (root.deviceClass.interfaces.indexOf("weather") >= 0) {
return sensorsComponent;
}
if (root.deviceClass.interfaces.indexOf("smartmeter") >= 0) {
return sensorsComponent;
}
if (root.deviceClass.interfaces.indexOf("mediacontroller") >= 0) {
return mediaComponent;
}
}
Binding { target: loader.item ? loader.item : null; property: "deviceClass"; value: root.deviceClass }
Binding { target: loader.item ? loader.item : null; property: "device"; value: root.device }
}
Component {
id: lightsComponent
RowLayout {
property var device: null
property var deviceClass: null
readonly property var powerStateType: deviceClass.stateTypes.findByName("power");
readonly property var powerState: device.states.getState(powerStateType.id)
readonly property var brightnessStateType: deviceClass.stateTypes.findByName("brightness");
readonly property var brightnessState: brightnessStateType ? device.states.getState(brightnessStateType.id) : null
ThrottledSlider {
Layout.fillWidth: true
Layout.leftMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
opacity: deviceClass.interfaces.indexOf("dimmablelight") >= 0 ? 1 : 0
enabled: opacity > 0
from: 0
to: 100
value: brightnessState ? brightnessState.value : 0
onMoved: {
var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId);
var actionType = deviceClass.actionTypes.findByName("brightness");
var params = [];
var powerParam = {}
powerParam["paramTypeId"] = actionType.paramTypes.get(0).id;
powerParam["value"] = value;
params.push(powerParam)
engine.deviceManager.executeAction(device.id, actionType.id, params);
}
}
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.rightMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
contentItem: ColorIcon {
name: deviceClass.interfaces.indexOf("light") >= 0
? (powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg")
: app.interfacesToIcon(deviceClass.interfaces)
color: powerState.value === true ? app.accentColor : keyColor
}
onClicked: {
var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId);
var actionType = deviceClass.actionTypes.findByName("power");
var params = [];
var powerParam = {}
powerParam["paramTypeId"] = actionType.paramTypes.get(0).id;
powerParam["value"] = !powerState.value;
params.push(powerParam)
engine.deviceManager.executeAction(device.id, actionType.id, params);
}
}
}
}
Component {
id: sensorsComponent
RowLayout {
id: sensorsRoot
property var device: null
property var deviceClass: null
spacing: 0
property var shownInterfaces: []
property int currentStateIndex: -1
property var currentStateType: deviceClass ? deviceClass.stateTypes.findByName(shownInterfaces[currentStateIndex].state) : null
property var currentState: currentStateType ? device.states.getState(currentStateType.id) : null
onDeviceClassChanged: {
if (deviceClass == null) {
return;
}
var tmp = []
if (deviceClass.interfaces.indexOf("temperaturesensor") >= 0) {
tmp.push({iface: "temperaturesensor", state: "temperature"});
}
if (deviceClass.interfaces.indexOf("humiditysensor") >= 0) {
tmp.push({iface: "humiditysensor", state: "humidity"});
}
if (deviceClass.interfaces.indexOf("moisturesensor") >= 0) {
tmp.push({iface: "moisturesensor", state: "moisture"});
}
if (deviceClass.interfaces.indexOf("pressuresensor") >= 0) {
tmp.push({iface: "pressuresensor", state: "pressure"});
}
if (deviceClass.interfaces.indexOf("lightsensor") >= 0) {
tmp.push({iface: "lightsensor", state: "lightIntensity"});
}
if (deviceClass.interfaces.indexOf("conductivitysensor") >= 0) {
tmp.push({iface: "conductivitysensor", state: "conductivity"});
}
if (deviceClass.interfaces.indexOf("noisesensor") >= 0) {
tmp.push({iface: "noisesensor", state: "noise"});
}
if (deviceClass.interfaces.indexOf("co2sensor") >= 0) {
tmp.push({iface: "co2sensor", state: "co2"});
}
if (deviceClass.interfaces.indexOf("smartmeterconsumer") >= 0) {
tmp.push({iface: "smartmeterconsumer", state: "totalEnergyConsumed"});
}
if (deviceClass.interfaces.indexOf("smartmeterproducer") >= 0) {
tmp.push({iface: "smartmeterproducer", state: "totalEnergyProduced"});
}
if (deviceClass.interfaces.indexOf("daylightsensor") >= 0) {
tmp.push({iface: "daylightsensor", state: "daylight"});
}
if (deviceClass.interfaces.indexOf("presencesensor") >= 0) {
tmp.push({iface: "presencesensor", state: "isPresent"});
}
if (deviceClass.interfaces.indexOf("weather") >= 0) {
tmp.push({iface: "temperaturesensor", state: "temperature"});
tmp.push({iface: "humiditysensor", state: "humidity"});
tmp.push({iface: "pressuresensor", state: "pressure"});
}
shownInterfaces = tmp
currentStateIndex = 0
}
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.leftMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
visible: sensorsRoot.shownInterfaces.length > 1
contentItem: ColorIcon {
name: "../images/back.svg"
}
onClicked: {
var newIndex = sensorsRoot.currentStateIndex - 1;
if (newIndex < 0) newIndex = sensorsRoot.shownInterfaces.length - 1
sensorsRoot.currentStateIndex = newIndex;
}
}
Item { Layout.fillHeight: true; Layout.fillWidth: true }
ColorIcon {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.alignment: Qt.AlignVCenter
color: app.interfaceToColor(sensorsRoot.shownInterfaces[sensorsRoot.currentStateIndex].iface)
name: app.interfaceToIcon(sensorsRoot.shownInterfaces[sensorsRoot.currentStateIndex].iface)
}
Item { Layout.fillHeight: true; Layout.fillWidth: true }
ColumnLayout {
Layout.fillWidth: true
spacing: 0
visible: sensorsRoot.currentStateType.type.toLowerCase() !== "bool"
Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
text: sensorsRoot.currentStateType.unitString
font.pixelSize: app.smallFont
}
Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
text: sensorsRoot.currentState.value// + " " + sensorsRoot.currentStateType.unitString
elide: Text.ElideRight
}
}
Led {
state: sensorsRoot.currentState.value === true ? "on" : "off"
visible: sensorsRoot.currentStateType.type.toLowerCase() === "bool"
}
Item { Layout.fillHeight: true; Layout.fillWidth: true }
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.rightMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
visible: sensorsRoot.shownInterfaces.length > 1
contentItem: ColorIcon {
name: "../images/next.svg"
}
onClicked: {
var newIndex = sensorsRoot.currentStateIndex + 1;
if (newIndex >= sensorsRoot.shownInterfaces.length) newIndex = 0;
sensorsRoot.currentStateIndex = newIndex;
}
}
}
}
Component {
id: closableComponent
RowLayout {
property var device: null
property var deviceClass: null
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.leftMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
contentItem: ColorIcon {
name: "../images/up.svg"
color: app.accentColor
}
onClicked: {
var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId);
var actionType = deviceClass.actionTypes.findByName("open");
engine.deviceManager.executeAction(device.id, actionType.id);
}
}
Slider {
id: closableSlider
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
visible: deviceClass.interfaces.indexOf("extendedclosable") >= 0
readonly property var percentageStateType: deviceClass.stateTypes.findByName("percentage");
readonly property var percentateState: percentageStateType ? device.states.getState(percentageStateType.id) : null
from: 0
to: 100
value: percentateState ? percentateState.value : 0
}
Item {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
visible: !closableSlider.visible
}
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.rightMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
contentItem: ColorIcon {
name: "../images/down.svg"
color: app.accentColor
}
onClicked: {
var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId);
var actionType = deviceClass.actionTypes.findByName("close");
engine.deviceManager.executeAction(device.id, actionType.id);
}
}
}
}
Component {
id: mediaComponent
RowLayout {
id: mediaRoot
property Device device: null
property DeviceClass deviceClass: null
readonly property State playbackState: device.states.getState(deviceClass.stateTypes.findByName("playbackStatus").id)
function executeAction(actionName, params) {
var actionTypeId = deviceClass.actionTypes.findByName(actionName).id;
engine.deviceManager.executeAction(device.id, actionTypeId, params)
}
Item { Layout.fillWidth: true }
ProgressButton {
Layout.preferredHeight: app.iconSize * .9
Layout.preferredWidth: height
imageSource: "../images/media-skip-backward.svg"
longpressImageSource: "../images/media-seek-backward.svg"
repeat: true
onClicked: {
mediaRoot.executeAction("skipBack")
}
onLongpressed: {
mediaRoot.executeAction("fastRewind")
}
}
Item { Layout.fillWidth: true }
ProgressButton {
Layout.preferredHeight: app.iconSize * 1.3
Layout.preferredWidth: height
imageSource: mediaRoot.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg"
longpressImageSource: "../images/media-playback-stop.svg"
longpressEnabled: mediaRoot.playbackState.value !== "Stopped"
onClicked: {
if (mediaRoot.playbackState.value === "Playing") {
mediaRoot.executeAction("pause")
} else {
mediaRoot.executeAction("play")
}
}
onLongpressed: {
mediaRoot.executeAction("stop")
}
}
Item { Layout.fillWidth: true }
ProgressButton {
Layout.preferredHeight: app.iconSize * .9
Layout.preferredWidth: height
imageSource: "../images/media-skip-forward.svg"
longpressImageSource: "../images/media-seek-forward.svg"
repeat: true
onClicked: {
mediaRoot.executeAction("skipNext")
}
onLongpressed: {
mediaRoot.executeAction("fastForward")
}
}
Item { Layout.fillWidth: true }
}
}
}

View File

@ -70,6 +70,13 @@ Page {
iconSource: Qt.binding(function() { return favoritesProxy.count === 0 ? "../images/starred.svg" : "../images/non-starred.svg"}),
functionName: "toggleFavorite"
}))
thingMenu.addItem(menuEntryComponent.createObject(thingMenu,
{
text: qsTr("Grouping"),
iconSource: "../images/view-grid-symbolic.svg",
functionName: "addToGroup"
}))
}
}
function openDeviceMagicPage() {
@ -85,6 +92,11 @@ Page {
engine.tagsManager.untagDevice(root.device.id, "favorites")
}
}
function addToGroup() {
var dialog = addToGroupDialog.createObject(root)
dialog.open();
}
function openThingSettingsPage() {
pageStack.push(Qt.resolvedUrl("../thingconfiguration/ConfigureThingPage.qml"), {device: root.device})
}
@ -101,6 +113,70 @@ Page {
onTriggered: thingMenu[functionName]()
}
}
Component {
id: addToGroupDialog
MeaDialog {
title: qsTr("Groups for %1").arg(root.device.name)
headerIcon: "../images/view-grid-symbolic.svg"
RowLayout {
Layout.leftMargin: app.margins
Layout.rightMargin: app.margins
spacing: app.margins
TextField {
id: newGroupdTextField
Layout.fillWidth: true
placeholderText: qsTr("New group")
}
Button {
text: qsTr("OK")
enabled: newGroupdTextField.displayText.length > 0 && !groupTags.containsId("group-" + newGroupdTextField.displayText)
onClicked: {
engine.tagsManager.tagDevice(root.device.id, "group-" + newGroupdTextField.text, 1000)
newGroupdTextField.text = ""
}
}
}
ListView {
Layout.fillWidth: true
height: 200
clip: true
ScrollIndicator.vertical: ScrollIndicator {}
model: TagListModel {
id: groupTags
tagsProxy: TagsProxyModel {
tags: engine.tagsManager.tags
filterTagId: "group-.*"
}
}
delegate: CheckDelegate {
width: parent.width
text: model.tagId.substring(6)
checked: innerProxy.count > 0
onClicked: {
if (innerProxy.count == 0) {
engine.tagsManager.tagDevice(root.device.id, model.tagId, 1000)
} else {
engine.tagsManager.untagDevice(root.device.id, model.tagId, model.value)
}
}
DevicesProxy {
id: innerProxy
engine: _engine
filterTagId: model.tagId
filterDeviceId: root.device.id
}
}
}
}
}
}
Rectangle {

View File

@ -0,0 +1,56 @@
import QtQuick 2.5
import QtQuick.Controls 2.1
import QtQuick.Controls.Material 2.1
import QtQuick.Layouts 1.1
import Nymea 1.0
import "../components"
import "../delegates"
Page {
id: root
header: NymeaHeader {
text: root.groupTag.substring(6)
onBackPressed: pageStack.pop()
}
property string groupTag
DevicesProxy {
id: devicesInGroup
engine: _engine
filterTagId: root.groupTag
}
InterfacesProxy {
id: interfacesInGroup
devicesProxyFilter: devicesInGroup
showStates: true
}
GridView {
id: gridView
anchors.fill: parent
anchors.margins: app.margins / 2
model: devicesInGroup
readonly property int minTileWidth: 180
readonly property int minTileHeight: 240
readonly property int tilesPerRow: root.width / minTileWidth
cellWidth: gridView.width / tilesPerRow
cellHeight: cellWidth
delegate: ThingTile {
width: gridView.cellWidth
height: gridView.cellHeight
device: devicesInGroup.get(index)
onClicked: pageStack.push(Qt.resolvedUrl("../devicepages/" + app.interfaceListToDevicePage(deviceClass.interfaces)), {device: device})
}
}
}

View File

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="96"
height="96"
id="svg4874"
version="1.1"
inkscape:version="0.91+devel r"
viewBox="0 0 96 96.000001"
sodipodi:docname="view-grid-symbolic.svg">
<defs
id="defs4876" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6199994"
inkscape:cx="-33.300718"
inkscape:cy="62.909239"
inkscape:document-units="px"
inkscape:current-layer="g4780"
showgrid="true"
showborder="true"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-global="true">
<inkscape:grid
type="xygrid"
id="grid5451"
empspacing="8" />
<sodipodi:guide
orientation="1,0"
position="8,-8.0000001"
id="guide4063" />
<sodipodi:guide
orientation="1,0"
position="4,-8.0000001"
id="guide4065" />
<sodipodi:guide
orientation="0,1"
position="-8,88.000001"
id="guide4067" />
<sodipodi:guide
orientation="0,1"
position="-8,92.000001"
id="guide4069" />
<sodipodi:guide
orientation="0,1"
position="104,4"
id="guide4071" />
<sodipodi:guide
orientation="0,1"
position="-5,8.0000001"
id="guide4073" />
<sodipodi:guide
orientation="1,0"
position="92,-8.0000001"
id="guide4075" />
<sodipodi:guide
orientation="1,0"
position="88,-8.0000001"
id="guide4077" />
<sodipodi:guide
orientation="0,1"
position="-8,84.000001"
id="guide4074" />
<sodipodi:guide
orientation="1,0"
position="12,-8.0000001"
id="guide4076" />
<sodipodi:guide
orientation="0,1"
position="-5,12"
id="guide4078" />
<sodipodi:guide
orientation="1,0"
position="84,-9.0000001"
id="guide4080" />
<sodipodi:guide
position="48,-8.0000001"
orientation="1,0"
id="guide4170" />
<sodipodi:guide
position="-8,48"
orientation="0,1"
id="guide4172" />
</sodipodi:namedview>
<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></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(67.857146,-78.50504)">
<g
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
id="g4845"
style="display:inline">
<g
inkscape:export-ydpi="90"
inkscape:export-xdpi="90"
inkscape:export-filename="next01.png"
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
id="g4778"
inkscape:label="Layer 1">
<g
transform="matrix(-1,0,0,1,575.99999,611)"
id="g4780"
style="display:inline">
<rect
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
id="rect4782"
width="96.037987"
height="96"
x="-438.00244"
y="345.36221"
transform="scale(-1,1)" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 429.99805,433.36328 -2,0 -34.01367,0 0,-36.00195 36.01367,0 0,36.00195 z m -4,-4.00195 0,-27.99805 -28.01172,0 0,27.99805 28.01172,0 z"
id="rect4201"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 429.99805,389.36133 -2,0 -34.01367,0 0,-36 36.01367,0 0,36 z m -4,-4 0,-27.99805 -28.01172,0 0,27.99805 28.01172,0 z"
id="rect4203"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 385.98242,433.36328 -2,0 -34.01562,0 0,-36.00195 36.01562,0 0,36.00195 z m -4.00195,-4.00195 0,-27.99805 -28.01172,0 0,27.99805 28.01172,0 z"
id="rect4205"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 385.98242,389.36133 -2,0 -34.01562,0 0,-36 36.01562,0 0,36 z m -4.00195,-4 0,-27.99805 -28.01172,0 0,27.99805 28.01172,0 z"
id="rect4207"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -5,11 +5,15 @@ import QtQuick.Layouts 1.2
import Nymea 1.0
import "../components"
Item {
MouseArea {
id: root
property alias count: interfacesGridView.count
property alias model: interfacesGridView.model
// Prevent scroll events to swipe left/right in case they fall through the grid
preventStealing: true
onWheel: wheel.accepted = true
GridView {
id: interfacesGridView
anchors.fill: parent

View File

@ -4,13 +4,18 @@ import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import Nymea 1.0
import "../components"
import "../delegates"
Item {
MouseArea {
id: root
property bool editMode: false
readonly property int count: tagsProxy.count
// Prevent scroll events to swipe left/right in case they fall through the grid
preventStealing: true
onWheel: wheel.accepted = true
TagsProxyModel {
id: tagsProxy
tags: engine.tagsManager.tags
@ -29,57 +34,16 @@ Item {
cellHeight: cellWidth
model: tagsProxy
delegate: MainPageTile {
delegate: ThingTile {
id: delegateRoot
width: gridView.cellWidth
height: gridView.cellHeight
text: device.name.toUpperCase()
iconName: app.interfacesToIcon(deviceClass.interfaces)
iconColor: app.accentColor
visible: !fakeDragItem.visible || fakeDragItem.deviceId !== device.id
batteryCritical: batteryCriticalState && batteryCriticalState.value === true
disconnected: connectedState && connectedState.value === false
property var modelIndex: index
property string deviceId: model.deviceId
property string ruleId: model.ruleId
readonly property Device device: engine.deviceManager.devices.getDevice(deviceId)
readonly property DeviceClass deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId) : null
readonly property State connectedState: deviceClass.interfaces.indexOf("connectable") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("connected").id) : null
readonly property State batteryCriticalState: deviceClass.interfaces.indexOf("battery") >= 0 ? device.states.getState(deviceClass.stateTypes.findByName("batteryCritical").id) : null
device: engine.deviceManager.devices.getDevice(deviceId)
onClicked: pageStack.push(Qt.resolvedUrl("../devicepages/" + app.interfaceListToDevicePage(deviceClass.interfaces)), {device: device})
onPressAndHold: root.editMode = true
contentItem: Loader {
id: loader
anchors.fill: parent
sourceComponent: {
if (delegateRoot.deviceClass.interfaces.indexOf("closable") >= 0) {
return closableComponent;
}
if (delegateRoot.deviceClass.interfaces.indexOf("power") >= 0) {
return lightsComponent;
}
if (delegateRoot.deviceClass.interfaces.indexOf("sensor") >= 0) {
return sensorsComponent;
}
if (delegateRoot.deviceClass.interfaces.indexOf("weather") >= 0) {
return sensorsComponent;
}
if (delegateRoot.deviceClass.interfaces.indexOf("smartmeter") >= 0) {
return sensorsComponent;
}
if (delegateRoot.deviceClass.interfaces.indexOf("mediacontroller") >= 0) {
return mediaComponent;
}
}
Binding { target: loader.item ? loader.item : null; property: "deviceClass"; value: delegateRoot.deviceClass }
Binding { target: loader.item ? loader.item : null; property: "device"; value: delegateRoot.device }
}
SequentialAnimation {
loops: Animation.Infinite
running: root.editMode
@ -174,330 +138,4 @@ Item {
}
}
}
Component {
id: lightsComponent
RowLayout {
property var device: null
property var deviceClass: null
readonly property var powerStateType: deviceClass.stateTypes.findByName("power");
readonly property var powerState: device.states.getState(powerStateType.id)
readonly property var brightnessStateType: deviceClass.stateTypes.findByName("brightness");
readonly property var brightnessState: brightnessStateType ? device.states.getState(brightnessStateType.id) : null
ThrottledSlider {
Layout.fillWidth: true
Layout.leftMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
opacity: deviceClass.interfaces.indexOf("dimmablelight") >= 0 ? 1 : 0
enabled: opacity > 0
from: 0
to: 100
value: brightnessState ? brightnessState.value : 0
onMoved: {
var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId);
var actionType = deviceClass.actionTypes.findByName("brightness");
var params = [];
var powerParam = {}
powerParam["paramTypeId"] = actionType.paramTypes.get(0).id;
powerParam["value"] = value;
params.push(powerParam)
engine.deviceManager.executeAction(device.id, actionType.id, params);
}
}
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.rightMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
contentItem: ColorIcon {
name: deviceClass.interfaces.indexOf("light") >= 0
? (powerState.value === true ? "../images/light-on.svg" : "../images/light-off.svg")
: app.interfacesToIcon(deviceClass.interfaces)
color: powerState.value === true ? app.accentColor : keyColor
}
onClicked: {
var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId);
var actionType = deviceClass.actionTypes.findByName("power");
var params = [];
var powerParam = {}
powerParam["paramTypeId"] = actionType.paramTypes.get(0).id;
powerParam["value"] = !powerState.value;
params.push(powerParam)
engine.deviceManager.executeAction(device.id, actionType.id, params);
}
}
}
}
Component {
id: sensorsComponent
RowLayout {
id: sensorsRoot
property var device: null
property var deviceClass: null
spacing: 0
property var shownInterfaces: []
property int currentStateIndex: -1
property var currentStateType: deviceClass ? deviceClass.stateTypes.findByName(shownInterfaces[currentStateIndex].state) : null
property var currentState: currentStateType ? device.states.getState(currentStateType.id) : null
onDeviceClassChanged: {
if (deviceClass == null) {
return;
}
var tmp = []
if (deviceClass.interfaces.indexOf("temperaturesensor") >= 0) {
tmp.push({iface: "temperaturesensor", state: "temperature"});
}
if (deviceClass.interfaces.indexOf("humiditysensor") >= 0) {
tmp.push({iface: "humiditysensor", state: "humidity"});
}
if (deviceClass.interfaces.indexOf("moisturesensor") >= 0) {
tmp.push({iface: "moisturesensor", state: "moisture"});
}
if (deviceClass.interfaces.indexOf("pressuresensor") >= 0) {
tmp.push({iface: "pressuresensor", state: "pressure"});
}
if (deviceClass.interfaces.indexOf("lightsensor") >= 0) {
tmp.push({iface: "lightsensor", state: "lightIntensity"});
}
if (deviceClass.interfaces.indexOf("conductivitysensor") >= 0) {
tmp.push({iface: "conductivitysensor", state: "conductivity"});
}
if (deviceClass.interfaces.indexOf("noisesensor") >= 0) {
tmp.push({iface: "noisesensor", state: "noise"});
}
if (deviceClass.interfaces.indexOf("co2sensor") >= 0) {
tmp.push({iface: "co2sensor", state: "co2"});
}
if (deviceClass.interfaces.indexOf("smartmeterconsumer") >= 0) {
tmp.push({iface: "smartmeterconsumer", state: "totalEnergyConsumed"});
}
if (deviceClass.interfaces.indexOf("smartmeterproducer") >= 0) {
tmp.push({iface: "smartmeterproducer", state: "totalEnergyProduced"});
}
if (deviceClass.interfaces.indexOf("daylightsensor") >= 0) {
tmp.push({iface: "daylightsensor", state: "daylight"});
}
if (deviceClass.interfaces.indexOf("presencesensor") >= 0) {
tmp.push({iface: "presencesensor", state: "isPresent"});
}
if (deviceClass.interfaces.indexOf("weather") >= 0) {
tmp.push({iface: "temperaturesensor", state: "temperature"});
tmp.push({iface: "humiditysensor", state: "humidity"});
tmp.push({iface: "pressuresensor", state: "pressure"});
}
shownInterfaces = tmp
currentStateIndex = 0
}
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.leftMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
visible: sensorsRoot.shownInterfaces.length > 1
contentItem: ColorIcon {
name: "../images/back.svg"
}
onClicked: {
var newIndex = sensorsRoot.currentStateIndex - 1;
if (newIndex < 0) newIndex = sensorsRoot.shownInterfaces.length - 1
sensorsRoot.currentStateIndex = newIndex;
}
}
Item { Layout.fillHeight: true; Layout.fillWidth: true }
ColorIcon {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.alignment: Qt.AlignVCenter
color: app.interfaceToColor(sensorsRoot.shownInterfaces[sensorsRoot.currentStateIndex].iface)
name: app.interfaceToIcon(sensorsRoot.shownInterfaces[sensorsRoot.currentStateIndex].iface)
}
Item { Layout.fillHeight: true; Layout.fillWidth: true }
ColumnLayout {
Layout.fillWidth: true
spacing: 0
visible: sensorsRoot.currentStateType.type.toLowerCase() !== "bool"
Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
text: sensorsRoot.currentStateType.unitString
font.pixelSize: app.smallFont
}
Label {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
text: sensorsRoot.currentState.value// + " " + sensorsRoot.currentStateType.unitString
elide: Text.ElideRight
}
}
Led {
state: sensorsRoot.currentState.value === true ? "on" : "off"
visible: sensorsRoot.currentStateType.type.toLowerCase() === "bool"
}
Item { Layout.fillHeight: true; Layout.fillWidth: true }
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.rightMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
visible: sensorsRoot.shownInterfaces.length > 1
contentItem: ColorIcon {
name: "../images/next.svg"
}
onClicked: {
var newIndex = sensorsRoot.currentStateIndex + 1;
if (newIndex >= sensorsRoot.shownInterfaces.length) newIndex = 0;
sensorsRoot.currentStateIndex = newIndex;
}
}
}
}
Component {
id: closableComponent
RowLayout {
property var device: null
property var deviceClass: null
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.leftMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
contentItem: ColorIcon {
name: "../images/up.svg"
color: app.accentColor
}
onClicked: {
var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId);
var actionType = deviceClass.actionTypes.findByName("open");
engine.deviceManager.executeAction(device.id, actionType.id);
}
}
Slider {
id: closableSlider
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
visible: deviceClass.interfaces.indexOf("extendedclosable") >= 0
readonly property var percentageStateType: deviceClass.stateTypes.findByName("percentage");
readonly property var percentateState: device.states.getState(percentageStateType.id)
from: 0
to: 100
value: percentateState.value
}
Item {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
visible: !closableSlider.visible
}
ItemDelegate {
Layout.preferredWidth: app.iconSize
Layout.preferredHeight: width
Layout.rightMargin: app.margins / 2
Layout.alignment: Qt.AlignVCenter
padding: 0; topPadding: 0; bottomPadding: 0
contentItem: ColorIcon {
name: "../images/down.svg"
color: app.accentColor
}
onClicked: {
var deviceClass = engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId);
var actionType = deviceClass.actionTypes.findByName("close");
engine.deviceManager.executeAction(device.id, actionType.id);
}
}
}
}
Component {
id: mediaComponent
RowLayout {
id: mediaRoot
property Device device: null
property DeviceClass deviceClass: null
readonly property State playbackState: device.states.getState(deviceClass.stateTypes.findByName("playbackStatus").id)
function executeAction(actionName, params) {
var actionTypeId = deviceClass.actionTypes.findByName(actionName).id;
engine.deviceManager.executeAction(device.id, actionTypeId, params)
}
Item { Layout.fillWidth: true }
ProgressButton {
Layout.preferredHeight: app.iconSize * .9
Layout.preferredWidth: height
imageSource: "../images/media-skip-backward.svg"
longpressImageSource: "../images/media-seek-backward.svg"
repeat: true
onClicked: {
mediaRoot.executeAction("skipBack")
}
onLongpressed: {
mediaRoot.executeAction("fastRewind")
}
}
Item { Layout.fillWidth: true }
ProgressButton {
Layout.preferredHeight: app.iconSize * 1.3
Layout.preferredWidth: height
imageSource: mediaRoot.playbackState.value === "Playing" ? "../images/media-playback-pause.svg" : "../images/media-playback-start.svg"
longpressImageSource: "../images/media-playback-stop.svg"
longpressEnabled: mediaRoot.playbackState.value !== "Stopped"
onClicked: {
if (mediaRoot.playbackState.value === "Playing") {
mediaRoot.executeAction("pause")
} else {
mediaRoot.executeAction("play")
}
}
onLongpressed: {
mediaRoot.executeAction("stop")
}
}
Item { Layout.fillWidth: true }
ProgressButton {
Layout.preferredHeight: app.iconSize * .9
Layout.preferredWidth: height
imageSource: "../images/media-skip-forward.svg"
longpressImageSource: "../images/media-seek-forward.svg"
repeat: true
onClicked: {
mediaRoot.executeAction("skipNext")
}
onLongpressed: {
mediaRoot.executeAction("fastForward")
}
}
Item { Layout.fillWidth: true }
}
}
}

View File

@ -0,0 +1,393 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import Nymea 1.0
import QtQuick.Controls.Material 2.2
import "../components"
MouseArea {
id: root
preventStealing: true
onWheel: wheel.accepted = true
readonly property int count: groupsGridView.count
GridView {
id: groupsGridView
anchors.fill: parent
anchors.margins: app.margins / 2
readonly property int minTileWidth: 180
readonly property int minTileHeight: 180
readonly property int tilesPerRow: root.width / minTileWidth
model: TagListModel {
tagsProxy: TagsProxyModel {
tags: engine.tagsManager.tags
filterTagId: "group-.*"
}
}
cellWidth: width / tilesPerRow
cellHeight: cellWidth
delegate: Item {
id: groupDelegate
width: groupsGridView.cellWidth
height: groupsGridView.cellHeight
Pane {
anchors.fill: parent
anchors.margins: app.margins / 2
Material.elevation: 2
padding: 0
DevicesProxy {
id: devicesInGroup
engine: _engine
filterTagId: model.tagId
filterTagValue: model.value
}
InterfacesProxy {
id: controlsInGroup
shownInterfaces: ["light", "simpleclosable", "mediacontroller"]
devicesProxyFilter: devicesInGroup
showStates: true
showActions: true
}
InterfacesProxy {
id: sensorsInGroup
shownInterfaces: ["temperaturesensor", "lightsensor", "presencesensor"]
devicesProxyFilter: devicesInGroup
showStates: true
}
contentItem: ItemDelegate {
leftPadding: 0
topPadding: 0
rightPadding: 0
bottomPadding: 0
onClicked: {
pageStack.push(Qt.resolvedUrl("../grouping/GroupPage.qml"), {groupTag: model.tagId})
}
contentItem: ColumnLayout {
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 30
color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .05)
Label {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
anchors { leftMargin: app.margins; rightMargin: app.margins }
text: model.tagId.substring(6)
elide: Text.ElideRight
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
ColorIcon {
anchors.centerIn: parent
height: app.iconSize * 2
width: height
visible: controlsInGroup.count == 0
color: app.accentColor
name: "../images/view-grid-symbolic.svg"
}
ColumnLayout {
anchors.fill: parent
Repeater {
model: Math.min(controlsInGroup.count, parent.height / 50)
delegate: Loader {
id: controlLoader
Layout.fillWidth: true
Layout.leftMargin: app.margins / 2
Layout.rightMargin: app.margins / 2
property string interfaceName: controlsInGroup.get(index).name
sourceComponent: {
switch (interfaceName) {
case "simpleclosable":
return closableDelegate
case "light":
return lightDelegate
case "mediacontroller":
return mediaControllerDelegate
}
}
Binding {
target: controlLoader.item
property: "devices"
value: devicesInGroup
}
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: app.iconSize * 1.2
color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, 0.05)
RowLayout {
anchors.fill: parent
Repeater {
model: sensorsInGroup
delegate: Row {
height: parent.height
ColorIcon {
height: app.iconSize * .8
width: height
name: app.interfaceToIcon(model.name)
color: app.interfaceToColor(model.name)
}
DevicesProxy {
id: innerProxy
engine: _engine
parentProxy: devicesInGroup
shownInterfaces: [model.name]
}
Led {
visible: ["presencesensor"].indexOf(model.name) >= 0
state: {
var stateName = null
switch (model.name) {
case "presencesensor":
stateName = "isPresent"
break;
}
if (!stateName) {
return "off";
}
var ret = false;
for (var i = 0; i < innerProxy.count; i++) {
ret |= innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value
}
return ret ? "on" : "off";
}
}
Label {
height: parent.height
verticalAlignment: Text.AlignVCenter
text: {
var stateName = null;
switch (model.name) {
case "temperaturesensor":
stateName = "temperature";
break;
case "lightsensor":
stateName = "lightIntensity"
break;
}
if (!stateName) {
return "";
}
var ret = 0
for (var i = 0; i < innerProxy.count; i++) {
ret += innerProxy.get(i).states.getState(innerProxy.get(i).deviceClass.stateTypes.findByName(stateName).id).value
}
return (ret / innerProxy.count).toFixed(1)
}
}
}
}
}
}
}
}
}
}
}
Component {
id: lightDelegate
RowLayout {
property var devices
DevicesProxy {
id: lights
engine: _engine
parentProxy: devices
shownInterfaces: ["light"]
}
DevicesProxy {
id: dimmableLights
engine: _engine
parentProxy: devices
shownInterfaces: ["dimmablelight"]
}
Label {
text: qsTr("Lighting")
Layout.fillWidth: true
Layout.preferredHeight: slider.height
verticalAlignment: Text.AlignVCenter
visible: dimmableLights.count == 0
}
Slider {
id: slider
from: 0
to: 100
visible: dimmableLights.count > 0
value: {
var median = 0
var count = 0;
for (var i = 0; i < dimmableLights.count; i++) {
var device = dimmableLights.get(i);
var brightnessId = device.deviceClass.stateTypes.findByName("brightness").id
median += device.states.getState(brightnessId).value
count++
}
return median / count;
}
Layout.fillWidth: true
onPressedChanged: {
for (var i = 0; i < dimmableLights.count; i++) {
var device = dimmableLights.get(i);
var brightnessId = device.deviceClass.actionTypes.findByName("brightness").id
engine.deviceManager.executeAction(device.id, brightnessId, [{paramTypeId: brightnessId, value: value}]);
}
}
}
ColorIcon {
Layout.preferredHeight: app.iconSize
Layout.preferredWidth: app.iconSize
name: isOn ? "../images/light-on.svg" : "../images/light-off.svg"
color: isOn ? app.accentColor : keyColor
property bool isOn: {
for (var i = 0; i < lights.count; i++) {
var device = lights.get(i)
var powerId = device.deviceClass.stateTypes.findByName("power").id
if (device.states.getState(powerId).value === true) {
return true
}
}
return false;
}
MouseArea {
anchors.fill: parent
onClicked: {
for (var i = 0; i < lights.count; i++) {
var device = lights.get(i)
var powerId = device.deviceClass.stateTypes.findByName("power").id
engine.deviceManager.executeAction(device.id, powerId, [{paramTypeId: powerId, value: !parent.isOn}])
}
}
}
}
}
}
Component {
id: closableDelegate
RowLayout {
property var devices: null
DevicesProxy {
id: simpleClosables
engine: _engine
parentProxy: devices
shownInterfaces: ["simpleclosable"]
}
DevicesProxy {
id: closables
engine: _engine
parentProxy: devices
shownInterfaces: ["closable"]
}
ItemDelegate {
Layout.fillWidth: true
Layout.preferredHeight: app.iconSize
ColorIcon {
height: parent.height
width: height
anchors.centerIn: parent
name: Qt.resolvedUrl("../images/up.svg")
}
onClicked: {
for (var i = 0; i < simpleClosables.count; i++) {
var device = simpleClosables.get(i)
var openId = device.deviceClass.actionTypes.findByName("open").id
engine.deviceManager.executeAction(device.id, openId)
}
}
}
ItemDelegate {
Layout.fillWidth: true
Layout.preferredHeight: app.iconSize
visible: closables.count > 0
ColorIcon {
height: parent.height
width: height
anchors.centerIn: parent
name: Qt.resolvedUrl("../images/media-playback-stop.svg")
}
onClicked: {
for (var i = 0; i < closables.count; i++) {
var device = closables.get(i)
var openId = device.deviceClass.actionTypes.findByName("stop").id
engine.deviceManager.executeAction(device.id, openId)
}
}
}
ItemDelegate {
Layout.fillWidth: true
Layout.preferredHeight: app.iconSize
ColorIcon {
height: parent.height
width: height
anchors.centerIn: parent
name: Qt.resolvedUrl("../images/down.svg")
}
onClicked: {
for (var i = 0; i < simpleClosables.count; i++) {
var device = simpleClosables.get(i)
var closeId = device.deviceClass.actionTypes.findByName("close").id
engine.deviceManager.executeAction(device.id, closeId)
}
}
}
}
}
Component {
id: mediaControllerDelegate
MediaControls {
property var devices: null
DevicesProxy {
id: mediaControllers
engine: _engine
parentProxy: devices
shownInterfaces: ["mediacontroller"]
}
// involve count in the statement to make the binding re-evaluate when the group is changed
device: mediaControllers.count > 0 ? mediaControllers.get(0) : null
}
}
}

View File

@ -5,11 +5,15 @@ import Nymea 1.0
import QtQuick.Controls.Material 2.2
import "../components"
Item {
MouseArea {
id: root
readonly property int count: interfacesGridView.count
// Prevent scroll events to swipe left/right in case they fall through the grid
preventStealing: true
onWheel: wheel.accepted = true
GridView {
id: interfacesGridView
anchors.fill: parent