Merge PR #337: Support for configuring logging and filtering states/events

This commit is contained in:
Jenkins nymea 2021-02-25 11:26:56 +01:00
commit 01e9ad8916
77 changed files with 990 additions and 202 deletions

View File

@ -506,6 +506,47 @@ Thing::ThingError ThingManagerImplementation::setThingSettings(const ThingId &th
return Thing::ThingErrorNoError;
}
Thing::ThingError ThingManagerImplementation::setEventLogging(const ThingId &thingId, const EventTypeId &eventTypeId, bool enabled)
{
Thing *thing = m_configuredThings.value(thingId);
if (!thing) {
qCWarning(dcThingManager()) << "Cannot configure event logging. Thing" << thingId.toString() << "not found";
return Thing::ThingErrorThingNotFound;
}
if (!thing->thingClass().eventTypes().findById(eventTypeId).isValid()) {
qCWarning(dcThingManager()) << "Cannot configure event logging. Thing" << thingId.toString() << "has no event type with id" << eventTypeId;
return Thing::ThingErrorEventTypeNotFound;
}
QList<EventTypeId> loggedEventTypes = thing->loggedEventTypeIds();
if (enabled && !loggedEventTypes.contains(eventTypeId)) {
loggedEventTypes.append(eventTypeId);
thing->setLoggedEventTypeIds(loggedEventTypes);
emit thingChanged(thing);
} else if (!enabled && loggedEventTypes.contains(eventTypeId)) {
loggedEventTypes.removeAll(eventTypeId);
thing->setLoggedEventTypeIds(loggedEventTypes);
emit thingChanged(thing);
}
return Thing::ThingErrorNoError;
}
Thing::ThingError ThingManagerImplementation::setStateFilter(const ThingId &thingId, const StateTypeId &stateTypeId, Types::StateValueFilter filter)
{
Thing *thing = m_configuredThings.value(thingId);
if (!thing) {
qCWarning(dcThingManager()) << "Cannot configure state filter. Thing" << thingId.toString() << "not found";
return Thing::ThingErrorThingNotFound;
}
if (!thing->thingClass().stateTypes().findById(stateTypeId).isValid()) {
qCWarning(dcThingManager()) << "Cannot configure state filter. Thing" << thingId.toString() << "has no state type with id" << stateTypeId;
return Thing::ThingErrorEventTypeNotFound;
}
thing->setStateValueFilter(stateTypeId, filter);
emit thingChanged(thing);
return Thing::ThingErrorNoError;
}
ThingPairingInfo* ThingManagerImplementation::pairThing(const ThingClassId &thingClassId, const ParamList &params, const QString &name)
{
PairingTransactionId transactionId = PairingTransactionId::createPairingTransactionId();
@ -650,6 +691,14 @@ ThingPairingInfo *ThingManagerImplementation::confirmPairing(const PairingTransa
ParamList settings = buildParams(thingClass.settingsTypes(), ParamList());
thing->setSettings(settings);
QList<EventTypeId> loggedEventTypeIds;
foreach (const EventType &eventType, thingClass.eventTypes()) {
if (eventType.suggestLogging()) {
loggedEventTypeIds.append(eventType.id());
}
}
thing->setLoggedEventTypeIds(loggedEventTypeIds);
ThingSetupInfo *info = setupThing(thing);
connect(info, &ThingSetupInfo::finished, thing, [this, info, externalInfo, addNewThing](){
@ -746,6 +795,14 @@ ThingSetupInfo* ThingManagerImplementation::addConfiguredThingInternal(const Thi
ParamList settings = buildParams(thingClass.settingsTypes(), ParamList());
thing->setSettings(settings);
QList<EventTypeId> loggedEventTypeIds;
foreach (const EventType &eventType, thingClass.eventTypes()) {
if (eventType.suggestLogging()) {
loggedEventTypeIds.append(eventType.id());
}
}
thing->setLoggedEventTypeIds(loggedEventTypeIds);
ThingSetupInfo *info = setupThing(thing);
connect(info, &ThingSetupInfo::finished, this, [this, info](){
if (info->status() != Thing::ThingErrorNoError) {
@ -1258,6 +1315,10 @@ ThingActionInfo *ThingManagerImplementation::executeAction(const Action &action)
return info;
}
connect(info, &ThingActionInfo::finished, this, [=](){
emit actionExecuted(action, info->status());
});
plugin->executeAction(info);
return info;
@ -1541,6 +1602,14 @@ void ThingManagerImplementation::loadConfiguredThings()
thing->setSettings(thingSettings);
QList<EventTypeId> loggedEventTypeIds;
foreach (const EventType &eventType, thingClass.eventTypes()) {
if (eventType.suggestLogging()) {
loggedEventTypeIds.append(eventType.id());
}
}
thing->setLoggedEventTypeIds(loggedEventTypeIds);
settings.endGroup(); // ThingId
// We always add the thing to the list in this case. If it's in the stored things
@ -1659,6 +1728,14 @@ void ThingManagerImplementation::onAutoThingsAppeared(const ThingDescriptors &th
thing->setSettings(settings);
thing->setParentId(thingDescriptor.parentId());
QList<EventTypeId> loggedEventTypeIds;
foreach (const EventType &eventType, thingClass.eventTypes()) {
if (eventType.suggestLogging()) {
loggedEventTypeIds.append(eventType.id());
}
}
thing->setLoggedEventTypeIds(loggedEventTypeIds);
qCDebug(dcThingManager()) << "Setting up auto thing:" << thing->name() << thing->id().toString();
ThingSetupInfo *info = setupThing(thing);
@ -1725,7 +1802,7 @@ void ThingManagerImplementation::cleanupThingStateCache()
}
}
void ThingManagerImplementation::onEventTriggered(const Event &event)
void ThingManagerImplementation::onEventTriggered(Event event)
{
// Doing some sanity checks here...
Thing *thing = m_configuredThings.value(event.thingId());
@ -1738,7 +1815,12 @@ void ThingManagerImplementation::onEventTriggered(const Event &event)
qCWarning(dcThingManager()) << "The given thing does not have an event type of id " + event.eventTypeId().toString() + ". Not forwarding event.";
return;
}
// All good, forward the event
// configure logging
if (thing->loggedEventTypeIds().contains(event.eventTypeId())) {
event.setLogged(true);
}
// Forward the event
emit eventTriggered(event);
}
@ -1755,7 +1837,7 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
Param valueParam(ParamTypeId(stateTypeId.toString()), value);
Event event(EventTypeId(stateTypeId.toString()), thing->id(), ParamList() << valueParam, true);
emit eventTriggered(event);
onEventTriggered(event);
syncIOConnection(thing, stateTypeId);
}
@ -2022,6 +2104,7 @@ void ThingManagerImplementation::loadThingStates(Thing *thing)
} else {
thing->setStateValue(stateType.id(), stateType.defaultValue());
}
thing->setStateValueFilter(stateType.id(), stateType.filter());
}
settings.endGroup();
}
@ -2179,7 +2262,7 @@ IntegrationPlugin *ThingManagerImplementation::createCppIntegrationPlugin(const
return nullptr;
}
pluginIface->setMetaData(PluginMetadata(pluginInfo));
pluginIface->setMetaData(metaData);
return pluginIface;
}

View File

@ -106,6 +106,9 @@ public:
Thing::ThingError editThing(const ThingId &thingId, const QString &name) override;
Thing::ThingError setThingSettings(const ThingId &thingId, const ParamList &settings) override;
Thing::ThingError setEventLogging(const ThingId &thingId, const EventTypeId &eventTypeId, bool enabled) override;
Thing::ThingError setStateFilter(const ThingId &thingId, const StateTypeId &stateTypeId, Types::StateValueFilter filter) override;
Thing::ThingError removeConfiguredThing(const ThingId &thingId) override;
ThingActionInfo* executeAction(const Action &action) override;
@ -140,7 +143,7 @@ private slots:
void onAutoThingDisappeared(const ThingId &thingId);
void onLoaded();
void cleanupThingStateCache();
void onEventTriggered(const Event &event);
void onEventTriggered(Event event);
// Only connect this to Things. It will query the sender()
void slotThingStateValueChanged(const StateTypeId &stateTypeId, const QVariant &value);

View File

@ -107,7 +107,7 @@ JsonReply* ActionHandler::ExecuteAction(const QVariantMap &params, const JsonCon
JsonReply *jsonReply = createAsyncReply("ExecuteAction");
ThingActionInfo *info = NymeaCore::instance()->executeAction(action);
ThingActionInfo *info = NymeaCore::instance()->thingManager()->executeAction(action);
connect(info, &ThingActionInfo::finished, jsonReply, [info, jsonReply, locale](){
QVariantMap data;
data.insert("deviceError", enumValueName(info->status()).replace("Thing", "Device"));

View File

@ -62,6 +62,7 @@ DeviceHandler::DeviceHandler(QObject *parent) :
registerEnum<Types::Unit>();
registerEnum<Types::InputType>();
registerEnum<Types::IOType>();
registerEnum<Types::StateValueFilter>();
registerEnum<RuleEngine::RemovePolicy>();
registerEnum<BrowserItem::BrowserIcon>();
registerEnum<MediaBrowserItem::MediaBrowserIcon>();
@ -900,7 +901,7 @@ JsonReply *DeviceHandler::ExecuteAction(const QVariantMap &params, const JsonCon
JsonReply *jsonReply = createAsyncReply("ExecuteAction");
ThingActionInfo *info = NymeaCore::instance()->executeAction(action);
ThingActionInfo *info = NymeaCore::instance()->thingManager()->executeAction(action);
connect(info, &ThingActionInfo::finished, jsonReply, [info, jsonReply, locale](){
QVariantMap data;
data.insert("deviceError", enumValueName(info->status()).replace("Thing", "Device"));

View File

@ -61,6 +61,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
registerEnum<Types::Unit>();
registerEnum<Types::InputType>();
registerEnum<Types::IOType>();
registerEnum<Types::StateValueFilter>();
registerEnum<RuleEngine::RemovePolicy>();
registerEnum<BrowserItem::BrowserIcon>();
registerEnum<MediaBrowserItem::MediaBrowserIcon>();
@ -241,6 +242,22 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
returns.insert("thingError", enumRef<Thing::ThingError>());
registerMethod("SetThingSettings", description, params, returns);
params.clear(); returns.clear();
description = "Enable/disable logging for the given event type on the given thing.";
params.insert("thingId", enumValueName(Uuid));
params.insert("eventTypeId", enumValueName(Uuid));
params.insert("enabled", enumValueName(Bool));
returns.insert("thingError", enumRef<Thing::ThingError>());
registerMethod("SetEventLogging", description, params, returns);
params.clear(); returns.clear();
description = "Set the filter for the given state on the given thing.";
params.insert("thingId", enumValueName(Uuid));
params.insert("stateTypeId", enumValueName(Uuid));
params.insert("filter", enumRef<Types::StateValueFilter>());
returns.insert("thingError", enumRef<Thing::ThingError>());
registerMethod("SetStateFilter", description, params, returns);
params.clear(); returns.clear();
description = "Remove a thing from the system.";
params.insert("thingId", enumValueName(Uuid));
@ -838,6 +855,26 @@ JsonReply *IntegrationsHandler::SetThingSettings(const QVariantMap &params)
return createReply(statusToReply(status));
}
JsonReply *IntegrationsHandler::SetEventLogging(const QVariantMap &params)
{
ThingId thingId = ThingId(params.value("thingId").toString());
EventTypeId eventTypeId = EventTypeId(params.value("eventTypeId").toUuid());
bool enabled = params.value("enabled").toBool();
Thing::ThingError status = NymeaCore::instance()->thingManager()->setEventLogging(thingId, eventTypeId, enabled);
return createReply(statusToReply(status));
}
JsonReply *IntegrationsHandler::SetStateFilter(const QVariantMap &params)
{
ThingId thingId = ThingId(params.value("thingId").toString());
StateTypeId stateTypeId = StateTypeId(params.value("stateTypeId").toUuid());
QString filterString = params.value("filter").toString();
QMetaEnum metaEnum = QMetaEnum::fromType<Types::StateValueFilter>();
Types::StateValueFilter filter = static_cast<Types::StateValueFilter>(metaEnum.keyToValue(filterString.toUtf8()));
Thing::ThingError status = NymeaCore::instance()->thingManager()->setStateFilter(thingId, stateTypeId, filter);
return createReply(statusToReply(status));
}
JsonReply* IntegrationsHandler::GetEventTypes(const QVariantMap &params, const JsonContext &context) const
{
ThingClass thingClass = NymeaCore::instance()->thingManager()->findThingClass(ThingClassId(params.value("thingClassId").toString()));
@ -958,7 +995,7 @@ JsonReply *IntegrationsHandler::ExecuteAction(const QVariantMap &params, const J
JsonReply *jsonReply = createAsyncReply("ExecuteAction");
ThingActionInfo *info = NymeaCore::instance()->executeAction(action);
ThingActionInfo *info = NymeaCore::instance()->thingManager()->executeAction(action);
connect(info, &ThingActionInfo::finished, jsonReply, [info, jsonReply, locale](){
QVariantMap data;
data.insert("thingError", enumValueName(info->status()));

View File

@ -59,6 +59,8 @@ public:
Q_INVOKABLE JsonReply *EditThing(const QVariantMap &params);
Q_INVOKABLE JsonReply *RemoveThing(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetThingSettings(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetEventLogging(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetStateFilter(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetEventTypes(const QVariantMap &params, const JsonContext &context) const;
Q_INVOKABLE JsonReply *GetActionTypes(const QVariantMap &params, const JsonContext &context) const;

View File

@ -34,6 +34,8 @@
#include "logging.h"
#include "logvaluetool.h"
#include "integrations/thingmanager.h"
#include <QCoreApplication>
#include <QSqlDatabase>
#include <QSqlDriver>
@ -104,6 +106,13 @@ LogEngine::~LogEngine()
m_db.close();
}
void LogEngine::setThingManager(ThingManager *thingManager)
{
m_thingManager = thingManager;
connect(thingManager, &ThingManager::eventTriggered, this, &LogEngine::logEvent);
connect(thingManager, &ThingManager::actionExecuted, this, &LogEngine::logAction);
}
LogEntriesFetchJob *LogEngine::fetchLogEntries(const LogFilter &filter)
{
QList<LogEntry> results;
@ -214,6 +223,7 @@ void LogEngine::clearDatabase()
void LogEngine::logSystemEvent(const QDateTime &dateTime, bool active, Logging::LoggingLevel level)
{
qCDebug(dcLogEngine()) << "Logging system event:" << active;
LogEntry entry(dateTime, level, Logging::LoggingSourceSystem);
entry.setEventType(Logging::LoggingEventTypeActiveChange);
entry.setActive(active);
@ -222,6 +232,10 @@ void LogEngine::logSystemEvent(const QDateTime &dateTime, bool active, Logging::
void LogEngine::logEvent(const Event &event)
{
if (!event.logged()) {
return;
}
QVariantList valueList;
Logging::LoggingSource sourceType;
if (event.isStateChangeEvent()) {
@ -249,9 +263,10 @@ void LogEngine::logEvent(const Event &event)
appendLogEntry(entry);
}
void LogEngine::logAction(const Action &action, Logging::LoggingLevel level, int errorCode)
void LogEngine::logAction(const Action &action, Thing::ThingError status)
{
LogEntry entry(level, Logging::LoggingSourceActions, errorCode);
Logging::LoggingLevel level = status == Thing::ThingErrorNoError ? Logging::LoggingLevelInfo : Logging::LoggingLevelAlert;
LogEntry entry(QDateTime::currentDateTime(), level, Logging::LoggingSourceActions, status);
entry.setTypeId(action.actionTypeId());
entry.setThingId(action.thingId());

View File

@ -38,6 +38,7 @@
#include "types/browseritemaction.h"
#include "types/browseraction.h"
#include "ruleengine/rule.h"
#include "integrations/thingmanager.h"
#include <QObject>
#include <QSqlDatabase>
@ -60,6 +61,8 @@ public:
LogEngine(const QString &driver, const QString &dbName, const QString &hostname = QString("127.0.0.1"), const QString &username = QString(), const QString &password = QString(), int maxDBSize = 50000, QObject *parent = nullptr);
~LogEngine();
void setThingManager(ThingManager *thingManager);
LogEntriesFetchJob *fetchLogEntries(const LogFilter &filter = LogFilter());
ThingsFetchJob *fetchThings();
@ -68,9 +71,11 @@ public:
void setMaxLogEntries(int maxLogEntries, int trimSize);
void clearDatabase();
void removeThingLogs(const ThingId &thingId);
void removeRuleLogs(const RuleId &ruleId);
public slots:
void logSystemEvent(const QDateTime &dateTime, bool active, Logging::LoggingLevel level = Logging::LoggingLevelInfo);
void logEvent(const Event &event);
void logAction(const Action &action, Logging::LoggingLevel level = Logging::LoggingLevelInfo, int errorCode = 0);
void logBrowserAction(const BrowserAction &browserAction, Logging::LoggingLevel level = Logging::LoggingLevelInfo, int errorCode = 0);
void logBrowserItemAction(const BrowserItemAction &browserItemAction, Logging::LoggingLevel level = Logging::LoggingLevelInfo, int errorCode = 0);
void logRuleTriggered(const Rule &rule);
@ -78,8 +83,10 @@ public:
void logRuleEnabledChanged(const Rule &rule, const bool &enabled);
void logRuleActionsExecuted(const Rule &rule);
void logRuleExitActionsExecuted(const Rule &rule);
void removeThingLogs(const ThingId &thingId);
void removeRuleLogs(const RuleId &ruleId);
private slots:
void logEvent(const Event &event);
void logAction(const Action &action, Thing::ThingError status);
signals:
void logEntryAdded(const LogEntry &logEntry);
@ -114,6 +121,8 @@ private:
bool m_initialized = false;
bool m_dbMalformed = false;
ThingManager *m_thingManager = nullptr;
// When maxQueueLength is exceeded, jobs will be flagged and discarded if this source logs more events
int m_maxQueueLength;
QHash<QString, QList<DatabaseJob*>> m_flaggedJobs;

View File

@ -99,9 +99,6 @@ void NymeaCore::init(const QStringList &additionalInterfaces) {
}
m_timeManager = new TimeManager(this);
qCDebug(dcCore) << "Creating Log Engine";
m_logger = new LogEngine(m_configuration->logDBDriver(), m_configuration->logDBName(), m_configuration->logDBHost(), m_configuration->logDBUser(), m_configuration->logDBPassword(), m_configuration->logDBMaxEntries(), this);
qCDebug(dcCore()) << "Creating User Manager";
m_userManager = new UserManager(NymeaSettings::settingsPath() + "/user-db.sqlite", this);
@ -120,6 +117,10 @@ void NymeaCore::init(const QStringList &additionalInterfaces) {
qCDebug(dcCore) << "Creating Rule Engine";
m_ruleEngine = new RuleEngine(this);
qCDebug(dcCore) << "Creating Log Engine";
m_logger = new LogEngine(m_configuration->logDBDriver(), m_configuration->logDBName(), m_configuration->logDBHost(), m_configuration->logDBUser(), m_configuration->logDBPassword(), m_configuration->logDBMaxEntries(), this);
m_logger->setThingManager(m_thingManager);
qCDebug(dcCore()) << "Creating Script Engine";
m_scriptEngine = new ScriptEngine(m_thingManager, this);
m_serverManager->jsonServer()->registerHandler(new ScriptsHandler(m_scriptEngine, m_scriptEngine));
@ -380,20 +381,6 @@ Thing::ThingError NymeaCore::removeConfiguredThing(const ThingId &thingId, const
return removeError;
}
ThingActionInfo* NymeaCore::executeAction(const Action &action)
{
ThingActionInfo *info = m_thingManager->executeAction(action);
connect(info, &ThingActionInfo::finished, this, [this, info](){
if (info->status() == Thing::ThingErrorNoError) {
m_logger->logAction(info->action());
} else {
m_logger->logAction(info->action(), Logging::LoggingLevelAlert, info->status());
}
});
return info;
}
BrowserActionInfo* NymeaCore::executeBrowserItem(const BrowserAction &browserAction)
{
BrowserActionInfo *info = m_thingManager->executeBrowserItem(browserAction);
@ -512,7 +499,7 @@ void NymeaCore::executeRuleActions(const QList<RuleAction> ruleActions)
foreach (const Action &action, actions) {
qCDebug(dcRuleEngine) << "Executing action" << action.actionTypeId() << action.params();
ThingActionInfo *info = executeAction(action);
ThingActionInfo *info = m_thingManager->executeAction(action);
connect(info, &ThingActionInfo::finished, this, [info](){
if (info->status() != Thing::ThingErrorNoError) {
qCWarning(dcRuleEngine) << "Error executing action:" << info->status() << info->displayMessage();
@ -660,7 +647,6 @@ ZigbeeManager *NymeaCore::zigbeeManager() const
void NymeaCore::gotEvent(const Event &event)
{
m_logger->logEvent(event);
emit eventTriggered(event);
QList<RuleAction> actions;

View File

@ -84,7 +84,6 @@ public:
QPair<Thing::ThingError, QList<RuleId> >removeConfiguredThing(const ThingId &thingId, const QHash<RuleId, RuleEngine::RemovePolicy> &removePolicyList);
Thing::ThingError removeConfiguredThing(const ThingId &thingId, const RuleEngine::RemovePolicy &removePolicy);
ThingActionInfo *executeAction(const Action &action);
BrowserActionInfo* executeBrowserItem(const BrowserAction &browserAction);
BrowserItemActionInfo* executeBrowserItemAction(const BrowserItemAction &browserItemAction);

View File

@ -301,7 +301,6 @@ void WebServer::readClient()
HttpRequest request;
if (m_incompleteRequests.contains(socket)) {
qCDebug(dcWebServer()) << "Append data to incomlete request";
request = m_incompleteRequests.take(socket);
request.appendData(data);
} else {

View File

@ -318,7 +318,11 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
QJsonObject st = stateTypesJson.toObject();
bool writableState = false;
QPair<QStringList, QStringList> verificationResult = verifyFields(StateType::typeProperties(), StateType::mandatoryTypeProperties(), st);
QStringList stateTypeProperties = {"id", "name", "displayName", "displayNameEvent", "type", "defaultValue", "cached",
"unit", "minValue", "maxValue", "possibleValues", "writable", "displayNameAction",
"ioType", "suggestLogging", "filter"};
QStringList mandatoryStateTypeProperties = {"id", "name", "displayName", "displayNameEvent", "type", "defaultValue"};
QPair<QStringList, QStringList> verificationResult = verifyFields(stateTypeProperties, mandatoryStateTypeProperties, st);
// Check mandatory fields
if (!verificationResult.first.isEmpty()) {
@ -470,6 +474,18 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
}
stateType.setIOType(ioType);
}
stateType.setSuggestLogging(st.value("suggestLogging").toBool());
if (st.contains("filter")) {
QString filter = st.value("filter").toString();
if (filter == "adaptive") {
stateType.setFilter(Types::StateValueFilterAdaptive);
} else if (!filter.isEmpty()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid filter value \"" + filter + "\". Supported filters are: \"adaptive\"");
hasError = true;
}
}
stateTypes.append(stateType);
// Events for state changed (Not checking for duplicate UUID, this is expected to be the same as the state!)
@ -485,6 +501,7 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
paramType.setUnit(stateType.unit());
eventType.setParamTypes(QList<ParamType>() << paramType);
eventType.setIndex(stateType.index());
eventType.setSuggestLogging(st.value("suggestLogging").toBool());
eventTypes.append(eventType);
// ActionTypes for writeable StateTypes
@ -497,7 +514,6 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
actionTypes.append(actionType);
}
}
thingClass.setStateTypes(stateTypes);
// ActionTypes
index = 0;
@ -544,7 +560,6 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
actionTypes.append(actionType);
}
thingClass.setActionTypes(actionTypes);
// EventTypes
index = 0;
@ -580,6 +595,7 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
EventType eventType(eventTypeId);
eventType.setName(eventTypeName);
eventType.setDisplayName(et.value("displayName").toString());
eventType.setSuggestLogging(et.value("suggestLogging").toBool());
eventType.setIndex(index++);
QPair<bool, QList<ParamType> > paramVerification = parseParamTypes(et.value("paramTypes").toArray());
@ -590,7 +606,6 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
}
eventTypes.append(eventType);
}
thingClass.setEventTypes(eventTypes);
// BrowserItemActionTypes
index = 0;
@ -637,7 +652,6 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
browserItemActionTypes.append(actionType);
}
thingClass.setBrowserItemActionTypes(browserItemActionTypes);
// Read interfaces
QStringList interfaces;
@ -646,22 +660,18 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
if (!iface.isValid()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" uses non-existing interface \"" + value.toString() + "\"");
hasError = true;
continue;
}
StateTypes stateTypes(thingClass.stateTypes());
ActionTypes actionTypes(thingClass.actionTypes());
EventTypes eventTypes(thingClass.eventTypes());
foreach (const InterfaceStateType &ifaceStateType, iface.stateTypes()) {
StateType stateType = stateTypes.findByName(ifaceStateType.name());
if (stateType.id().isNull()) {
if (!stateTypes.contains(ifaceStateType.name())) {
if (!ifaceStateType.optional()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but doesn't implement state \"" + ifaceStateType.name() + "\"");
hasError = true;
} else {
continue;
}
continue;
}
StateType &stateType = stateTypes[ifaceStateType.name()];
if (ifaceStateType.type() != stateType.type()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + stateType.name() + "\" has not matching type: \"" + QVariant::typeToName(stateType.type()) + "\" != \"" + QVariant::typeToName(ifaceStateType.type()) + "\"");
hasError = true;
@ -697,18 +707,22 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but state \"" + stateType.name() + "\" has not matching unit: \"" + unitEnum.valueToKey(ifaceStateType.unit()) + "\" != \"" + unitEnum.valueToKey(stateType.unit()));
hasError = true;
}
// Override logged property as the interface has higher priority than the plugin dev
if (ifaceStateType.loggingOverride()) {
stateType.setSuggestLogging(ifaceStateType.suggestLogging());
}
}
foreach (const InterfaceActionType &ifaceActionType, iface.actionTypes()) {
ActionType actionType = actionTypes.findByName(ifaceActionType.name());
if (actionType.id().isNull()) {
if (!actionTypes.contains(ifaceActionType.name())) {
if (!ifaceActionType.optional()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but doesn't implement action \"" + ifaceActionType.name() + "\"");
hasError = true;
} else {
continue;
}
continue;
}
ActionType &actionType = actionTypes[ifaceActionType.name()];
// Verify the params as required by the interface are available
foreach (const ParamType &ifaceActionParamType, ifaceActionType.paramTypes()) {
ParamType paramType = actionType.paramTypes().findByName(ifaceActionParamType.name());
@ -779,15 +793,14 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
}
foreach (const InterfaceEventType &ifaceEventType, iface.eventTypes()) {
EventType eventType = eventTypes.findByName(ifaceEventType.name());
if (!eventType.isValid()) {
if (!eventTypes.contains(ifaceEventType.name())) {
if (!ifaceEventType.optional()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" claims to implement interface \"" + value.toString() + "\" but doesn't implement event \"" + ifaceEventType.name() + "\"");
hasError = true;
} else {
continue;
}
continue;
}
EventType &eventType = eventTypes[ifaceEventType.name()];
// Verify all the params as required by the interface are available
foreach (const ParamType &ifaceEventParamType, ifaceEventType.paramTypes()) {
@ -842,6 +855,11 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
}
}
// Override logging
if (ifaceEventType.loggingOverride()) {
eventType.setSuggestLogging(ifaceEventType.suggestLogging());
}
// Note: No need to check for default values (as with actions) for additional params as
// an emitted event always needs to have params filled with values. The client might use them or not...
}
@ -851,6 +869,11 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
interfaces.removeDuplicates();
thingClass.setInterfaces(interfaces);
thingClass.setStateTypes(stateTypes);
thingClass.setActionTypes(actionTypes);
thingClass.setEventTypes(eventTypes);
thingClass.setBrowserItemActionTypes(browserItemActionTypes);
m_thingClasses.append(thingClass);
}
}

View File

@ -0,0 +1,45 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "statevaluefilter.h"
#include "loggingcategories.h"
NYMEA_LOGGING_CATEGORY(dcStateValueFilter, "StateValueFilter")
StateValueFilter::StateValueFilter()
{
}
StateValueFilter::~StateValueFilter()
{
}

View File

@ -0,0 +1,51 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef STATEVALUEFILTER_H
#define STATEVALUEFILTER_H
#include <QVariant>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcStateValueFilter)
class StateValueFilter
{
public:
StateValueFilter();
virtual ~StateValueFilter();
virtual void addValue(const QVariant &value) = 0;
virtual QVariant filteredValue() const = 0;
};
#endif // STATEVALUEFILTER_H

View File

@ -0,0 +1,142 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "statevaluefilteradaptive.h"
#include <qmath.h>
StateValueFilterAdaptive::StateValueFilterAdaptive()
{
}
void StateValueFilterAdaptive::addValue(const QVariant &value)
{
m_inputValues.prepend(value.toDouble());
m_inputValueCount++;
update();
}
QVariant StateValueFilterAdaptive::filteredValue() const
{
return m_outputValue;
}
void StateValueFilterAdaptive::update()
{
while (m_inputValues.count() > m_windowSize) {
m_inputValues.removeLast();
}
if (m_inputValues.isEmpty()) {
m_outputValue = 0;
return;
}
if (m_inputValues.count() == 1) {
// Not enough data
m_outputValue = m_inputValues.first();
m_outputValueCount++;
return;
}
double currentValue = m_inputValues.first();
if (qFuzzyCompare(currentValue, 0)) {
// If we went to 0, follow right away.
m_outputValue = 0;
return;
}
// Calculate average of history, for all values and for all but the last one
double sum = 0;
for (int i = 0; i < m_inputValues.count(); i++) {
sum += m_inputValues.at(i);
}
double normalizedValue = sum / m_inputValues.count();
double previousNormalizedValue = (sum - m_inputValues.first()) / (m_inputValues.count() - 1);
if (qFuzzyCompare(previousNormalizedValue, 0)) {
// We can't calculate anything if the history is at 0. Follow right away to the new value.
m_outputValue = currentValue;
m_outputValueCount++;
return;
}
// Calculate change ratio of the last value compared to the previous one, unflitered and filtered
double changeRatioToAverage = 1 - qAbs(currentValue / previousNormalizedValue);
double changeRatioToCurrentOutput = 1 - qAbs(currentValue / m_outputValue);
double changeRatioFiltered = 1 - qAbs(normalizedValue / previousNormalizedValue);
// If the unfiltered value changes for more than 3 times the standard deviation of the jittering values
// it's a 99% chance a big change happened that's not jitter (e.g turned on/off)
// Discard the history and follow the new value right away
if (qAbs(changeRatioToAverage) > m_standardDeviation * 3) {
m_inputValues.clear();
m_inputValues.prepend(currentValue);
m_totalDeviation = 0;
if (!qFuzzyCompare(m_outputValue, normalizedValue)) {
m_outputValue = currentValue;
qCDebug(dcStateValueFilter()) << "Updating output value:" << m_outputValue << "(input exceeds max jitter)";
m_outputValueCount++;
}
// We're considering it jitter
} else {
// Add up the deviation from the current actual value to the currently filtered value
m_totalDeviation += changeRatioToCurrentOutput;
// If the filtered value changed for more than the the standard deviation, follow slowly
// In order to not get stuck on being off for the standard deviation forever, also move closer
// to the new value when the summed up deviation exceeds the maximum allowed total deviation
if (qAbs(changeRatioFiltered) > m_standardDeviation || qAbs(m_totalDeviation) > m_maxTotalDeviation) {
m_totalDeviation = 0;
if (!qFuzzyCompare(m_outputValue, normalizedValue)) {
qCDebug(dcStateValueFilter()) << "Updating output value:" << normalizedValue << "(drift compensation)";
m_outputValue = normalizedValue;
m_outputValueCount++;
}
}
// Poor mans solution to calculate standard deviation. Not as precise, but much faster than looping over history again
m_standardDeviation = ((m_standardDeviation * m_windowSize) + qAbs(changeRatioToAverage)) / (m_windowSize + 1);
}
// reset stats on overflow of counters
if (m_inputValueCount < m_outputValueCount) {
m_outputValueCount = 0;
}
qCDebug(dcStateValueFilter()) << "Filter statistics for" << this;
qCDebug(dcStateValueFilter()) << "Input:" << currentValue << "AVG:" << previousNormalizedValue << "Filtered:" << normalizedValue;
qCDebug(dcStateValueFilter()) << "Change ratios: Input/average:" << changeRatioToAverage << "Filtered/average:" << changeRatioFiltered << "Input/output:" << changeRatioToCurrentOutput;
qCDebug(dcStateValueFilter()) << "Std deviation:" << m_standardDeviation << "Total deviation:" << m_totalDeviation;
qCDebug(dcStateValueFilter()) << "Compression ratio:" << (1.0 * m_inputValueCount / m_outputValueCount) << "(" << m_outputValueCount << "/" << m_inputValueCount << ")";
}

View File

@ -0,0 +1,65 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef STATEVALUEFILTERADAPTIVE_H
#define STATEVALUEFILTERADAPTIVE_H
#include "statevaluefilter.h"
class StateValueFilterAdaptive : public StateValueFilter
{
public:
StateValueFilterAdaptive();
void addValue(const QVariant &value) override;
QVariant filteredValue() const override;
private:
void update();
private:
QList<double> m_inputValues;
int m_windowSize = 20;
double m_standardDeviation = 0.05;
double m_maxTotalDeviation = 0.4;
double m_totalDeviation = 0;
double m_outputValue = 0;
// Stats for debugging
quint64 m_inputValueCount = 0;
quint64 m_outputValueCount = 0;
};
#endif // STATEVALUEFILTERADAPTIVE_H

View File

@ -133,6 +133,7 @@
#include "thing.h"
#include "types/event.h"
#include "loggingcategories.h"
#include "statevaluefilters/statevaluefilteradaptive.h"
#include <QDebug>
@ -334,9 +335,6 @@ void Thing::setStateValue(const StateTypeId &stateTypeId, const QVariant &value)
}
for (int i = 0; i < m_states.count(); ++i) {
if (m_states.at(i).stateTypeId() == stateTypeId) {
if (m_states.at(i).value() == value)
return;
QVariant newValue = value;
if (!newValue.convert(stateType.type())) {
qCWarning(dcThing()).nospace() << m_name << ": Invalid value " << value << " for state " << stateType.name() << ". Type mismatch. Expected type: " << QVariant::typeToName(stateType.type()) << " (Discarding change)";
@ -355,8 +353,13 @@ void Thing::setStateValue(const StateTypeId &stateTypeId, const QVariant &value)
return;
}
QVariant oldValue = m_states.at(i).value();
StateValueFilter *filter = m_stateValueFilters.value(stateTypeId);
if (filter) {
filter->addValue(newValue);
newValue = filter->filteredValue();
}
QVariant oldValue = m_states.at(i).value();
if (oldValue == newValue) {
qCDebug(dcThing()).nospace() << m_name << ": Discarding state change for " << stateType.name() << " as the value did not actually change. Old value:" << oldValue << "New value:" << newValue;
return;
@ -383,6 +386,11 @@ State Thing::state(const StateTypeId &stateTypeId) const
return State(StateTypeId(), ThingId());
}
QList<EventTypeId> Thing::loggedEventTypeIds() const
{
return m_loggedEventTypeIds;
}
/*! Returns the \l{ThingId} of the parent of this thing. If the parentId
is not set, this thing does not have a parent.
*/
@ -442,6 +450,27 @@ void Thing::setSetupStatus(Thing::ThingSetupStatus status, Thing::ThingError set
emit setupStatusChanged();
}
void Thing::setLoggedEventTypeIds(const QList<EventTypeId> loggedEventTypeIds)
{
m_loggedEventTypeIds = loggedEventTypeIds;
}
void Thing::setStateValueFilter(const StateTypeId &stateTypeId, Types::StateValueFilter filter)
{
for (int i = 0; i < m_states.count(); i++) {
if (m_states.at(i).stateTypeId() == stateTypeId) {
m_states[i].setFilter(filter);
StateValueFilter *stateValueFilter = m_stateValueFilters.take(stateTypeId);
if (stateValueFilter) {
delete stateValueFilter;
}
if (filter == Types::StateValueFilterAdaptive) {
m_stateValueFilters.insert(stateTypeId, new StateValueFilterAdaptive());
}
}
}
}
Things::Things(const QList<Thing*> &other)
{
foreach (Thing* thing, other) {

View File

@ -45,6 +45,7 @@
#include <QVariant>
class IntegrationPlugin;
class StateValueFilter;
class LIBNYMEA_EXPORT Thing: public QObject
{
@ -60,6 +61,7 @@ class LIBNYMEA_EXPORT Thing: public QObject
Q_PROPERTY(QString setupDisplayMessage READ setupDisplayMessage NOTIFY setupStatusChanged USER true)
Q_PROPERTY(ThingError setupError READ setupError NOTIFY setupStatusChanged)
Q_PROPERTY(QUuid parentId READ parentId USER true)
Q_PROPERTY(QList<EventTypeId> loggedEventTypeIds READ loggedEventTypeIds USER true)
public:
enum ThingError {
@ -133,6 +135,8 @@ public:
Q_INVOKABLE State state(const StateTypeId &stateTypeId) const;
QList<EventTypeId> loggedEventTypeIds() const;
ThingId parentId() const;
void setParentId(const ThingId &parentId);
@ -161,6 +165,8 @@ private:
Thing(const PluginId &pluginId, const ThingClass &thingClass, QObject *parent = nullptr);
void setSetupStatus(ThingSetupStatus status, ThingError setupError, const QString &displayMessage = QString());
void setLoggedEventTypeIds(const QList<EventTypeId> loggedEventTypeIds);
void setStateValueFilter(const StateTypeId &stateTypeId, Types::StateValueFilter filter);
private:
ThingClass m_thingClass;
@ -176,6 +182,9 @@ private:
ThingSetupStatus m_setupStatus = ThingSetupStatusNone;
ThingError m_setupError = ThingErrorNoError;
QString m_setupDisplayMessage;
QList<EventTypeId> m_loggedEventTypeIds;
QHash<StateTypeId, StateValueFilter*> m_stateValueFilters;
};
QDebug operator<<(QDebug dbg, Thing *device);

View File

@ -81,6 +81,9 @@ public:
virtual Thing::ThingError editThing(const ThingId &thingId, const QString &name) = 0;
virtual Thing::ThingError setThingSettings(const ThingId &thingId, const ParamList &settings) = 0;
virtual Thing::ThingError setEventLogging(const ThingId &thingId, const EventTypeId &eventTypeId, bool enabled) = 0;
virtual Thing::ThingError setStateFilter(const ThingId &thingId, const StateTypeId &stateTypeId, Types::StateValueFilter filter) = 0;
virtual Thing::ThingError removeConfiguredThing(const ThingId &thingId) = 0;
virtual ThingActionInfo* executeAction(const Action &action) = 0;
@ -112,10 +115,11 @@ signals:
void thingRemoved(const ThingId &thingId);
void thingDisappeared(const ThingId &thingId);
void thingAdded(Thing *thing);
void thingChanged(Thing *device);
void thingChanged(Thing *thing);
void thingSettingChanged(const ThingId &thingId, const ParamTypeId &settingParamTypeId, const QVariant &value);
void ioConnectionAdded(const IOConnection &ioConnection);
void ioConnectionRemoved(const IOConnectionId &ioConnectionId);
void actionExecuted(const Action &action, Thing::ThingError status);
};
#endif // THINGMANAGER_H

View File

@ -200,6 +200,10 @@ Interface ThingUtils::loadInterface(const QString &name)
stateType.setMinValue(stateVariant.toMap().value("minValue"));
stateType.setMaxValue(stateVariant.toMap().value("maxValue"));
stateType.setOptional(stateVariant.toMap().value("optional", false).toBool());
if (stateVariant.toMap().contains("logged")) {
stateType.setLoggingOverride(true);
stateType.setSuggestLogging(stateVariant.toMap().value("logged", false).toBool());
}
if (stateVariant.toMap().contains("unit")) {
QMetaEnum unitEnum = QMetaEnum::fromType<Types::Unit>();
int enumValue = unitEnum.keyToValue("Unit" + stateVariant.toMap().value("unit").toByteArray());
@ -214,6 +218,8 @@ Interface ThingUtils::loadInterface(const QString &name)
InterfaceEventType stateChangeEventType;
stateChangeEventType.setName(stateType.name());
stateChangeEventType.setOptional(stateType.optional());
stateChangeEventType.setSuggestLogging(stateType.suggestLogging());
stateChangeEventType.setLoggingOverride(stateType.loggingOverride());
ParamType stateChangeEventParamType;
stateChangeEventParamType.setName(stateType.name());
stateChangeEventParamType.setType(stateType.type());
@ -236,6 +242,10 @@ Interface ThingUtils::loadInterface(const QString &name)
InterfaceActionType actionType;
actionType.setName(actionVariant.toMap().value("name").toString());
actionType.setOptional(actionVariant.toMap().value("optional").toBool());
// if (actionVariant.toMap().contains("logged")) {
// actionType.setLoggingOverride(true);
// actionType.setSuggestLogging(actionVariant.toMap().value("logged").toBool());
// }
ParamTypes paramTypes;
foreach (const QVariant &actionParamVariant, actionVariant.toMap().value("params").toList()) {
ParamType paramType;
@ -254,6 +264,10 @@ Interface ThingUtils::loadInterface(const QString &name)
InterfaceEventType eventType;
eventType.setName(eventVariant.toMap().value("name").toString());
eventType.setOptional(eventVariant.toMap().value("optional").toBool());
if (eventVariant.toMap().contains("logged")) {
eventType.setLoggingOverride(true);
eventType.setSuggestLogging(eventVariant.toMap().value("logged").toBool());
}
ParamTypes paramTypes;
foreach (const QVariant &eventParamVariant, eventVariant.toMap().value("params").toList()) {
ParamType paramType;

View File

@ -2,7 +2,8 @@
"states": [
{
"name": "batteryCritical",
"type": "bool"
"type": "bool",
"logged": true
}
]
}

View File

@ -2,7 +2,8 @@
"description": "The base for all buttons that emit a pressed event.",
"events": [
{
"name": "pressed"
"name": "pressed",
"logged": true
}
]
}

View File

@ -4,7 +4,8 @@
"states": [
{
"name": "closed",
"type": "bool"
"type": "bool",
"logged": true
}
]
}

View File

@ -5,7 +5,8 @@
{
"name": "co2",
"type": "double",
"unit": "PartsPerMillion"
"unit": "PartsPerMillion",
"logged": true
}
]
}

View File

@ -4,7 +4,8 @@
{
"name": "conductivity",
"type": "double",
"unit": "MicroSiemensPerCentimeter"
"unit": "MicroSiemensPerCentimeter",
"logged": true
}
]
}

View File

@ -3,7 +3,8 @@
{
"name": "connected",
"type": "bool",
"defaultValue": false
"defaultValue": false,
"logged": true
}
]
}

View File

@ -4,7 +4,8 @@
"states": [
{
"name": "daylight",
"type": "bool"
"type": "bool",
"logged": true
},
{
"name": "sunriseTime",

View File

@ -2,7 +2,8 @@
"description": "An interface for doorbells. Emits \"doorbellPressed\" when the doorbell is pressed.",
"events": [
{
"name": "doorbellPressed"
"name": "doorbellPressed",
"logged": true
}
]
}

View File

@ -4,7 +4,8 @@
"states": [
{
"name": "moving",
"type": "bool"
"type": "bool",
"logged": true
},
{
"name": "percentage",

View File

@ -5,7 +5,8 @@
{
"name": "currentPower",
"type": "double",
"unit": "Watt"
"unit": "Watt",
"logged": true
}
]
}

View File

@ -4,7 +4,9 @@
"states": [
{
"name": "currentPower",
"type": "double"
"type": "double",
"unit": "Watt",
"logged": true
}
]
}

View File

@ -24,7 +24,8 @@
"PinkyRight"
]
}
]
],
"logged": true
}
],
"actions": [

View File

@ -5,7 +5,8 @@
{
"name": "state",
"type": "QString",
"allowedValues": ["open", "closed", "opening", "closing"]
"allowedValues": ["open", "closed", "opening", "closing"],
"logged": true
},
{
"name": "intermediatePosition",

View File

@ -5,7 +5,8 @@
"name": "humidity",
"type": "double",
"minValue": 0,
"maxValue": 100
"maxValue": 100,
"logged": true
}
]
}

View File

@ -3,7 +3,8 @@
"description": "This interface is for the simplest form of garage doors which can be controlled only via an impulse. Triggering the impulse will start moving the door, triggering it again will stop the movement. Triggering it yet another time will start movement in the reverse direction. Note that there is no feedback channel on such devices. The system has no chance of knowing the current state this device is actually in.",
"actions": [
{
"name": "triggerImpulse"
"name": "triggerImpulse",
"logged": true
}
]
}

View File

@ -1,7 +1,8 @@
{
"events": [
{
"name": "triggered"
"name": "triggered",
"logged": true
}
]
}

View File

@ -4,7 +4,8 @@
{
"name": "lightIntensity",
"type": "double",
"unit": "Lux"
"unit": "Lux",
"logged": true
}
]
}

View File

@ -3,7 +3,8 @@
"extends": "button",
"events": [
{
"name": "longPressed"
"name": "longPressed",
"logged": true
}
]
}

View File

@ -9,7 +9,8 @@
"name": "buttonName",
"type": "QString"
}
]
],
"logged": true
}
]
}

View File

@ -3,7 +3,8 @@
"states": [
{
"name": "moisture",
"type": "double"
"type": "double",
"logged": true
}
]
}

View File

@ -9,7 +9,8 @@
"name": "buttonName",
"type": "QString"
}
]
],
"logged": true
}
]
}

View File

@ -5,7 +5,8 @@
{
"name": "noise",
"type": "double",
"unit": "Dezibel"
"unit": "Dezibel",
"logged": true
}
]
}

View File

@ -11,8 +11,8 @@
"name": "body",
"type": "QString"
}
]
],
"logged": true
}
]
}

View File

@ -3,7 +3,8 @@
{
"name": "power",
"type": "bool",
"writable": true
"writable": true,
"logged": true
}
]
}

View File

@ -4,7 +4,8 @@
"states": [
{
"name": "isPresent",
"type": "bool"
"type": "bool",
"logged": true
},
{
"name": "lastSeenTime",

View File

@ -4,7 +4,8 @@
{
"name": "pressure",
"type": "double",
"unit": "MilliBar"
"unit": "MilliBar",
"logged": true
}
]
}

View File

@ -4,7 +4,8 @@
{
"name": "state",
"type": "QString",
"allowedValues": ["locked", "locking", "unlocked", "unlocking", "unlatched", "unlatching"]
"allowedValues": ["locked", "locking", "unlocked", "unlocking", "unlatched", "unlatching"],
"logged": true
}
],
"actions": [

View File

@ -5,7 +5,8 @@
{
"name": "state",
"type": "QString",
"allowedValues": ["open", "closed", "opening", "closing", "intermediate"]
"allowedValues": ["open", "closed", "opening", "closing", "intermediate"],
"logged": true
}
]
}

View File

@ -4,7 +4,8 @@
{
"name": "temperature",
"type": "double",
"unit": "DegreeCelsius"
"unit": "DegreeCelsius",
"logged": true
}
]
}

View File

@ -14,7 +14,8 @@
"name": "userId",
"type": "QString"
}
]
],
"logged": true
}
],
"actions": [
@ -25,8 +26,8 @@
"name": "userId",
"type": "QString"
}
]
],
"logged": true
},
{
"name": "removeUser",
@ -36,7 +37,8 @@
"type": "QString"
}
]
],
"logged": true
}
]
}

View File

@ -2,32 +2,39 @@
"states": [
{
"name": "weatherDescription",
"type": "QString"
"type": "QString",
"logged": true
},
{
"name": "weatherCondition",
"type": "QString",
"allowedValues": ["clear-day", "clear-night", "few-clouds-day", "few-clouds-night", "clouds", "overcast", "light-rain", "shower-rain", "thunderstorm", "snow", "fog"]
"allowedValues": ["clear-day", "clear-night", "few-clouds-day", "few-clouds-night", "clouds", "overcast", "light-rain", "shower-rain", "thunderstorm", "snow", "fog"],
"logged": true
},
{
"name": "temperature",
"type": "double"
"type": "double",
"logged": true
},
{
"name": "humidity",
"type": "int"
"type": "int",
"logged": true
},
{
"name": "pressure",
"type": "double"
"type": "double",
"logged": true
},
{
"name": "windSpeed",
"type": "double"
"type": "double",
"logged": true
},
{
"name": "windDirection",
"type": "int"
"type": "int",
"logged": true
}
]
}

View File

@ -5,7 +5,8 @@
{
"name": "windSpeed",
"type": "double",
"unit": "MeterPerSecond"
"unit": "MeterPerSecond",
"logged": true
}
]
}

View File

@ -29,7 +29,7 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "jsonhandler.h"
#include "typeutils.h"
#include "loggingcategories.h"
#include <QDebug>
@ -207,6 +207,9 @@ void JsonHandler::registerObject(const QMetaObject &metaObject)
typeName = QString("$ref:BasicType");
} else if (QString(metaProperty.typeName()).startsWith("QList")) {
QString elementType = QString(metaProperty.typeName()).remove("QList<").remove(">");
if (elementType == "EventTypeId" || elementType == "StateTypeId" || elementType == "ActionTypeId") {
elementType = "QUuid";
}
QVariant::Type variantType = QVariant::nameToType(elementType.toUtf8());
typeName = QVariantList() << enumValueName(variantTypeToBasicType(variantType));
} else {
@ -344,6 +347,18 @@ QVariant JsonHandler::pack(const QMetaObject &metaObject, const void *value) con
foreach (const QUuid &entry, propertyValue.value<QList<QUuid>>()) {
list << entry;
}
} else if (propertyTypeName == "QList<EventTypeId>") {
foreach (const EventTypeId &entry, propertyValue.value<QList<EventTypeId>>()) {
list << entry;
}
} else if (propertyTypeName == "QList<StateTypeId>") {
foreach (const EventTypeId &entry, propertyValue.value<QList<EventTypeId>>()) {
list << entry;
}
} else if (propertyTypeName == "QList<ActionTypeId>") {
foreach (const EventTypeId &entry, propertyValue.value<QList<EventTypeId>>()) {
list << entry;
}
} else {
Q_ASSERT_X(false, this->metaObject()->className(), QString("Unhandled list type: %1").arg(propertyTypeName).toUtf8());
qCWarning(dcJsonRpc()) << "Cannot pack property of unhandled list type" << propertyTypeName;
@ -455,7 +470,10 @@ QVariant JsonHandler::unpack(const QMetaObject &metaObject, const QVariant &valu
intList.append(val.toInt());
}
metaProperty.writeOnGadget(ptr, QVariant::fromValue(intList));
} else if (metaProperty.typeName() == QStringLiteral("QList<QUuid>")) {
} else if (metaProperty.typeName() == QStringLiteral("QList<QUuid>")
|| metaProperty.typeName() == QStringLiteral("QList<EventTypeId>")
|| metaProperty.typeName() == QStringLiteral("QList<StateTypeId>")
|| metaProperty.typeName() == QStringLiteral("QList<ActionTypeId>")) {
QList<QUuid> uuidList;
foreach (const QVariant &val, variant.toList()) {
uuidList.append(val.toUuid());

View File

@ -126,6 +126,8 @@ SOURCES += \
integrations/thingsetupinfo.cpp \
integrations/thingutils.cpp \
integrations/servicedata.cpp \
integrations/statevaluefilters/statevaluefilter.cpp \
integrations/statevaluefilters/statevaluefilteradaptive.cpp \
jsonrpc/jsoncontext.cpp \
jsonrpc/jsonhandler.cpp \
jsonrpc/jsonreply.cpp \

View File

@ -130,6 +130,26 @@ ActionTypes::ActionTypes(const QList<ActionType> &other)
}
}
bool ActionTypes::contains(const ActionTypeId &id) const
{
foreach (const ActionType &actionType, *this) {
if (actionType.id() == id) {
return true;
}
}
return false;
}
bool ActionTypes::contains(const QString &name) const
{
foreach (const ActionType &actionType, *this) {
if (actionType.name() == name) {
return true;
}
}
return false;
}
QVariant ActionTypes::get(int index) const
{
return QVariant::fromValue(at(index));
@ -160,8 +180,21 @@ ActionType ActionTypes::findById(const ActionTypeId &id)
return ActionType(ActionTypeId());
}
ActionType &ActionTypes::operator[](const QString &name)
{
int index = -1;
for (int i = 0; i < count(); i++) {
if (at(i).name() == name) {
index = i;
break;
}
}
return QList::operator[](index);
}
QDebug operator<<(QDebug dbg, const ActionType &actionType)
{
dbg.nospace().noquote() << "ActionType: " << actionType.name() << actionType.displayName() << actionType.id();
return dbg;
}

View File

@ -84,10 +84,13 @@ class ActionTypes: public QList<ActionType>
public:
ActionTypes() = default;
ActionTypes(const QList<ActionType> &other);
bool contains(const ActionTypeId &id) const;
bool contains(const QString &name) const;
Q_INVOKABLE QVariant get(int index) const;
Q_INVOKABLE void put(const QVariant &variant);
ActionType findByName(const QString &name);
ActionType findById(const ActionTypeId &id);
ActionType &operator[](const QString &name);
};
Q_DECLARE_METATYPE(ActionTypes)

View File

@ -126,6 +126,16 @@ bool Event::isStateChangeEvent() const
return m_isStateChangeEvent;
}
bool Event::logged() const
{
return m_logged;
}
void Event::setLogged(bool logged)
{
m_logged = logged;
}
/*! Compare this Event to the Event given by \a other.
* Events are equal (returns true) if eventTypeId, deviceId and params match. */
bool Event::operator ==(const Event &other) const

View File

@ -65,12 +65,16 @@ public:
bool isStateChangeEvent() const;
bool logged() const;
void setLogged(bool logged);
private:
EventTypeId m_eventTypeId;
ThingId m_thingId;
ParamList m_params;
bool m_isStateChangeEvent;
bool m_logged = false;
};
Q_DECLARE_METATYPE(Event)
QDebug operator<<(QDebug dbg, const Event &event);

View File

@ -110,6 +110,16 @@ void EventType::setParamTypes(const ParamTypes &paramTypes)
m_paramTypes = paramTypes;
}
bool EventType::suggestLogging() const
{
return m_logged;
}
void EventType::setSuggestLogging(bool logged)
{
m_logged = logged;
}
/*! Returns true if this EventType has a valid id and name */
bool EventType::isValid() const
{
@ -130,11 +140,31 @@ QStringList EventType::mandatoryTypeProperties()
EventTypes::EventTypes(const QList<EventType> &other)
{
foreach (const EventType &at, other) {
append(at);
foreach (const EventType &et, other) {
append(et);
}
}
bool EventTypes::contains(const EventTypeId &id) const
{
foreach (const EventType &eventType, *this) {
if (eventType.id() == id) {
return true;
}
}
return false;
}
bool EventTypes::contains(const QString &name) const
{
foreach (const EventType &eventType, *this) {
if (eventType.name() == name) {
return true;
}
}
return false;
}
QVariant EventTypes::get(int index) const
{
return QVariant::fromValue(at(index));
@ -164,3 +194,15 @@ EventType EventTypes::findById(const EventTypeId &id)
}
return EventType(EventTypeId());
}
EventType &EventTypes::operator[](const QString &name)
{
int index = -1;
for (int i = 0; i < count(); i++) {
if (at(i).name() == name) {
index = i;
break;
}
}
return QList::operator[](index);
}

View File

@ -64,6 +64,9 @@ public:
ParamTypes paramTypes() const;
void setParamTypes(const ParamTypes &paramTypes);
bool suggestLogging() const;
void setSuggestLogging(bool logged);
bool isValid() const;
static QStringList typeProperties();
@ -75,6 +78,7 @@ private:
QString m_displayName;
int m_index;
QList<ParamType> m_paramTypes;
bool m_logged = false;
};
Q_DECLARE_METATYPE(EventType)
@ -85,10 +89,13 @@ class EventTypes: public QList<EventType>
public:
EventTypes() = default;
EventTypes(const QList<EventType> &other);
bool contains(const EventTypeId &id) const;
bool contains(const QString &name) const;
Q_INVOKABLE QVariant get(int index) const;
Q_INVOKABLE void put(const QVariant &variant);
EventType findByName(const QString &name);
EventType findById(const EventTypeId &id);
EventType &operator[](const QString &name);
};
Q_DECLARE_METATYPE(EventTypes)

View File

@ -15,6 +15,16 @@ void InterfaceEventType::setOptional(bool optional)
m_optional = optional;
}
bool InterfaceEventType::loggingOverride() const
{
return m_loggingOverride;
}
void InterfaceEventType::setLoggingOverride(bool loggingOverride)
{
m_loggingOverride = loggingOverride;
}
InterfaceEventTypes::InterfaceEventTypes(const QList<InterfaceEventType> &other):
QList<InterfaceEventType>(other)
{

View File

@ -11,8 +11,12 @@ public:
bool optional() const;
void setOptional(bool optional);
bool loggingOverride() const;
void setLoggingOverride(bool loggingOverride);
private:
bool m_optional = false;
bool m_loggingOverride = false;
};
class InterfaceEventTypes: public QList<InterfaceEventType>

View File

@ -15,6 +15,16 @@ void InterfaceStateType::setOptional(bool optional)
m_optional = optional;
}
bool InterfaceStateType::loggingOverride() const
{
return m_loggingOverride;
}
void InterfaceStateType::setLoggingOverride(bool loggingOverride)
{
m_loggingOverride = loggingOverride;
}
InterfaceStateTypes::InterfaceStateTypes(const QList<InterfaceStateType> &other):
QList<InterfaceStateType>(other)
{

View File

@ -11,8 +11,12 @@ public:
bool optional() const;
void setOptional(bool optional);
bool loggingOverride() const;
void setLoggingOverride(bool loggingOverride);
private:
bool m_optional = false;
bool m_loggingOverride = false;
};
class InterfaceStateTypes: public QList<InterfaceStateType>

View File

@ -80,6 +80,16 @@ void State::setValue(const QVariant &value)
m_value = value;
}
Types::StateValueFilter State::filter() const
{
return m_filter;
}
void State::setFilter(Types::StateValueFilter filter)
{
m_filter = filter;
}
/*! Writes the stateTypeId, the deviceId and the value of the given \a state to \a dbg. */
QDebug operator<<(QDebug dbg, const State &state)
{

View File

@ -42,6 +42,7 @@ class LIBNYMEA_EXPORT State
Q_GADGET
Q_PROPERTY(QUuid stateTypeId READ stateTypeId)
Q_PROPERTY(QVariant value READ value)
Q_PROPERTY(Types::StateValueFilter filter READ filter)
public:
State();
@ -53,10 +54,14 @@ public:
QVariant value() const;
void setValue(const QVariant &value);
Types::StateValueFilter filter() const;
void setFilter(Types::StateValueFilter filter);
private:
StateTypeId m_stateTypeId;
ThingId m_thingId;
QVariant m_value;
Types::StateValueFilter m_filter = Types::StateValueFilterNone;
};
Q_DECLARE_METATYPE(State)

View File

@ -208,18 +208,24 @@ void StateType::setCached(bool cached)
m_cached = cached;
}
/*! Returns a list of all valid properties a DeviceClass definition can have. */
QStringList StateType::typeProperties()
bool StateType::suggestLogging() const
{
return QStringList() << "id" << "name" << "displayName" << "displayNameEvent" << "type" << "defaultValue"
<< "cached" << "unit" << "minValue" << "maxValue" << "possibleValues" << "writable"
<< "displayNameAction" << "ioType";
return m_logged;
}
/*! Returns a list of mandatory properties a DeviceClass definition must have. */
QStringList StateType::mandatoryTypeProperties()
void StateType::setSuggestLogging(bool logged)
{
return QStringList() << "id" << "name" << "displayName" << "displayNameEvent" << "type" << "defaultValue";
m_logged = logged;
}
Types::StateValueFilter StateType::filter() const
{
return m_filter;
}
void StateType::setFilter(Types::StateValueFilter filter)
{
m_filter = filter;
}
/*! Returns true if this state type has an ID, a type and a name set. */
@ -245,6 +251,16 @@ bool StateTypes::contains(const StateTypeId &stateTypeId)
return false;
}
bool StateTypes::contains(const QString &name)
{
foreach (const StateType &stateType, *this) {
if (stateType.name() == name) {
return true;
}
}
return false;
}
QVariant StateTypes::get(int index) const
{
return QVariant::fromValue(at(index));
@ -274,3 +290,15 @@ StateType StateTypes::findById(const StateTypeId &id)
}
return StateType(StateTypeId());
}
StateType &StateTypes::operator[](const QString &name)
{
int index = -1;
for (int i = 0; i < count(); i++) {
if (at(i).name() == name) {
index = i;
break;
}
}
return QList::operator[](index);
}

View File

@ -93,8 +93,11 @@ public:
bool cached() const;
void setCached(bool cached);
static QStringList typeProperties();
static QStringList mandatoryTypeProperties();
bool suggestLogging() const;
void setSuggestLogging(bool logged);
Types::StateValueFilter filter() const;
void setFilter(Types::StateValueFilter filter);
bool isValid() const;
@ -112,6 +115,8 @@ private:
Types::IOType m_ioType = Types::IOTypeNone;
bool m_writable = false;
bool m_cached = true;
bool m_logged = false;
Types::StateValueFilter m_filter = Types::StateValueFilterNone;
};
Q_DECLARE_METATYPE(StateType)
@ -123,10 +128,12 @@ public:
StateTypes() = default;
StateTypes(const QList<StateType> &other);
bool contains(const StateTypeId &stateTypeId);
bool contains(const QString &name);
Q_INVOKABLE QVariant get(int index) const;
Q_INVOKABLE void put(const QVariant &variant);
StateType findByName(const QString &name);
StateType findById(const StateTypeId &id);
StateType &operator[](const QString &name);
};
Q_DECLARE_METATYPE(StateTypes)

View File

@ -173,6 +173,12 @@ public:
IOTypeAnalogOutput
};
Q_ENUM(IOType)
enum StateValueFilter {
StateValueFilterNone,
StateValueFilterAdaptive
};
Q_ENUM(StateValueFilter)
};
Q_DECLARE_METATYPE(Types::InputType)

View File

@ -5,7 +5,7 @@ NYMEA_VERSION_STRING=$$system('dpkg-parsechangelog | sed -n -e "s/^Version: //p"
# define protocol versions
JSON_PROTOCOL_VERSION_MAJOR=5
JSON_PROTOCOL_VERSION_MINOR=3
JSON_PROTOCOL_VERSION_MINOR=4
JSON_PROTOCOL_VERSION="$${JSON_PROTOCOL_VERSION_MAJOR}.$${JSON_PROTOCOL_VERSION_MINOR}"
LIBNYMEA_API_VERSION_MAJOR=7
LIBNYMEA_API_VERSION_MINOR=0

View File

@ -1,5 +1,10 @@
/* This file is generated by the nymea build system. Any changes to this file will *
* be lost. If you want to change this file, edit the plugin's json file. */
* be lost. If you want to change this file, edit the plugin's json file. *
* *
* NOTE: This file can be included only once per plugin. If you need to access *
* definitions from this file in multiple source files, use *
* #include extern-plugininfo.h *
* instead and re-run qmake. */
#ifndef EXTERNPLUGININFO_H
#define EXTERNPLUGININFO_H

View File

@ -104,7 +104,7 @@ void HttpDaemon::readClient()
} else if (stateTypeId == mockDoubleStateTypeId) {
stateValue.convert(QVariant::Double);
}
qCDebug(dcMock()) << "Set state value" << stateValue;
qCDebug(dcMock()) << "Setting state value" << stateValue;
emit setState(stateTypeId, stateValue);
} else if (url.path() == "/generateevent") {
emit triggerEvent(EventTypeId(query.queryItemValue("eventtypeid")));

View File

@ -82,7 +82,8 @@
"displayName": "Dummy int state",
"displayNameEvent": "Dummy int state changed",
"defaultValue": 10,
"type": "int"
"type": "int",
"suggestLogging": true
},
{
"id": "9dd6a97c-dfd1-43dc-acbd-367932742310",
@ -152,7 +153,8 @@
"minValue": 0,
"maxValue": 100,
"defaultValue": 50,
"writable": true
"writable": true,
"filter": "adaptive"
},
{
"id": "ebc41327-53d5-40c2-8e7b-1164a8ff359e",
@ -307,7 +309,8 @@
"displayName": "Dummy int state",
"displayNameEvent": "Dummy int state changed",
"defaultValue": 10,
"type": "int"
"type": "int",
"suggestLogging": true
},
{
"id": "978b0ba5-d008-41bd-b63d-a3bd23cb6469",

View File

@ -1,5 +1,10 @@
/* This file is generated by the nymea build system. Any changes to this file will *
* be lost. If you want to change this file, edit the plugin's json file. */
* be lost. If you want to change this file, edit the plugin's json file. *
* *
* NOTE: This file can be included only once per plugin. If you need to access *
* definitions from this file in multiple source files, use *
* #include extern-plugininfo.h *
* instead and re-run qmake. */
#ifndef PLUGININFO_H
#define PLUGININFO_H

View File

@ -1,4 +1,4 @@
5.3
5.4
{
"enums": {
"BasicType": [
@ -244,6 +244,10 @@
"StateOperatorAnd",
"StateOperatorOr"
],
"StateValueFilter": [
"StateValueFilterNone",
"StateValueFilterAdaptive"
],
"TagError": [
"TagErrorNoError",
"TagErrorThingNotFound",
@ -1215,6 +1219,17 @@
"thingError": "$ref:ThingError"
}
},
"Integrations.SetEventLogging": {
"description": "Enable/disable logging for the given event type on the given thing.",
"params": {
"enabled": "Bool",
"eventTypeId": "Uuid",
"thingId": "Uuid"
},
"returns": {
"thingError": "$ref:ThingError"
}
},
"Integrations.SetPluginConfiguration": {
"description": "Set a plugin's params.",
"params": {
@ -1225,6 +1240,17 @@
"thingError": "$ref:ThingError"
}
},
"Integrations.SetStateFilter": {
"description": "Set the filter for the given state on the given thing.",
"params": {
"filter": "$ref:StateValueFilter",
"stateTypeId": "Uuid",
"thingId": "Uuid"
},
"returns": {
"thingError": "$ref:ThingError"
}
},
"Integrations.SetThingSettings": {
"description": "Change the settings of a thing.",
"params": {
@ -2521,6 +2547,9 @@
"o:settings": "$ref:ParamList",
"r:deviceClassId": "Uuid",
"r:id": "Uuid",
"r:o:loggedEventTypeIds": [
"Uuid"
],
"r:o:parentId": "Uuid",
"r:o:setupDisplayMessage": "String",
"r:params": "$ref:ParamList",
@ -2774,6 +2803,7 @@
"sslEnabled": "Bool"
},
"State": {
"r:filter": "$ref:StateValueFilter",
"r:stateTypeId": "Uuid",
"r:value": "Variant"
},
@ -2833,6 +2863,9 @@
"o:name": "String",
"o:settings": "$ref:ParamList",
"r:id": "Uuid",
"r:o:loggedEventTypeIds": [
"Uuid"
],
"r:o:parentId": "Uuid",
"r:o:setupDisplayMessage": "String",
"r:params": "$ref:ParamList",

View File

@ -852,22 +852,6 @@ void TestJSONRPC::ruleActiveChangedNotifications()
clientSpy.wait();
waitForDBSync();
// Make sure the logg notification contains all the stuff we expect
QVariantList logEntryAddedVariants = checkNotifications(clientSpy, "Logging.LogEntryAdded");
QVERIFY2(!logEntryAddedVariants.isEmpty(), "Did not get Logging.LogEntryAdded notification.");
bool found = false;
foreach (const QVariant &loggEntryAddedVariant, logEntryAddedVariants) {
if (loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("typeId").toUuid() == mockIntStateTypeId) {
found = true;
QCOMPARE(loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("source").toString(), QString("LoggingSourceStates"));
QCOMPARE(loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("value").toInt(), 20);
break;
}
}
QVERIFY2(found, "LogEntryAdded notification not received");
spy.clear(); clientSpy.clear();
// set the rule inactive
@ -889,22 +873,6 @@ void TestJSONRPC::ruleActiveChangedNotifications()
clientSpy.wait();
}
// Make sure the logg notification contains all the stuff we expect
logEntryAddedVariants = checkNotifications(clientSpy, "Logging.LogEntryAdded");
QVERIFY2(!logEntryAddedVariants.isEmpty(), "Did not get Logging.LogEntryAdded notification.");
found = false;
foreach (const QVariant &logEntryAddedVariant, logEntryAddedVariants) {
qCDebug(dcTests()) << "Checking log entry" << mockIntStateTypeId << qUtf8Printable(QJsonDocument::fromVariant(logEntryAddedVariant).toJson());
if (logEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("typeId").toUuid() == mockIntStateTypeId) {
found = true;
QCOMPARE(logEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("source").toString(), QString("LoggingSourceStates"));
QCOMPARE(logEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("value").toInt(), 42);
break;
}
}
QVERIFY2(found, "LogEntryAdded notification not received");
if (clientSpy.count() == 0) clientSpy.wait();
notificationVariant = checkNotification(clientSpy, "Rules.RuleActiveChanged");
verifyRuleError(response);
@ -960,26 +928,10 @@ void TestJSONRPC::stateChangeEmitsNotifications()
// Devices.StateChanged
// Devices.EventTriggered
// Events.EventTriggered <-- deprecated
// Logging.LogEntryAdded
while (clientSpy.count() < 4) {
while (clientSpy.count() < 3) {
clientSpy.wait();
}
// Make sure the logg notification contains all the stuff we expect
QVariantList logEntryAddedVariants = checkNotifications(clientSpy, "Logging.LogEntryAdded");
QVERIFY2(!logEntryAddedVariants.isEmpty(), "Did not get Logging.LogEntryAdded notification.");
found = false;
foreach (const QVariant &loggEntryAddedVariant, logEntryAddedVariants) {
if (loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("typeId").toUuid() == stateTypeId) {
found = true;
QCOMPARE(loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("source").toString(), QString("LoggingSourceStates"));
QCOMPARE(loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap().value("value").toInt(), newVal);
break;
}
}
QVERIFY2(found, "Could not find the corresponding Logging.LogEntryAdded notification");
// Make sure the notification contains all the stuff we expect
QVariantList eventTriggeredVariants = checkNotifications(clientSpy, "Events.EventTriggered");
QVERIFY2(!eventTriggeredVariants.isEmpty(), "Did not get Events.EventTriggered notification.");

View File

@ -71,6 +71,7 @@ private slots:
void invalidFilter_data();
void invalidFilter();
void eventLogs_data();
void eventLogs();
void actionLog();
@ -198,6 +199,7 @@ void TestLogging::systemLogs()
qWarning() << "Restarting server";
restartServer();
qWarning() << "Restart done";
waitForDBSync();
// there should be 2 log entries, one for shutdown, one for startup (from server restart)
response = injectAndWait("Logging.GetLogEntries", params);
@ -261,24 +263,53 @@ void TestLogging::invalidFilter()
qDebug() << response.toMap().value("error").toString();
}
void TestLogging::eventLogs_data()
{
QTest::addColumn<StateTypeId>("stateTypeId");
QTest::addColumn<QVariant>("initValue");
QTest::addColumn<QVariant>("newValue");
QTest::addColumn<bool>("expectLogEntry");
QTest::newRow("logged event") << mockConnectedStateTypeId << QVariant(false) << QVariant(true) << true;
QTest::newRow("not logged event") << mockSignalStrengthStateTypeId << QVariant(10) << QVariant(20) << false;
}
void TestLogging::eventLogs()
{
QFETCH(StateTypeId, stateTypeId);
QFETCH(QVariant, initValue);
QFETCH(QVariant, newValue);
QFETCH(bool, expectLogEntry);
QList<Thing*> devices = NymeaCore::instance()->thingManager()->findConfiguredThings(mockThingClassId);
QVERIFY2(devices.count() > 0, "There needs to be at least one configured Mock Device for this test");
Thing *device = devices.first();
enableNotifications({"Events", "Logging"});
// Setup connection to mock client
QNetworkAccessManager nam;
int port = device->paramValue(mockThingHttpportParamTypeId).toInt();
// init state in mock device
QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(port).arg(stateTypeId.toString()).arg(initValue.toString())));
QNetworkReply *reply = nam.get(request);
{
QSignalSpy finishedSpy(reply, &QNetworkReply::finished);
finishedSpy.wait();
}
// Now snoop in for the events
clearLoggingDatabase();
enableNotifications({"Events", "Logging"});
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
// trigger event in mock device
int port = device->paramValue(mockThingHttpportParamTypeId).toInt();
QNetworkRequest request(QUrl(QString("http://localhost:%1/generateevent?eventtypeid=%2").arg(port).arg(mockEvent1EventTypeId.toString())));
QNetworkReply *reply = nam.get(request);
// trigger state change in mock device
request = QNetworkRequest(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(port).arg(stateTypeId.toString()).arg(newValue.toString())));
reply = nam.get(request);
{
QSignalSpy finishedSpy(reply, &QNetworkReply::finished);
finishedSpy.wait();
}
// Lets wait for the notification
QTest::qWait(200);
@ -286,41 +317,42 @@ void TestLogging::eventLogs()
reply->deleteLater();
// Make sure the logg notification contains all the stuff we expect
QVariantList loggEntryAddedVariants = checkNotifications(clientSpy, "Logging.LogEntryAdded");
QVERIFY2(!loggEntryAddedVariants.isEmpty(), "Did not get Logging.LogEntryAdded notification.");
qDebug() << "got" << loggEntryAddedVariants.count() << "Logging.LogEntryAdded notifications";
QVariantList logEntryAddedVariants = checkNotifications(clientSpy, "Logging.LogEntryAdded");
qDebug() << "got" << logEntryAddedVariants.count() << "Logging.LogEntryAdded notifications";
bool found = false;
qDebug() << "got" << loggEntryAddedVariants.count() << "Logging.LogEntryAdded";
foreach (const QVariant &loggEntryAddedVariant, loggEntryAddedVariants) {
QVariantMap logEntry = loggEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap();
qDebug() << "got" << logEntryAddedVariants.count() << "Logging.LogEntryAdded";
foreach (const QVariant &logEntryAddedVariant, logEntryAddedVariants) {
QVariantMap logEntry = logEntryAddedVariant.toMap().value("params").toMap().value("logEntry").toMap();
if (logEntry.value("thingId").toUuid() == device->id()) {
found = true;
// Make sure the notification contains all the stuff we expect
QCOMPARE(logEntry.value("typeId").toUuid().toString(), mockEvent1EventTypeId.toString());
QCOMPARE(logEntry.value("typeId").toUuid().toString(), stateTypeId.toString());
QCOMPARE(logEntry.value("eventType").toString(), enumValueName(Logging::LoggingEventTypeTrigger));
QCOMPARE(logEntry.value("source").toString(), enumValueName(Logging::LoggingSourceEvents));
QCOMPARE(logEntry.value("source").toString(), enumValueName(Logging::LoggingSourceStates));
QCOMPARE(logEntry.value("loggingLevel").toString(), enumValueName(Logging::LoggingLevelInfo));
break;
}
}
if (!found)
qDebug() << QJsonDocument::fromVariant(loggEntryAddedVariants).toJson();
QVERIFY2(found, "Could not find the corresponding Logging.LogEntryAdded notification");
QVERIFY2(found == expectLogEntry, "Could not find the corresponding Logging.LogEntryAdded notification");
// get this logentry with filter
QVariantMap params;
params.insert("thingIds", QVariantList() << device->id());
params.insert("loggingSources", QVariantList() << enumValueName(Logging::LoggingSourceEvents));
params.insert("eventTypes", QVariantList() << enumValueName(Logging::LoggingEventTypeTrigger));
params.insert("typeIds", QVariantList() << mockEvent1EventTypeId);
if (expectLogEntry) {
// get this logentry with filter
QVariantMap params;
params.insert("thingIds", QVariantList() << device->id());
params.insert("loggingSources", QVariantList() << enumValueName(Logging::LoggingSourceStates));
params.insert("eventTypes", QVariantList() << enumValueName(Logging::LoggingEventTypeTrigger));
params.insert("typeIds", QVariantList() << stateTypeId);
QVariant response = injectAndWait("Logging.GetLogEntries", params);
verifyLoggingError(response);
QVariant response = injectAndWait("Logging.GetLogEntries", params);
verifyLoggingError(response);
QVariantList logEntries = response.toMap().value("params").toMap().value("logEntries").toList();
qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(logEntries).toJson());
QCOMPARE(logEntries.count(), 1);
}
QVariantList logEntries = response.toMap().value("params").toMap().value("logEntries").toList();
QVERIFY(logEntries.count() == 1);
// disable notifications
QCOMPARE(disableNotifications(), true);
@ -629,7 +661,7 @@ void TestLogging::testHouseKeeping()
// Trigger something that creates a logging entry
QNetworkAccessManager nam;
QSignalSpy spy(&nam, SIGNAL(finished(QNetworkReply*)));
QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(6667).arg(mockIntStateTypeId.toString()).arg(4321)));
QNetworkRequest request(QUrl(QString("http://localhost:%1/setstate?%2=%3").arg(6667).arg(mockConnectedStateTypeId.toString()).arg(false)));
QNetworkReply *reply = nam.get(request);
connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater()));
spy.wait();