Implement a tagging system

This commit is contained in:
Michael Zanetti 2018-06-29 17:37:21 +02:00
parent 4e0091fdff
commit a6f4ddf188
25 changed files with 1043 additions and 7 deletions

View File

@ -220,6 +220,14 @@ QVariantMap JsonHandler::statusToReply(NetworkManager::NetworkManagerError statu
return returns;
}
/*! Returns the formated error map for the given \a status. */
QVariantMap JsonHandler::statusToReply(TagsStorage::TagError status) const
{
QVariantMap returns;
returns.insert("tagError", JsonTypes::tagErrorToString(status));
return returns;
}
/*!
\class nymeaserver::JsonReply

View File

@ -113,6 +113,7 @@ protected:
QVariantMap statusToReply(Logging::LoggingError status) const;
QVariantMap statusToReply(NymeaConfiguration::ConfigurationError status) const;
QVariantMap statusToReply(NetworkManager::NetworkManagerError status) const;
QVariantMap statusToReply(TagsStorage::TagError status) const;
private:
QHash<QString, QString> m_descriptions;

View File

@ -57,6 +57,7 @@
#include "websocketserver.h"
#include "configurationhandler.h"
#include "networkmanagerhandler.h"
#include "tagshandler.h"
#include <QJsonDocument>
#include <QStringList>
@ -453,6 +454,7 @@ void JsonRPCServer::setup()
registerHandler(new StateHandler(this));
registerHandler(new ConfigurationHandler(this));
registerHandler(new NetworkManagerHandler(this));
registerHandler(new TagsHandler(this));
connect(NymeaCore::instance()->cloudManager(), &CloudManager::pairingReply, this, &JsonRPCServer::pairingFinished);
connect(NymeaCore::instance()->cloudManager(), &CloudManager::connectedChanged, this, &JsonRPCServer::onCloudConnectedChanged);

View File

@ -89,6 +89,7 @@ QVariantList JsonTypes::s_networkManagerError;
QVariantList JsonTypes::s_networkManagerState;
QVariantList JsonTypes::s_networkDeviceState;
QVariantList JsonTypes::s_userError;
QVariantList JsonTypes::s_tagError;
QVariantMap JsonTypes::s_paramType;
QVariantMap JsonTypes::s_param;
@ -122,6 +123,7 @@ QVariantMap JsonTypes::s_wirelessNetworkDevice;
QVariantMap JsonTypes::s_tokenInfo;
QVariantMap JsonTypes::s_serverConfiguration;
QVariantMap JsonTypes::s_webServerConfiguration;
QVariantMap JsonTypes::s_tag;
void JsonTypes::init()
{
@ -148,6 +150,7 @@ void JsonTypes::init()
s_networkManagerState = enumToStrings(NetworkManager::staticMetaObject, "NetworkManagerState");
s_networkDeviceState = enumToStrings(NetworkDevice::staticMetaObject, "NetworkDeviceState");
s_userError = enumToStrings(UserManager::staticMetaObject, "UserError");
s_tagError = enumToStrings(TagsStorage::staticMetaObject, "TagError");
// ParamType
s_paramType.insert("id", basicTypeToString(Uuid));
@ -387,6 +390,13 @@ void JsonTypes::init()
s_webServerConfiguration = s_serverConfiguration;
s_webServerConfiguration.insert("publicFolder", basicTypeToString(QVariant::String));
// Tag
s_tag.insert("o:deviceId", basicTypeToString(QVariant::Uuid));
s_tag.insert("o:ruleId", basicTypeToString(QVariant::Uuid));
s_tag.insert("appId", basicTypeToString(QVariant::String));
s_tag.insert("tagId", basicTypeToString(QVariant::String));
s_tag.insert("o:value", basicTypeToString(QVariant::String));
s_initialized = true;
}
@ -435,6 +445,7 @@ QVariantMap JsonTypes::allTypes()
allTypes.insert("NetworkManagerState", networkManagerState());
allTypes.insert("NetworkDeviceState", networkDeviceState());
allTypes.insert("UserError", userError());
allTypes.insert("TagError", tagErrorRef());
allTypes.insert("StateType", stateTypeDescription());
allTypes.insert("StateDescriptor", stateDescriptorDescription());
@ -467,6 +478,7 @@ QVariantMap JsonTypes::allTypes()
allTypes.insert("TokenInfo", tokenInfoDescription());
allTypes.insert("ServerConfiguration", serverConfigurationDescription());
allTypes.insert("WebServerConfiguration", serverConfigurationDescription());
allTypes.insert("Tag", tagDescription());
return allTypes;
}
@ -946,6 +958,21 @@ QVariantMap JsonTypes::packLogEntry(const LogEntry &logEntry)
return logEntryMap;
}
/*! Returns a variant map of the given \a tag. */
QVariantMap JsonTypes::packTag(const Tag &tag)
{
QVariantMap ret;
if (!tag.deviceId().isNull()){
ret.insert("deviceId", tag.deviceId());
} else {
ret.insert("ruleId", tag.ruleId());
}
ret.insert("appId", tag.appId());
ret.insert("tagId", tag.tagId());
ret.insert("value", tag.value());
return ret;
}
/*! Returns a variant list of the given \a createMethods. */
QVariantList JsonTypes::packCreateMethods(DeviceClass::CreateMethods createMethods)
{
@ -1609,6 +1636,20 @@ TimeDescriptor JsonTypes::unpackTimeDescriptor(const QVariantMap &timeDescriptor
return timeDescriptor;
}
/*! Returns a \l{Tag} created from the given \a tagMap. */
Tag JsonTypes::unpackTag(const QVariantMap &tagMap)
{
DeviceId deviceId = DeviceId(tagMap.value("deviceId").toString());
RuleId ruleId = RuleId(tagMap.value("ruleId").toString());
QString appId = tagMap.value("appId").toString();
QString tagId = tagMap.value("tagId").toString();
QString value = tagMap.value("value").toString();
if (!deviceId.isNull()) {
return Tag(deviceId, appId, tagId, value);
}
return Tag(ruleId, appId, tagId, value);
}
ServerConfiguration JsonTypes::unpackServerConfiguration(const QVariantMap &serverConfigurationMap)
{
ServerConfiguration serverConfiguration;
@ -1939,6 +1980,12 @@ QPair<bool, QString> JsonTypes::validateVariant(const QVariant &templateVariant,
qCWarning(dcJsonRpc) << "WebServerConfiguration not matching";
return result;
}
} else if (refName == tagRef()) {
QPair<bool, QString> result = validateMap(tagDescription(), variant.toMap());
if (!result.first) {
qCWarning(dcJsonRpc) << "Tag not matching";
return result;
}
} else if (refName == basicTypeRef()) {
QPair<bool, QString> result = validateBasicType(variant);
if (!result.first) {
@ -2071,11 +2118,16 @@ QPair<bool, QString> JsonTypes::validateVariant(const QVariant &templateVariant,
qCWarning(dcJsonRpc) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(userErrorRef());
return result;
}
} else if (refName == tagErrorRef()) {
QPair<bool, QString> result = validateEnum(s_tagError, variant);
if (!result.first) {
qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(logEntryRef());
return result;
}
} else {
Q_ASSERT_X(false, "JsonTypes", QString("Unhandled ref: %1").arg(refName).toLatin1().data());
return report(false, QString("Unhandled ref %1. Server implementation incomplete.").arg(refName));
}
} else {
QPair<bool, QString> result = JsonTypes::validateProperty(templateVariant, variant);
if (!result.first) {

View File

@ -42,6 +42,9 @@
#include "logging/logentry.h"
#include "logging/logfilter.h"
#include "tagging/tagsstorage.h"
#include "tagging/tag.h"
#include "time/calendaritem.h"
#include "time/repeatingoption.h"
#include "time/timedescriptor.h"
@ -135,6 +138,7 @@ public:
DECLARE_TYPE(networkManagerState, "NetworkManagerState", NetworkManager, NetworkManagerState)
DECLARE_TYPE(networkDeviceState, "NetworkDeviceState", NetworkDevice, NetworkDeviceState)
DECLARE_TYPE(userError, "UserError", UserManager, UserError)
DECLARE_TYPE(tagError, "TagError", TagsStorage, TagError)
DECLARE_OBJECT(paramType, "ParamType")
DECLARE_OBJECT(param, "Param")
@ -168,6 +172,7 @@ public:
DECLARE_OBJECT(tokenInfo, "TokenInfo")
DECLARE_OBJECT(serverConfiguration, "ServerConfiguration")
DECLARE_OBJECT(webServerConfiguration, "WebServerConfiguration")
DECLARE_OBJECT(tag, "Tag")
// pack types
static QVariantMap packEventType(const EventType &eventType);
@ -192,6 +197,7 @@ public:
static QVariantMap packRule(const Rule &rule);
static QVariantMap packRuleDescription(const Rule &rule);
static QVariantMap packLogEntry(const LogEntry &logEntry);
static QVariantMap packTag(const Tag &tag);
static QVariantMap packRepeatingOption(const RepeatingOption &option);
static QVariantMap packCalendarItem(const CalendarItem &calendarItem);
static QVariantMap packTimeEventItem(const TimeEventItem &timeEventItem);
@ -241,6 +247,7 @@ public:
static CalendarItem unpackCalendarItem(const QVariantMap &calendarItemMap);
static TimeEventItem unpackTimeEventItem(const QVariantMap &timeEventItemMap);
static TimeDescriptor unpackTimeDescriptor(const QVariantMap &timeDescriptorMap);
static Tag unpackTag(const QVariantMap &tagMap);
static ServerConfiguration unpackServerConfiguration(const QVariantMap &serverConfigurationMap);
static WebServerConfiguration unpackWebServerConfiguration(const QVariantMap &webServerConfigurationMap);

View File

@ -0,0 +1,143 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* nymea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 2 of the License. *
* *
* nymea 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 nymea. If not, see <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "tagshandler.h"
#include "nymeacore.h"
#include "tagging/tagsstorage.h"
TagsHandler::TagsHandler(QObject *parent) : JsonHandler(parent)
{
QVariantMap params;
QVariantMap returns;
params.clear(); returns.clear();
setDescription("GetTags", "Get the Tags matching the given filter. Tags can be filtered by a deviceID, a ruleId, an appId, a tagId or a combination of any (however, combining deviceId and ruleId will return an empty result set).");
params.insert("o:deviceId", JsonTypes::basicTypeToString(JsonTypes::Uuid));
params.insert("o:ruleId", JsonTypes::basicTypeToString(JsonTypes::Uuid));
params.insert("o:appId", JsonTypes::basicTypeToString(JsonTypes::String));
params.insert("o:tagId", JsonTypes::basicTypeToString(JsonTypes::String));
setParams("GetTags", params);
returns.insert("tagError", JsonTypes::tagErrorRef());
returns.insert("o:tags", QVariantList() << JsonTypes::tagRef());
setReturns("GetTags", returns);
params.clear(); returns.clear();
setDescription("AddTag", "Add a Tag. A Tag must have a deviceId OR a ruleId (call this method twice if you want to attach the same tag to a device and a rule), an appId (Use the appId of your app), a tagId (e.g. \"favorites\") and a value. Upon success, a TagAdded notification will be emitted. Calling this method twice for the same ids (device/rule, appId and tagId) but with a different value will update the tag's value and the TagValueChanged notification will be emitted.");
params.insert("tag", JsonTypes::tagRef());
setParams("AddTag", params);
returns.insert("tagError", JsonTypes::tagErrorRef());
setReturns("AddTag", returns);
params.clear(); returns.clear();
setDescription("RemoveTag", "Remove a Tag. Tag value is optional and will be disregarded. If the ids match, the tag will be deleted and a TagRemoved notification will be emitted.");
params.insert("tag", JsonTypes::tagRef());
setParams("RemoveTag", params);
returns.insert("tagError", JsonTypes::tagErrorRef());
setReturns("RemoveTag", returns);
// Notifications
params.clear();
setDescription("TagAdded", "Emitted whenever a tag is added to the system. ");
params.insert("tag", JsonTypes::tagRef());
setParams("TagAdded", params);
connect(NymeaCore::instance()->tagsStorage(), &TagsStorage::tagAdded, this, &TagsHandler::onTagAdded);
params.clear();
setDescription("TagRemoved", "Emitted whenever a tag is removed from the system. ");
params.insert("tag", JsonTypes::tagRef());
setParams("TagRemoved", params);
connect(NymeaCore::instance()->tagsStorage(), &TagsStorage::tagRemoved, this, &TagsHandler::onTagRemoved);
params.clear();
setDescription("TagValueChanged", "Emitted whenever a tag's value is changed in the system. ");
params.insert("tag", JsonTypes::tagRef());
setParams("TagValueChanged", params);
connect(NymeaCore::instance()->tagsStorage(), &TagsStorage::tagValueChanged, this, &TagsHandler::onTagValueChanged);
}
QString TagsHandler::name() const
{
return "Tags";
}
JsonReply *TagsHandler::GetTags(const QVariantMap &params) const
{
QVariantList ret;
foreach (const Tag &tag, NymeaCore::instance()->tagsStorage()->tags()) {
if (params.contains("deviceId") && params.value("deviceId").toString() != tag.deviceId().toString()) {
continue;
}
if (params.contains("ruleId") && params.value("ruleId").toString() != tag.ruleId().toString()) {
continue;
}
if (params.contains("appId") && params.value("appId").toString() != tag.appId()) {
continue;
}
if (params.contains("tagId") && params.value("tagId").toString() != tag.tagId()) {
continue;
}
ret.append(JsonTypes::packTag(tag));
}
QVariantMap returns = statusToReply(TagsStorage::TagErrorNoError);
returns.insert("tags", ret);
return createReply(returns);
}
JsonReply *TagsHandler::AddTag(const QVariantMap &params) const
{
Tag tag = JsonTypes::unpackTag(params.value("tag").toMap());
TagsStorage::TagError error = NymeaCore::instance()->tagsStorage()->addTag(tag);
QVariantMap returns = statusToReply(error);
return createReply(returns);
}
JsonReply *TagsHandler::RemoveTag(const QVariantMap &params) const
{
Tag tag = JsonTypes::unpackTag(params.value("tag").toMap());
TagsStorage::TagError error = NymeaCore::instance()->tagsStorage()->removeTag(tag);
QVariantMap returns = statusToReply(error);
return createReply(returns);
}
void TagsHandler::onTagAdded(const Tag &tag)
{
qCDebug(dcJsonRpc) << "Notify \"Tags.TagAdded\"";
QVariantMap params;
params.insert("tag", JsonTypes::packTag(tag));
emit TagAdded(params);
}
void TagsHandler::onTagRemoved(const Tag &tag)
{
qCDebug(dcJsonRpc) << "Notify \"Tags.TagRemoved\"";
QVariantMap params;
params.insert("tag", JsonTypes::packTag(tag));
emit TagRemoved(params);
}
void TagsHandler::onTagValueChanged(const Tag &tag)
{
qCDebug(dcJsonRpc) << "Notify \"Tags.TagValueChanged\"";
QVariantMap params;
params.insert("tag", JsonTypes::packTag(tag));
emit TagValueChanged(params);
}

View File

@ -0,0 +1,54 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* nymea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 2 of the License. *
* *
* nymea 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 nymea. If not, see <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef TAGSHANDLER_H
#define TAGSHANDLER_H
#include <QObject>
#include "jsonhandler.h"
namespace nymeaserver {
class TagsHandler : public JsonHandler
{
Q_OBJECT
public:
explicit TagsHandler(QObject *parent = nullptr);
QString name() const override;
Q_INVOKABLE JsonReply *GetTags(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *AddTag(const QVariantMap &params) const;
Q_INVOKABLE JsonReply *RemoveTag(const QVariantMap &params) const;
signals:
void TagAdded(const QVariantMap &params);
void TagRemoved(const QVariantMap &params);
void TagValueChanged(const QVariantMap &params);
private slots:
void onTagAdded(const Tag &tag);
void onTagRemoved(const Tag &tag);
void onTagValueChanged(const Tag &tag);
};
}
#endif // TAGSHANDLER_H

View File

@ -93,7 +93,10 @@ HEADERS += nymeacore.h \
hardware/network/avahi/qtavahiservice_p.h \
hardware/network/avahi/qtavahiservicebrowserimplementation.h \
hardware/network/avahi/qtavahiservicebrowserimplementation_p.h \
debugserverhandler.h
debugserverhandler.h \
tagging/tagsstorage.h \
tagging/tag.h \
jsonrpc/tagshandler.h
SOURCES += nymeacore.cpp \
tcpserver.cpp \
@ -171,4 +174,7 @@ SOURCES += nymeacore.cpp \
hardware/network/avahi/qtavahiservice_p.cpp \
hardware/network/avahi/qtavahiservicebrowserimplementation.cpp \
hardware/network/avahi/qtavahiservicebrowserimplementation_p.cpp \
debugserverhandler.cpp
debugserverhandler.cpp \
tagging/tagsstorage.cpp \
tagging/tag.cpp \
jsonrpc/tagshandler.cpp

View File

@ -101,6 +101,7 @@
#include "ruleengine.h"
#include "networkmanager/networkmanager.h"
#include "nymeasettings.h"
#include "tagging/tagsstorage.h"
#include "devicemanager.h"
#include "plugin/device.h"
@ -485,6 +486,12 @@ DebugServerHandler *NymeaCore::debugServerHandler() const
return m_debugServerHandler;
}
/*! Returns a pointer to the \l{TagsStorage} instance owned by NymeaCore. */
TagsStorage *NymeaCore::tagsStorage() const
{
return m_tagsStorage;
}
/*! Constructs NymeaCore with the given \a parent. This is private.
Use \l{NymeaCore::instance()} to access the single instance.*/
@ -516,6 +523,9 @@ void NymeaCore::init() {
qCDebug(dcApplication()) << "Creating User Manager";
m_userManager = new UserManager(NymeaSettings::settingsPath() + "/user-db.sqlite", this);
qCDebug(dcApplication()) << "Creating Tags Storage";
m_tagsStorage = new TagsStorage(m_deviceManager, m_ruleEngine, this);
qCDebug(dcApplication) << "Creating Server Manager";
m_serverManager = new ServerManager(m_configuration, this);

View File

@ -49,6 +49,7 @@ class JsonRPCServer;
class LogEngine;
class NetworkManager;
class NymeaConfiguration;
class TagsStorage;
class NymeaCore : public QObject
{
@ -84,6 +85,7 @@ public:
UserManager *userManager() const;
CloudManager *cloudManager() const;
DebugServerHandler *debugServerHandler() const;
TagsStorage *tagsStorage() const;
static QStringList getAvailableLanguages();
@ -121,6 +123,7 @@ private:
CloudManager *m_cloudManager;
HardwareManagerImplementation *m_hardwareManager;
DebugServerHandler *m_debugServerHandler;
TagsStorage *m_tagsStorage;
NetworkManager *m_networkManager;
UserManager *m_userManager;

View File

@ -0,0 +1,95 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* nymea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 2 of the License. *
* *
* nymea 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 nymea. If not, see <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "tag.h"
#include <QDebug>
namespace nymeaserver {
Tag::Tag(const DeviceId &deviceId, const QString &appId, const QString &tagId, const QString &value):
m_deviceId(deviceId),
m_appId(appId),
m_tagId(tagId),
m_value(value)
{
}
Tag::Tag(const RuleId &ruleId, const QString &appId, const QString &tagId, const QString &value):
m_ruleId(ruleId),
m_appId(appId),
m_tagId(tagId),
m_value(value)
{
}
DeviceId Tag::deviceId() const
{
return m_deviceId;
}
RuleId Tag::ruleId() const
{
return m_ruleId;
}
QString Tag::appId() const
{
return m_appId;
}
QString Tag::tagId() const
{
return m_tagId;
}
QString Tag::value() const
{
return m_value;
}
void Tag::setValue(const QString &value)
{
m_value = value;
}
bool Tag::operator==(const Tag &other) const
{
return m_deviceId == other.deviceId() &&
m_ruleId == other.ruleId() &&
m_appId == other.appId() &&
m_tagId == other.tagId();
}
QDebug operator<<(QDebug dbg, const Tag &tag)
{
if (!tag.deviceId().isNull()) {
dbg.nospace() << "Tag (DeviceId:" << tag.deviceId();
} else {
dbg.nospace() << "Tag (RuleId:" << tag.ruleId();
}
dbg.nospace() << ", AppId:" << tag.appId() << ", TagId:" << tag.tagId() << ", Value:" << tag.value() << ")" << endl;
return dbg;
}
}

View File

@ -0,0 +1,57 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* nymea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 2 of the License. *
* *
* nymea 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 nymea. If not, see <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef TAG_H
#define TAG_H
#include "typeutils.h"
#include <QString>
namespace nymeaserver {
class Tag
{
public:
Tag(const DeviceId &deviceId, const QString &appId, const QString &tagId, const QString &value);
Tag(const RuleId &ruleId, const QString &appId, const QString &tagId, const QString &value);
DeviceId deviceId() const;
RuleId ruleId() const;
QString appId() const;
QString tagId() const;
QString value() const;
void setValue(const QString &value);
bool operator==(const Tag &other) const;
private:
DeviceId m_deviceId;
RuleId m_ruleId;
QString m_appId;
QString m_tagId;
QString m_value;
};
QDebug operator<<(QDebug dbg, const Tag &tag);
}
#endif // TAG_H

View File

@ -0,0 +1,185 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* nymea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 2 of the License. *
* *
* nymea 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 nymea. If not, see <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "tagsstorage.h"
#include "devicemanager.h"
#include "ruleengine.h"
#include "nymeasettings.h"
namespace nymeaserver {
TagsStorage::TagsStorage(DeviceManager *deviceManager, RuleEngine *ruleEngine, QObject *parent):
QObject(parent),
m_deviceManager(deviceManager),
m_ruleEngine(ruleEngine)
{
connect(deviceManager, &DeviceManager::deviceRemoved, this, &TagsStorage::deviceRemoved);
NymeaSettings settings(NymeaSettings::SettingsRoleTags);
settings.beginGroup("Devices");
foreach (const QString &deviceId, settings.childGroups()) {
settings.beginGroup(deviceId);
foreach (const QString &appId, settings.childGroups()) {
settings.beginGroup(appId);
foreach (const QString &tagId, settings.childKeys()) {
Tag tag(DeviceId(deviceId), appId, tagId, settings.value(tagId).toString());
m_tags.append(tag);
}
settings.endGroup();
}
settings.endGroup();
}
settings.endGroup();
settings.beginGroup("Rules");
foreach (const QString &ruleId, settings.childGroups()) {
settings.beginGroup(ruleId);
foreach (const QString &appId, settings.childGroups()) {
settings.beginGroup(appId);
foreach (const QString &tagId, settings.childKeys()) {
Tag tag(RuleId(ruleId), appId, tagId, settings.value(tagId).toString());
m_tags.append(tag);
}
settings.endGroup();
}
settings.endGroup();
}
settings.endGroup();
}
QList<Tag> TagsStorage::tags() const
{
return m_tags;
}
QList<Tag> TagsStorage::tags(const DeviceId &deviceId) const
{
QList<Tag> ret;
foreach (const Tag &tag, m_tags) {
if (tag.deviceId() == deviceId) {
ret.append(tag);
}
}
return ret;
}
QList<Tag> TagsStorage::tags(const RuleId &ruleId) const
{
QList<Tag> ret;
foreach (const Tag &tag, m_tags) {
if (tag.ruleId() == ruleId) {
ret.append(tag);
}
}
return ret;
}
TagsStorage::TagError TagsStorage::addTag(const Tag &tag)
{
if (!tag.deviceId().isNull()) {
if (!m_deviceManager->findConfiguredDevice(tag.deviceId())) {
return TagsStorage::TagErrorDeviceNotFound;
}
} else if (!tag.ruleId().isNull()) {
if (!m_ruleEngine->findRule(tag.ruleId()).isValid()) {
return TagsStorage::TagErrorRuleNotFound;
}
}
int index = m_tags.indexOf(tag);
if (index >= 0) {
m_tags.replace(index, tag);
emit tagValueChanged(tag);
} else {
m_tags.append(tag);
emit tagAdded(tag);
}
saveTag(tag);
return TagsStorage::TagErrorNoError;
}
TagsStorage::TagError TagsStorage::removeTag(const Tag &tag)
{
if (!m_tags.contains(tag)) {
return TagErrorTagNotFound;
}
m_tags.removeAll(tag);
unsaveTag(tag);
emit tagRemoved(tag);
return TagErrorNoError;
}
void TagsStorage::deviceRemoved(const DeviceId &deviceId)
{
QList<Tag> tagsToRemove;
foreach (const Tag &tag, m_tags) {
if (tag.deviceId() == deviceId) {
tagsToRemove.append(tag);
}
}
while (!tagsToRemove.isEmpty()) {
removeTag(tagsToRemove.takeFirst());
}
}
void TagsStorage::ruleRemoved(const RuleId &ruleId)
{
QList<Tag> tagsToRemove;
foreach (const Tag &tag, m_tags) {
if (tag.ruleId() == ruleId) {
tagsToRemove.append(tag);
}
}
while (!tagsToRemove.isEmpty()) {
removeTag(tagsToRemove.takeFirst());
}
}
void TagsStorage::saveTag(const Tag &tag)
{
NymeaSettings settings(NymeaSettings::SettingsRoleTags);
if (!tag.deviceId().isNull()) {
settings.beginGroup("Devices");
settings.beginGroup(tag.deviceId().toString());
} else {
settings.beginGroup("Rules");
settings.beginGroup(tag.ruleId().toString());
}
settings.beginGroup(tag.appId());
settings.setValue(tag.tagId(), tag.value());
}
void TagsStorage::unsaveTag(const Tag &tag)
{
NymeaSettings settings(NymeaSettings::SettingsRoleTags);
if (!tag.deviceId().isNull()) {
settings.beginGroup("Devices");
settings.beginGroup(tag.deviceId().toString());
} else {
settings.beginGroup("Rules");
settings.beginGroup(tag.deviceId().toString());
}
settings.beginGroup(tag.appId());
settings.remove(tag.tagId());
}
}

View File

@ -0,0 +1,78 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* nymea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 2 of the License. *
* *
* nymea 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 nymea. If not, see <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef TAGSSTORAGE_H
#define TAGSSTORAGE_H
#include "tag.h"
#include "typeutils.h"
#include <QObject>
#include <QVector>
class DeviceManager;
namespace nymeaserver {
class RuleEngine;
class TagsStorage : public QObject
{
Q_OBJECT
public:
enum TagError {
TagErrorNoError,
TagErrorDeviceNotFound,
TagErrorRuleNotFound,
TagErrorTagNotFound
};
Q_ENUM(TagError)
explicit TagsStorage(DeviceManager* deviceManager, RuleEngine* ruleEngine, QObject *parent = nullptr);
TagError addTag(const Tag &tag);
TagError removeTag(const Tag &tag);
QList<Tag> tags() const;
QList<Tag> tags(const DeviceId &deviceId) const;
QList<Tag> tags(const RuleId &ruleId) const;
signals:
void tagAdded(const Tag &tag);
void tagRemoved(const Tag &tag);
void tagValueChanged(const Tag &tag);
private slots:
void deviceRemoved(const DeviceId &deviceId);
void ruleRemoved(const RuleId &ruleId);
private:
void saveTag(const Tag &tag);
void unsaveTag(const Tag &tag);
private:
DeviceManager *m_deviceManager;
RuleEngine *m_ruleEngine;
QList<Tag> m_tags;
};
}
#endif // TAGSSTORAGE_H

View File

@ -99,6 +99,9 @@ NymeaSettings::NymeaSettings(const SettingsRole &role, QObject *parent):
case SettingsRoleDeviceStates:
fileName = "devicestates.conf";
break;
case SettingsRoleTags:
fileName = "tags.conf";
break;
}
m_settings = new QSettings(basePath + settingsPrefix + fileName, QSettings::IniFormat, this);
}

View File

@ -40,7 +40,8 @@ public:
SettingsRoleRules,
SettingsRolePlugins,
SettingsRoleGlobal,
SettingsRoleDeviceStates
SettingsRoleDeviceStates,
SettingsRoleTags
};
explicit NymeaSettings(const SettingsRole &role = SettingsRoleNone, QObject *parent = nullptr);

View File

@ -6,7 +6,7 @@ NYMEA_PLUGINS_PATH=/usr/lib/$$system('dpkg-architecture -q DEB_HOST_MULTIARCH')/
# define protocol versions
JSON_PROTOCOL_VERSION_MAJOR=1
JSON_PROTOCOL_VERSION_MINOR=5
JSON_PROTOCOL_VERSION_MINOR=6
REST_API_VERSION=1
DEFINES += NYMEA_VERSION_STRING=\\\"$${NYMEA_VERSION_STRING}\\\" \

View File

@ -1,4 +1,4 @@
1.5
1.6
{
"methods": {
"Actions.ExecuteAction": {
@ -789,6 +789,39 @@
"deviceError": "$ref:DeviceError",
"o:stateType": "$ref:StateType"
}
},
"Tags.AddTag": {
"description": "Add a Tag. A Tag must have a deviceId OR a ruleId (call this method twice if you want to attach the same tag to a device and a rule), an appId (Use the appId of your app), a tagId (e.g. \"favorites\") and a value. Upon success, a TagAdded notification will be emitted. Calling this method twice for the same ids (device/rule, appId and tagId) but with a different value will update the tag's value and the TagValueChanged notification will be emitted.",
"params": {
"tag": "$ref:Tag"
},
"returns": {
"tagError": "$ref:TagError"
}
},
"Tags.GetTags": {
"description": "Get the Tags matching the given filter. Tags can be filtered by a deviceID, a ruleId, an appId, a tagId or a combination of any (however, combining deviceId and ruleId will return an empty result set).",
"params": {
"o:appId": "String",
"o:deviceId": "Uuid",
"o:ruleId": "Uuid",
"o:tagId": "String"
},
"returns": {
"o:tags": [
"$ref:Tag"
],
"tagError": "$ref:TagError"
}
},
"Tags.RemoveTag": {
"description": "Remove a Tag. Tag value is optional and will be disregarded. If the ids match, the tag will be deleted and a TagRemoved notification will be emitted.",
"params": {
"tag": "$ref:Tag"
},
"returns": {
"tagError": "$ref:TagError"
}
}
},
"notifications": {
@ -962,6 +995,24 @@
"params": {
"ruleId": "Uuid"
}
},
"Tags.TagAdded": {
"description": "Emitted whenever a tag is added to the system. ",
"params": {
"tag": "$ref:Tag"
}
},
"Tags.TagRemoved": {
"description": "Emitted whenever a tag is removed from the system. ",
"params": {
"tag": "$ref:Tag"
}
},
"Tags.TagValueChanged": {
"description": "Emitted whenever a tag's value is changed in the system. ",
"params": {
"tag": "$ref:Tag"
}
}
},
"types": {
@ -1437,6 +1488,14 @@
"o:unit": "$ref:Unit",
"type": "$ref:BasicType"
},
"Tag": {
"appId": "String",
"o:deviceId": "Uuid",
"o:ruleId": "Uuid",
"o:value": "String",
"tagId": "String"
},
"TagError": "$ref:TagError",
"TimeDescriptor": {
"o:calendarItems": [
"$ref:CalendarItem"

View File

@ -22,3 +22,4 @@ SUBDIRS = versioning \
configurations \
timemanager \
userloading \
tags \

View File

@ -503,7 +503,7 @@ void TestJSONRPC::introspect()
foreach (const QString &ref, extractRefs(item)) {
QString typeId = ref;
typeId.remove("$ref:");
QVERIFY2(types.contains(typeId), QString("Undefined ref: %1").arg(ref).toLatin1().data());
QVERIFY2(types.contains(typeId), QString("Undefined ref: %1. Did you forget to add it to JsonTypes::allTypes()?").arg(ref).toLatin1().data());
}
}
}

View File

@ -154,6 +154,10 @@ protected:
verifyError(response, "configurationError", JsonTypes::configurationErrorToString(error));
}
inline void verifyTagError(const QVariant &response, TagsStorage::TagError error = TagsStorage::TagErrorNoError) {
verifyError(response, "tagError", JsonTypes::tagErrorToString(error));
}
inline void verifyParams(const QVariantList &requestList, const QVariantList &responseList, bool allRequired = true)
{
if (allRequired)

5
tests/auto/tags/tags.pro Normal file
View File

@ -0,0 +1,5 @@
include(../../../nymea.pri)
include(../autotests.pri)
TARGET = testtags
SOURCES += testtags.cpp

View File

@ -0,0 +1,248 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* nymea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 2 of the License. *
* *
**
* 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 nymea. If not, see <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "nymeatestbase.h"
#include "nymeacore.h"
#include "devicemanager.h"
#include "mocktcpserver.h"
#include <QtTest/QtTest>
#include <QCoreApplication>
#include <QTcpSocket>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QCoreApplication>
using namespace nymeaserver;
class TestTags: public NymeaTestBase
{
Q_OBJECT
private slots:
void addTag_data();
void addTag();
void updateTagValue();
void removeTag();
private:
QVariantMap createDeviceTag(const QString &deviceId, const QString &appId, const QString &tagId, const QString &value);
bool compareDeviceTag(const QVariantMap &tag, const QString &deviceId, const QString &appId, const QString &tagId, const QString &value);
QVariantMap createRuleTag(const QString &ruleId, const QString &appId, const QString &tagId, const QString &value);
bool comapreRuleTag(const QVariantMap &tag, const QString &ruleId, const QString &appId, const QString &tagId, const QString &value);
};
QVariantMap TestTags::createDeviceTag(const QString &deviceId, const QString &appId, const QString &tagId, const QString &value)
{
QVariantMap tag;
tag.insert("deviceId", deviceId);
tag.insert("appId", appId);
tag.insert("tagId", tagId);
tag.insert("value", value);
return tag;
}
QVariantMap TestTags::createRuleTag(const QString &ruleId, const QString &appId, const QString &tagId, const QString &value)
{
QVariantMap tag;
tag.insert("ruleId", ruleId);
tag.insert("appId", appId);
tag.insert("tagId", tagId);
tag.insert("value", value);
return tag;
}
bool TestTags::compareDeviceTag(const QVariantMap &tag, const QString &deviceId, const QString &appId, const QString &tagId, const QString &value)
{
return tag.value("deviceId").toString() == deviceId &&
tag.value("appId").toString() == appId &&
tag.value("tagId").toString() == tagId &&
tag.value("value").toString() == value;
}
void TestTags::addTag_data()
{
QTest::addColumn<DeviceId>("deviceId");
QTest::addColumn<QString>("appId");
QTest::addColumn<QString>("tagId");
QTest::addColumn<QString>("value");
QTest::addColumn<TagsStorage::TagError>("expectedError");
QTest::newRow("tagDevice") << m_mockDeviceId << "testtags" << "favorites" << "1" << TagsStorage::TagErrorNoError;
QTest::newRow("invalidDevice") << DeviceId::createDeviceId() << "testtags" << "favorites" << "1" << TagsStorage::TagErrorDeviceNotFound;
}
void TestTags::addTag()
{
QFETCH(DeviceId, deviceId);
QFETCH(QString, appId);
QFETCH(QString, tagId);
QFETCH(QString, value);
QFETCH(TagsStorage::TagError, expectedError);
// enable notificartions
QCOMPARE(enableNotifications(), true);
// Setup connection to mock client
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
// Create a tag;
QVariantMap params;
params.insert("tag", createDeviceTag(deviceId.toString(), appId, tagId, value));
QVariant response = injectAndWait("Tags.AddTag", params);
verifyTagError(response, expectedError);
if (expectedError != TagsStorage::TagErrorNoError) {
// If we expected an error, we can drop out here
return;
}
// Make sure the TagAdded notification is emitted.
QVariantMap notificationTagMap = checkNotification(clientSpy, "Tags.TagAdded").toMap().value("params").toMap().value("tag").toMap();
QJsonDocument jsonDoc = QJsonDocument::fromVariant(notificationTagMap);
QVERIFY2(compareDeviceTag(notificationTagMap, deviceId.toString(), appId, tagId, value), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1());
// Try getting the tag via GetTag
params.clear();
params.insert("deviceId", deviceId.toString());
params.insert("appId", appId);
params.insert("tagId", tagId);
response = injectAndWait("Tags.GetTags", params);
QVariantList tagsList = response.toMap().value("params").toMap().value("tags").toList();
QCOMPARE(tagsList.count(), 1);
QVERIFY2(compareDeviceTag(tagsList.first().toMap(), deviceId.toString(), appId, tagId, value), "Fetched tag isn't matching the one we added");
}
void TestTags::updateTagValue()
{
// enable notificartions
QCOMPARE(enableNotifications(), true);
// Setup connection to mock client
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
QString deviceId = m_mockDeviceId.toString();
QString appId = "testtags";
QString tagId = "changedNotificationTag";
// Create a Tag
QVariantMap params;
params.insert("tag", createDeviceTag(deviceId, appId, tagId, "1"));
QVariant response = injectAndWait("Tags.AddTag", params);
verifyTagError(response, TagsStorage::TagErrorNoError);
// Check for TagAdded notification
QVariantMap notificationTagMap = checkNotification(clientSpy, "Tags.TagAdded").toMap().value("params").toMap().value("tag").toMap();
QJsonDocument jsonDoc = QJsonDocument::fromVariant(notificationTagMap);
QVERIFY2(compareDeviceTag(notificationTagMap, deviceId, appId, tagId, "1"), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1());
clientSpy.clear();
// Try getting the changed tag via GetTag
params.clear();
params.insert("deviceId", deviceId);
params.insert("appId", appId);
params.insert("tagId", tagId);
response = injectAndWait("Tags.GetTags", params);
QVariantList tagsList = response.toMap().value("params").toMap().value("tags").toList();
QCOMPARE(tagsList.count(), 1);
QVERIFY2(compareDeviceTag(tagsList.first().toMap(), deviceId, appId, tagId, "1"), "Fetched tag isn't matching the one we added");
// Now update the tag
params.clear();
params.insert("tag", createDeviceTag(deviceId, appId, tagId, "2"));
response = injectAndWait("Tags.AddTag", params);
verifyTagError(response, TagsStorage::TagErrorNoError);
// Check for TagAdded notification
notificationTagMap = checkNotification(clientSpy, "Tags.TagValueChanged").toMap().value("params").toMap().value("tag").toMap();
jsonDoc = QJsonDocument::fromVariant(notificationTagMap);
QVERIFY2(compareDeviceTag(notificationTagMap, deviceId, appId, tagId, "2"), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1());
// Try getting the changed tag via GetTag
params.clear();
params.insert("deviceId", deviceId);
params.insert("appId", appId);
params.insert("tagId", tagId);
response = injectAndWait("Tags.GetTags", params);
tagsList = response.toMap().value("params").toMap().value("tags").toList();
QCOMPARE(tagsList.count(), 1);
QVERIFY2(compareDeviceTag(tagsList.first().toMap(), deviceId, appId, tagId, "2"), "Fetched tag isn't matching the one we added");
}
void TestTags::removeTag()
{
// enable notificartions
QCOMPARE(enableNotifications(), true);
// Setup connection to mock client
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
QString deviceId = m_mockDeviceId.toString();
QString appId = "testtags";
QString tagId = "removeTagTest";
QString value = "1";
// Create a Tag
QVariantMap params;
params.insert("tag", createDeviceTag(deviceId, appId, tagId, value));
QVariant response = injectAndWait("Tags.AddTag", params);
verifyTagError(response, TagsStorage::TagErrorNoError);
// Check for TagAdded notification
QVariantMap notificationTagMap = checkNotification(clientSpy, "Tags.TagAdded").toMap().value("params").toMap().value("tag").toMap();
QJsonDocument jsonDoc = QJsonDocument::fromVariant(notificationTagMap);
QVERIFY2(compareDeviceTag(notificationTagMap, deviceId, appId, tagId, value), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1());
clientSpy.clear();
// Try getting the tag via GetTag
params.clear();
params.insert("deviceId", deviceId);
params.insert("appId", appId);
params.insert("tagId", tagId);
response = injectAndWait("Tags.GetTags", params);
QVariantList tagsList = response.toMap().value("params").toMap().value("tags").toList();
QCOMPARE(tagsList.count(), 1);
QVERIFY2(compareDeviceTag(tagsList.first().toMap(), deviceId, appId, tagId, value), "Fetched tag isn't matching the one we added");
// Now remove the tag
params.clear();
params.insert("tag", createDeviceTag(deviceId, appId, tagId, QString()));
response = injectAndWait("Tags.RemoveTag", params);
verifyTagError(response, TagsStorage::TagErrorNoError);
// Check for TagRemoved notification
notificationTagMap = checkNotification(clientSpy, "Tags.TagRemoved").toMap().value("params").toMap().value("tag").toMap();
jsonDoc = QJsonDocument::fromVariant(notificationTagMap);
QVERIFY2(compareDeviceTag(notificationTagMap, deviceId, appId, tagId, QString()), QString("Tag in notification not matching: %1").arg(qUtf8Printable(jsonDoc.toJson())).toLatin1());
// Try getting the tag via GetTag
params.clear();
params.insert("deviceId", deviceId);
params.insert("appId", appId);
params.insert("tagId", tagId);
response = injectAndWait("Tags.GetTags", params);
tagsList = response.toMap().value("params").toMap().value("tags").toList();
QCOMPARE(tagsList.count(), 0);
}
#include "testtags.moc"
QTEST_MAIN(TestTags)

7
tests/scripts/gettags.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
if [ -z $1 ]; then
echo "usage: $0 host"
else
(echo '{"id":1, "method":"Tags.GetTags"}'; sleep 1) | nc $1 2222
fi

7
tests/scripts/tagdevice.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
if [ -z $5 ]; then
echo "usage: $0 host deviceId appId tagId value"
else
(echo '{"id":1, "method":"Tags.AddTag", "params": { "tag": {"deviceId": "'$2'", "appId": "'$3'", "tagId": "'$4'", "value":"'$5'"}}}'; sleep 1) | nc $1 2222
fi