Merge PR #282: Add support for generic IO connections

This commit is contained in:
Jenkins nymea 2020-06-07 19:16:23 +02:00
commit 60eebfc289
45 changed files with 1787 additions and 107 deletions

4
debian/changelog vendored
View File

@ -1,3 +1,7 @@
nymea (0.21.0) UNRELEASED; urgency=medium
-- Michael Zanetti <michael.zanetti@guh.io> Thu, 09 Apr 2020 18:19:52 +0200
nymea (0.20.0) xenial; urgency=medium
[ Michael Zanetti ]

View File

@ -168,7 +168,7 @@ void CloudNotifications::startMonitoringAutoThings()
void CloudNotifications::executeAction(ThingActionInfo *info)
{
qCDebug(dcCloud()) << "executeAction" << info->thing() << info->action().id() << info->action().params();
qCDebug(dcCloud()) << "executeAction" << info->thing() << info->action().params();
QString userId = info->thing()->paramValue(cloudNotificationsThingClassUserParamId).toString();
QString endpointId = info->thing()->paramValue(cloudNotificationsThingClassEndpointParamId).toString();
int id = m_awsConnector->sendPushNotification(userId, endpointId, info->action().param(notifyActionParamTitleId).value().toString(), info->action().param(notifyActionParamBodyId).value().toString());

View File

@ -343,6 +343,35 @@ HttpReply *DebugServerHandler::processDebugRequest(const QString &requestPath, c
return reply;
}
if (requestPath.startsWith("/debug/settings/ioconnections")) {
QString settingsFileName = NymeaSettings(NymeaSettings::SettingsRoleIOConnections).fileName();
qCDebug(dcDebugServer()) << "Loading" << settingsFileName;
QFile settingsFile(settingsFileName);
if (!settingsFile.exists()) {
qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName << "file does not exist.";
HttpReply *reply = HttpReply::createErrorReply(HttpReply::NotFound);
reply->setHeader(HttpReply::ContentTypeHeader, "text/html");
reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not find file \"%1\".").arg(settingsFileName)));
return reply;
}
if (!settingsFile.open(QFile::ReadOnly)) {
qCWarning(dcDebugServer()) << "Could not read file for debug download" << settingsFileName;
HttpReply *reply = HttpReply::createErrorReply(HttpReply::Forbidden);
reply->setHeader(HttpReply::ContentTypeHeader, "text/html");
reply->setPayload(createErrorXmlDocument(HttpReply::NotFound, tr("Could not open file \"%1\".").arg(settingsFileName)));
return reply;
}
QByteArray settingsFileData = settingsFile.readAll();
settingsFile.close();
HttpReply *reply = HttpReply::createSuccessReply();
reply->setHeader(HttpReply::ContentTypeHeader, "text/plain");
reply->setPayload(settingsFileData);
return reply;
}
if (requestPath.startsWith("/debug/ping")) {
// Only one ping process should run
if (m_pingProcess || m_pingReply)
@ -1608,6 +1637,56 @@ QByteArray DebugServerHandler::createDebugXmlDocument()
writer.writeEndElement(); // div download-row
// Download row IO connections
writer.writeStartElement("div");
writer.writeAttribute("class", "download-row");
writer.writeStartElement("div");
writer.writeAttribute("class", "download-name-column");
//: The MQTT policies download description of the debug interface
writer.writeTextElement("p", tr("IO Connections"));
writer.writeEndElement(); // div download-name-column
writer.writeStartElement("div");
writer.writeAttribute("class", "download-path-column");
writer.writeTextElement("p", NymeaSettings(NymeaSettings::SettingsRoleIOConnections).fileName());
writer.writeEndElement(); // div download-path-column
writer.writeStartElement("div");
writer.writeAttribute("class", "download-button-column");
writer.writeStartElement("form");
writer.writeAttribute("class", "download-button");
writer.writeStartElement("button");
writer.writeAttribute("class", "button");
writer.writeAttribute("type", "button");
if (!QFile::exists(NymeaSettings(NymeaSettings::SettingsRoleMqttPolicies).fileName())) {
writer.writeAttribute("disabled", "true");
}
writer.writeAttribute("onClick", "downloadFile('/debug/settings/ioconnections', 'ioconnections.conf')");
writer.writeCharacters(tr("Download"));
writer.writeEndElement(); // button
writer.writeEndElement(); // form
writer.writeEndElement(); // div download-button-column
writer.writeStartElement("div");
writer.writeAttribute("class", "show-button-column");
writer.writeStartElement("form");
writer.writeAttribute("class", "show-button");
writer.writeStartElement("button");
writer.writeAttribute("class", "button");
writer.writeAttribute("type", "button");
if (!QFile::exists(NymeaSettings(NymeaSettings::SettingsRoleMqttPolicies).fileName())) {
writer.writeAttribute("disabled", "true");
}
writer.writeAttribute("onClick", "showFile('/debug/settings/ioconnections')");
writer.writeCharacters(tr("Show"));
writer.writeEndElement(); // button
writer.writeEndElement(); // form
writer.writeEndElement(); // div show-button-column
writer.writeEndElement(); // div download-row
writer.writeEndElement(); // downloads-section

View File

@ -760,6 +760,13 @@ Thing::ThingError ThingManagerImplementation::removeConfiguredThing(const ThingI
NymeaSettings stateCache(NymeaSettings::SettingsRoleThingStates);
stateCache.remove(thingId.toString());
foreach (const IOConnectionId &ioConnectionId, m_ioConnections.keys()) {
IOConnection ioConnection = m_ioConnections.value(ioConnectionId);
if (ioConnection.inputThingId() == thing->id() || ioConnection.outputThingId() == thing->id()) {
disconnectIO(ioConnectionId);
}
}
emit thingRemoved(thingId);
return Thing::ThingErrorNoError;
@ -907,6 +914,126 @@ BrowserItemActionInfo* ThingManagerImplementation::executeBrowserItemAction(cons
return info;
}
IOConnections ThingManagerImplementation::ioConnections(const ThingId &thingId) const
{
if (thingId.isNull()) {
return m_ioConnections.values();
}
IOConnections ioConnections;
foreach (const IOConnection &ioConnection, m_ioConnections) {
if (ioConnection.inputThingId() == thingId || ioConnection.outputThingId() == thingId) {
ioConnections.append(ioConnection);
}
}
return ioConnections;
}
IOConnectionResult ThingManagerImplementation::connectIO(const IOConnection &connection)
{
IOConnectionResult result;
// Do some sanity checks
Thing *inputThing = m_configuredThings.value(connection.inputThingId());
if (!inputThing) {
qCWarning(dcThingManager()) << "Could not find inputThing" << connection.inputThingId() << "in configured things. Not adding IO connection.";
result.error = Thing::ThingErrorThingNotFound;
return result;
}
if (!inputThing->thingClass().stateTypes().contains(connection.inputStateTypeId())) {
qCWarning(dcThingManager()) << "Input thing" << inputThing->name() << "does not have a state with id" << connection.inputStateTypeId();
result.error = Thing::ThingErrorStateTypeNotFound;
return result;
}
StateType inputStateType = inputThing->thingClass().stateTypes().findById(connection.inputStateTypeId());
// Check if this is actually an input
if (inputStateType.ioType() != Types::IOTypeDigitalInput && inputStateType.ioType() != Types::IOTypeAnalogInput) {
qCWarning(dcThingManager()) << "The given input state is neither a digital nor an analog input.";
result.error = Thing::ThingErrorInvalidParameter;
return result;
}
Thing *outputThing = m_configuredThings.value(connection.outputThingId());
if (!outputThing) {
qCWarning(dcThingManager()) << "Could not find outputThing" << connection.outputThingId() << "in configured things. Not adding IO connection.";
result.error = Thing::ThingErrorThingNotFound;
return result;
}
if (!outputThing->thingClass().stateTypes().contains(connection.outputStateTypeId())) {
qCWarning(dcThingManager()) << "Output thing" << outputThing->name() << "does not have a state with id" << connection.outputStateTypeId();
result.error = Thing::ThingErrorStateTypeNotFound;
return result;
}
StateType outputStateType = outputThing->thingClass().stateTypes().findById(connection.outputStateTypeId());
// Check if this is actually an output
if (outputStateType.ioType() != Types::IOTypeDigitalOutput && outputStateType.ioType() != Types::IOTypeAnalogOutput) {
qCWarning(dcThingManager()) << "The given output state is neither a digital nor an analog output.";
result.error = Thing::ThingErrorInvalidParameter;
return result;
}
// Check if io types are compatible
if (inputStateType.ioType() == Types::IOTypeDigitalInput && outputStateType.ioType() != Types::IOTypeDigitalOutput) {
qCWarning(dcThingManager()) << "Cannot connect IOs of different type:" << inputStateType.ioType() << "is not compatible with" << outputStateType.ioType();
result.error = Thing::ThingErrorInvalidParameter;
return result;
}
if (inputStateType.ioType() == Types::IOTypeAnalogInput && outputStateType.ioType() != Types::IOTypeAnalogOutput) {
qCWarning(dcThingManager()) << "Cannot connect IOs of different type:" << inputStateType.ioType() << "is not compatible with" << outputStateType.ioType();
result.error = Thing::ThingErrorInvalidParameter;
return result;
}
// Check if either input or output is already connected
foreach (const IOConnectionId &id, m_ioConnections.keys()) {
if (m_ioConnections.value(id).inputThingId() == connection.inputThingId() && m_ioConnections.value(id).inputStateTypeId() == connection.inputStateTypeId()) {
qCDebug(dcThingManager()).nospace() << "Thing " << inputThing->name() << " already has an IO connection on " << inputStateType.displayName() << ". Replacing old connection.";
disconnectIO(id);
continue;
}
if (m_ioConnections.value(id).outputThingId() == connection.outputThingId() && m_ioConnections.value(id).outputStateTypeId() == connection.outputStateTypeId()) {
qCDebug(dcThingManager()).nospace() << "Thing " << inputThing->name() << " already has an IO connection on " << inputStateType.displayName() << ". Replacing old connection.";
disconnectIO(id);
}
}
// Finally add the connection
m_ioConnections.insert(connection.id(), connection);
storeIOConnections();
emit ioConnectionAdded(connection);
qCDebug(dcThingManager()) << "IO connected added:" << inputThing << "->" << outputThing;
// Sync initial state
syncIOConnection(inputThing, connection.inputStateTypeId());
result.error = Thing::ThingErrorNoError;
result.ioConnectionId = connection.id();
return result;
}
Thing::ThingError ThingManagerImplementation::disconnectIO(const IOConnectionId &ioConnectionId)
{
if (!m_ioConnections.contains(ioConnectionId)) {
qCWarning(dcThingManager()) << "IO connection" << ioConnectionId << "not found. Cannot disconnect.";
return Thing::ThingErrorItemNotFound;
}
m_ioConnections.remove(ioConnectionId);
NymeaSettings settings(NymeaSettings::SettingsRoleIOConnections);
settings.beginGroup("IOConnections");
settings.remove(ioConnectionId.toString());
settings.endGroup();
qCDebug(dcThingManager()) << "IO connection disconnected:" << ioConnectionId;
emit ioConnectionRemoved(ioConnectionId);
return Thing::ThingErrorNoError;
}
QString ThingManagerImplementation::translate(const PluginId &pluginId, const QString &string, const QLocale &locale)
{
return m_translator->translate(pluginId, string, locale);
@ -1428,6 +1555,8 @@ void ThingManagerImplementation::loadConfiguredThings()
postSetupThing(info->thing());
});
}
loadIOConnections();
}
void ThingManagerImplementation::storeConfiguredThings()
@ -1616,6 +1745,127 @@ 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);
syncIOConnection(thing, stateTypeId);
}
void ThingManagerImplementation::syncIOConnection(Thing *thing, const StateTypeId &stateTypeId)
{
foreach (const IOConnection &ioConnection, m_ioConnections) {
// Check if this state is an input to an IO connection.
if (ioConnection.inputThingId() == thing->id() && ioConnection.inputStateTypeId() == stateTypeId) {
Thing *inputThing = thing;
QVariant inputValue = inputThing->stateValue(stateTypeId);
Thing *outputThing = m_configuredThings.value(ioConnection.outputThingId());
if (!outputThing) {
qCWarning(dcThingManager()) << "IO connection contains invalid output thing!";
continue;
}
IntegrationPlugin *plugin = m_integrationPlugins.value(outputThing->pluginId());
if (!plugin) {
qCWarning(dcThingManager()) << "Plugin not found for IO connection's output action.";
continue;
}
StateType inputStateType = inputThing->thingClass().getStateType(stateTypeId);
StateType outputStateType = outputThing->thingClass().getStateType(ioConnection.outputStateTypeId());
if (outputStateType.id().isNull()) {
qCWarning(dcThingManager()) << "Could not find output state type for IO connection.";
continue;
}
QVariant outputValue;
if (outputStateType.ioType() == Types::IOTypeDigitalOutput) {
// Digital IOs are mapped as-is
outputValue = ioConnection.inverted() xor inputValue.toBool();
// We're already in sync! Skipping action.
if (outputThing->stateValue(outputStateType.id()) == outputValue) {
continue;
}
} else {
// Analog IOs are mapped within the according min/max ranges
outputValue = mapValue(inputValue, inputStateType, outputStateType, ioConnection.inverted());
// We're already in sync (fuzzy, good enough)! Skipping action.
if (qFuzzyCompare(1.0 + outputThing->stateValue(outputStateType.id()).toDouble(), 1.0 + outputValue.toDouble())) {
continue;
}
}
Action outputAction(ActionTypeId(ioConnection.outputStateTypeId()), ioConnection.outputThingId());
Param outputParam(ioConnection.outputStateTypeId(), outputValue);
outputAction.setParams(ParamList() << outputParam);
qCDebug(dcThingManager()) << "Executing IO connection action on" << outputThing->name() << outputParam;
ThingActionInfo* info = executeAction(outputAction);
connect(info, &ThingActionInfo::finished, this, [=](){
if (info->status() != Thing::ThingErrorNoError) {
// An error happened... let's switch the input back to be in sync with the output
qCWarning(dcThingManager()) << "Error syncing IO connection state. Reverting input back to old value.";
if (inputStateType.ioType() == Types::IOTypeDigitalInput) {
inputThing->setStateValue(inputStateType.id(), outputThing->stateValue(outputStateType.id()));
} else {
inputThing->setStateValue(inputStateType.id(), mapValue(outputThing->stateValue(outputStateType.id()), outputStateType, inputStateType, ioConnection.inverted()));
}
}
});
}
// Now check if this is an output state type and - if possible - update the inputs for bidirectional connections
if (ioConnection.outputThingId() == thing->id() && ioConnection.outputStateTypeId() == stateTypeId) {
Thing *outputThing = thing;
QVariant outputValue = outputThing->stateValue(stateTypeId);
Thing *inputThing = m_configuredThings.value(ioConnection.inputThingId());
if (!inputThing) {
qCWarning(dcThingManager()) << "IO connection contains invalid input thing!";
continue;
}
IntegrationPlugin *plugin = m_integrationPlugins.value(inputThing->pluginId());
if (!plugin) {
qCWarning(dcThingManager()) << "Plugin not found for IO connection's input action.";
continue;
}
StateType outputStateType = outputThing->thingClass().getStateType(stateTypeId);
StateType inputStateType = inputThing->thingClass().getStateType(ioConnection.inputStateTypeId());
if (inputStateType.id().isNull()) {
qCWarning(dcThingManager()) << "Could not find input state type for IO connection.";
continue;
}
if (!inputStateType.writable()) {
qCDebug(dcThingManager()) << "Input state is not writable. This connection is unidirectional.";
continue;
}
QVariant inputValue;
if (inputStateType.ioType() == Types::IOTypeDigitalInput) {
// Digital IOs are mapped as-is
inputValue = ioConnection.inverted() xor outputValue.toBool();
// Prevent looping
if (inputThing->stateValue(inputStateType.id()) == inputValue) {
continue;
}
} else {
// Analog IOs are mapped within the according min/max ranges
inputValue = mapValue(outputValue, outputStateType, inputStateType, ioConnection.inverted());
// Prevent looping even if the above calculation has rounding errors... Just skip this action if we're close enough already
if (qFuzzyCompare(1.0 + inputThing->stateValue(inputStateType.id()).toDouble(), 1.0 + inputValue.toDouble())) {
continue;
}
}
Action inputAction(ActionTypeId(ioConnection.inputStateTypeId()), ioConnection.inputThingId());
Param inputParam(ioConnection.inputStateTypeId(), inputValue);
inputAction.setParams(ParamList() << inputParam);
qCDebug(dcThingManager()) << "Executing reverse IO connection action on" << inputThing->name() << inputParam;
executeAction(inputAction);
}
}
}
void ThingManagerImplementation::slotThingSettingChanged(const ParamTypeId &paramTypeId, const QVariant &value)
@ -1761,6 +2011,62 @@ void ThingManagerImplementation::loadThingStates(Thing *thing)
settings.endGroup();
}
void ThingManagerImplementation::storeIOConnections()
{
NymeaSettings connectionSettings(NymeaSettings::SettingsRoleIOConnections);
connectionSettings.beginGroup("IOConnections");
foreach (const IOConnection &ioConnection, m_ioConnections) {
connectionSettings.beginGroup(ioConnection.id().toString());
connectionSettings.setValue("inputThingId", ioConnection.inputThingId().toString());
connectionSettings.setValue("inputStateTypeId", ioConnection.inputStateTypeId().toString());
connectionSettings.setValue("outputThingId", ioConnection.outputThingId().toString());
connectionSettings.setValue("outputStateTypeId", ioConnection.outputStateTypeId().toString());
connectionSettings.setValue("inverted", ioConnection.inverted());
connectionSettings.endGroup();
}
connectionSettings.endGroup();
}
void ThingManagerImplementation::loadIOConnections()
{
NymeaSettings connectionSettings(NymeaSettings::SettingsRoleIOConnections);
connectionSettings.beginGroup("IOConnections");
foreach (const QString &idString, connectionSettings.childGroups()) {
connectionSettings.beginGroup(idString);
IOConnectionId id(idString);
ThingId inputThingId = connectionSettings.value("inputThingId").toUuid();
StateTypeId inputStateTypeId = connectionSettings.value("inputStateTypeId").toUuid();
ThingId outputThingId = connectionSettings.value("outputThingId").toUuid();
StateTypeId outputStateTypeId = connectionSettings.value("outputStateTypeId").toUuid();
bool inverted = connectionSettings.value("inverted").toBool();
IOConnection ioConnection(id, inputThingId, inputStateTypeId, outputThingId, outputStateTypeId, inverted);
m_ioConnections.insert(id, ioConnection);
connectionSettings.endGroup();
Thing *inputThing = m_configuredThings.value(inputThingId);
if (!inputThing) {
continue;
}
syncIOConnection(inputThing, inputStateTypeId);
}
connectionSettings.endGroup();
}
QVariant ThingManagerImplementation::mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType, bool inverted) const
{
double fromMin = fromStateType.minValue().toDouble();
double fromMax = fromStateType.maxValue().toDouble();
double toMin = toStateType.minValue().toDouble();
double toMax = toStateType.maxValue().toDouble();
double fromValue = value.toDouble();
double fromPercent = (fromValue - fromMin) / (fromMax - fromMin);
fromPercent = inverted ? 1 - fromPercent : fromPercent;
double toValue = toMin + (toMax - toMin) * fromPercent;
return toValue;
}
void ThingManagerImplementation::storeThingStates(Thing *thing)
{
NymeaSettings settings(NymeaSettings::SettingsRoleThingStates);

View File

@ -36,6 +36,7 @@
#include "integrations/thing.h"
#include "integrations/thingdescriptor.h"
#include "integrations/pluginmetadata.h"
#include "integrations/ioconnection.h"
#include "types/thingclass.h"
#include "types/interface.h"
@ -113,6 +114,10 @@ public:
BrowserActionInfo *executeBrowserItem(const BrowserAction &browserAction) override;
BrowserItemActionInfo *executeBrowserItemAction(const BrowserItemAction &browserItemAction) override;
IOConnections ioConnections(const ThingId &thingId = ThingId()) const override;
IOConnectionResult connectIO(const IOConnection &connection) override;
Thing::ThingError disconnectIO(const IOConnectionId &ioConnectionId) override;
QString translate(const PluginId &pluginId, const QString &string, const QLocale &locale) override;
ParamType translateParamType(const PluginId &pluginId, const ParamType &paramType, const QLocale &locale) override;
ThingClass translateThingClass(const ThingClass &thingClass, const QLocale &locale) override;
@ -149,6 +154,11 @@ private:
void postSetupThing(Thing *thing);
void storeThingStates(Thing *thing);
void loadThingStates(Thing *thing);
void storeIOConnections();
void loadIOConnections();
void syncIOConnection(Thing *inputThing, const StateTypeId &stateTypeId);
QVariant mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType, bool inverted) const;
private:
HardwareManager *m_hardwareManager;
@ -173,6 +183,8 @@ private:
QString thingName;
};
QHash<PairingTransactionId, PairingContext> m_pendingPairings;
QHash<IOConnectionId, IOConnection> m_ioConnections;
};
#endif // THINGMANAGERIMPLEMENTATION_H

View File

@ -176,7 +176,7 @@ JsonReply *ActionHandler::ExecuteBrowserItemAction(const QVariantMap &params)
BrowserItemActionInfo *info = NymeaCore::instance()->executeBrowserItemAction(browserItemAction);
connect(info, &BrowserItemActionInfo::finished, jsonReply, [info, jsonReply](){
QVariantMap data;
data.insert("deviceError", enumValueName<Thing::ThingError>(info->status()).replace("ThingError", "DeviceError"));
data.insert("deviceError", enumValueName<Thing::ThingError>(info->status()).replace("Thing", "Device"));
jsonReply->setData(data);
jsonReply->finished();
});

View File

@ -60,6 +60,7 @@ DeviceHandler::DeviceHandler(QObject *parent) :
registerEnum<DeviceClass::CreateMethod, DeviceClass::CreateMethods>();
registerEnum<Types::Unit>();
registerEnum<Types::InputType>();
registerEnum<Types::IOType>();
registerEnum<RuleEngine::RemovePolicy>();
registerEnum<BrowserItem::BrowserIcon>();
registerEnum<MediaBrowserItem::MediaBrowserIcon>();

View File

@ -59,6 +59,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
registerEnum<ThingClass::CreateMethod, ThingClass::CreateMethods>();
registerEnum<Types::Unit>();
registerEnum<Types::InputType>();
registerEnum<Types::IOType>();
registerEnum<RuleEngine::RemovePolicy>();
registerEnum<BrowserItem::BrowserIcon>();
registerEnum<MediaBrowserItem::MediaBrowserIcon>();
@ -77,6 +78,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
registerObject<Action>();
registerObject<State, States>();
registerUncreatableObject<Thing, Things>();
registerObject<IOConnection, IOConnections>();
// Registering browseritem manually for now. Not sure how to deal with the
// polymorphism in it (e.g MediaBrowserItem)
@ -346,6 +348,31 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
returns.insert("o:displayMessage", enumValueName(String));
registerMethod("ExecuteBrowserItemAction", description, params, returns);
params.clear(); returns.clear();
description = "Fetch IO connections. Optionally filtered by thingId and stateTypeId.";
params.insert("o:thingId", enumValueName(Uuid));
returns.insert("ioConnections", objectRef<IOConnections>());
registerMethod("GetIOConnections", description, params, returns);
params.clear(); returns.clear();
description = "Connect two generic IO states. Input and output need to be compatible, that is, either a digital input "
"and a digital output, or an analog input and an analog output. If successful, the connectionId will be returned.";
params.insert("inputThingId", enumValueName(Uuid));
params.insert("inputStateTypeId", enumValueName(Uuid));
params.insert("outputThingId", enumValueName(Uuid));
params.insert("outputStateTypeId", enumValueName(Uuid));
params.insert("o:inverted", enumValueName(Bool));
returns.insert("thingError", enumRef<Thing::ThingError>());
returns.insert("o:ioConnectionId", enumValueName(Uuid));
registerMethod("ConnectIO", description, params, returns);
params.clear(); returns.clear();
description = "Disconnect an existing IO connection.";
params.insert("ioConnectionId", enumValueName(Uuid));
returns.insert("thingError", enumRef<Thing::ThingError>());
registerMethod("DisconnectIO", description, params, returns);
// Notifications
params.clear(); returns.clear();
description = "Emitted whenever a state of a thing changes.";
@ -392,6 +419,26 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
emit EventTriggered(params);
});
params.clear(); returns.clear();
description = "Emitted whenever an IO connection has been added.";
params.insert("ioConnection", objectRef<IOConnection>());
registerNotification("IOConnectionAdded", description, params);
connect(m_thingManager, &ThingManager::ioConnectionAdded, this, [this](const IOConnection &connection) {
QVariantMap params;
params.insert("ioConnection", pack(connection));
emit IOConnectionAdded(params);
});
params.clear(); returns.clear();
description = "Emitted whenever an IO connection has been removed.";
params.insert("ioConnectionId", enumValueName(Uuid));
registerNotification("IOConnectionRemoved", description, params);
connect(m_thingManager, &ThingManager::ioConnectionRemoved, this, [this](const IOConnectionId &ioConnectionId) {
QVariantMap params;
params.insert("ioConnectionId", ioConnectionId);
emit IOConnectionRemoved(params);
});
connect(NymeaCore::instance(), &NymeaCore::pluginConfigChanged, this, &IntegrationsHandler::pluginConfigChanged);
connect(NymeaCore::instance(), &NymeaCore::thingStateChanged, this, &IntegrationsHandler::thingStateChanged);
connect(NymeaCore::instance(), &NymeaCore::thingRemoved, this, &IntegrationsHandler::thingRemovedNotification);
@ -930,6 +977,38 @@ JsonReply *IntegrationsHandler::ExecuteBrowserItemAction(const QVariantMap &para
return jsonReply;
}
JsonReply *IntegrationsHandler::GetIOConnections(const QVariantMap &params)
{
ThingId thingId = params.value("thingId").toUuid();
IOConnections ioConnections = m_thingManager->ioConnections(thingId);
QVariantMap returns;
QVariant bla = pack(ioConnections);
returns.insert("ioConnections", pack(ioConnections));
return createReply(returns);
}
JsonReply *IntegrationsHandler::ConnectIO(const QVariantMap &params)
{
ThingId inputThingId = params.value("inputThingId").toUuid();
StateTypeId inputStateTypeId = params.value("inputStateTypeId").toUuid();
ThingId outputThingId = params.value("outputThingId").toUuid();
StateTypeId outputStateTypeId = params.value("outputStateTypeId").toUuid();
bool inverted = params.value("inverted", false).toBool();
IOConnectionResult result = m_thingManager->connectIO(inputThingId, inputStateTypeId, outputThingId, outputStateTypeId, inverted);
QVariantMap reply = statusToReply(result.error);
if (result.error == Thing::ThingErrorNoError) {
reply.insert("ioConnectionId", result.ioConnectionId);
}
return createReply(reply);
}
JsonReply *IntegrationsHandler::DisconnectIO(const QVariantMap &params)
{
IOConnectionId ioConnectionId = params.value("ioConnectionId").toUuid();
Thing::ThingError error = m_thingManager->disconnectIO(ioConnectionId);
return createReply(statusToReply(error));
}
QVariantMap IntegrationsHandler::packBrowserItem(const BrowserItem &item)
{
QVariantMap ret;

View File

@ -72,6 +72,10 @@ public:
Q_INVOKABLE JsonReply *ExecuteBrowserItem(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *ExecuteBrowserItemAction(const QVariantMap &params, const JsonContext &context);
Q_INVOKABLE JsonReply *GetIOConnections(const QVariantMap &params);
Q_INVOKABLE JsonReply *ConnectIO(const QVariantMap &params);
Q_INVOKABLE JsonReply *DisconnectIO(const QVariantMap &params);
static QVariantMap packBrowserItem(const BrowserItem &item);
signals:
@ -82,6 +86,8 @@ signals:
void ThingChanged(const QVariantMap &params);
void ThingSettingChanged(const QVariantMap &params);
void EventTriggered(const QVariantMap &params);
void IOConnectionAdded(const QVariantMap &params);
void IOConnectionRemoved(const QVariantMap &params);
private slots:
void pluginConfigChanged(const PluginId &id, const ParamList &config);

View File

@ -424,7 +424,7 @@ void NymeaCore::executeRuleActions(const QList<RuleAction> ruleActions)
} else if (ruleActionParam.isStateBased()) {
Thing *stateThing = m_thingManager->findConfiguredThing(ruleActionParam.stateThingId());
if (!stateThing) {
qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action" << ruleAction.id();
qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action";
ok = false;
break;
}
@ -438,7 +438,7 @@ void NymeaCore::executeRuleActions(const QList<RuleAction> ruleActions)
}
}
if (!ok) {
qCWarning(dcRuleEngine()) << "Not executing rule action" << ruleAction.id();
qCWarning(dcRuleEngine()) << "Not executing rule action";
continue;
}
Action action(actionTypeId, thing->id());
@ -476,7 +476,7 @@ void NymeaCore::executeRuleActions(const QList<RuleAction> ruleActions)
} else if (ruleActionParam.isStateBased()) {
Thing *stateThing = m_thingManager->findConfiguredThing(ruleActionParam.stateThingId());
if (!stateThing) {
qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action" << ruleAction.id();
qCWarning(dcRuleEngine()) << "Cannot find thing" << ruleActionParam.stateThingId() << "required by rule action";
ok = false;
break;
}
@ -490,7 +490,7 @@ void NymeaCore::executeRuleActions(const QList<RuleAction> ruleActions)
}
}
if (!ok) {
qCWarning(dcRuleEngine()) << "Not executing rule action" << ruleAction.id();
qCWarning(dcRuleEngine()) << "Not executing rule action";
continue;
}

View File

@ -31,7 +31,6 @@
#include "ruleaction.h"
RuleAction::RuleAction(const ActionTypeId &actionTypeId, const ThingId &thingId, const RuleActionParams &params):
m_id(ActionId::createActionId()),
m_thingId(thingId),
m_actionTypeId(actionTypeId),
m_ruleActionParams(params)
@ -55,7 +54,6 @@ RuleAction::RuleAction(const ThingId &thingId, const QString &browserItemId):
}
RuleAction::RuleAction(const RuleAction &other) :
m_id(other.id()),
m_thingId(other.thingId()),
m_actionTypeId(other.actionTypeId()),
m_browserItemId(other.browserItemId()),
@ -66,11 +64,6 @@ RuleAction::RuleAction(const RuleAction &other) :
}
ActionId RuleAction::id() const
{
return m_id;
}
bool RuleAction::isValid() const
{
return (!m_actionTypeId.isNull() && !m_thingId.isNull())
@ -211,7 +204,6 @@ RuleActionParam RuleAction::ruleActionParam(const QString &ruleActionParamName)
void RuleAction::operator=(const RuleAction &other)
{
m_id = other.id();
m_actionTypeId = other.actionTypeId();
m_ruleActionParams = other.ruleActionParams();
}

View File

@ -58,7 +58,6 @@ public:
explicit RuleAction(const ThingId &thingId, const QString &browserItemId);
RuleAction(const RuleAction &other);
ActionId id() const;
bool isValid() const;
Type type() const;
@ -92,7 +91,6 @@ public:
void operator=(const RuleAction &other);
private:
ActionId m_id;
ThingId m_thingId;
ActionTypeId m_actionTypeId;
QString m_browserItemId;

View File

@ -201,12 +201,12 @@ QList<Rule> RuleEngine::evaluateEvent(const Event &event)
} else {
// Event based rule
if (containsEvent(rule, event, thing->thingClassId())) {
qCDebug(dcRuleEngineDebug()).nospace().noquote() << "Rule " << rule.name() << " (" << rule.id().toString() << ") contains event " << event.eventId();
qCDebug(dcRuleEngineDebug()).nospace().noquote() << "Rule " << rule.name() << " (" << rule.id().toString() << ") contains event";
if (rule.statesActive() && rule.timeActive()) {
qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" + rule.id().toString() << ") contains event" << event.eventId() << "and all states match.";
qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" + rule.id().toString() << ") contains event and all states match.";
rules.append(rule);
} else {
qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" + rule.id().toString() << ") contains event" << event.eventId() << "but state are not matching.";
qCDebug(dcRuleEngine).nospace().noquote() << "Rule " << rule.name() << " (" + rule.id().toString() << ") contains event but state are not matching.";
rules.append(rule);
}
}

View File

@ -0,0 +1,108 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU 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
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*!
\class IOConnection
\brief The IOConnection class stores information about generic IO connections.
\ingroup things
\inmodule libnymea
Generic IO connections allow a user to connect states of two devices so they'll be synced whenever
those states change.
\sa ThingManager::connectIO
*/
#include "ioconnection.h"
IOConnection::IOConnection()
{
}
/*! Constructs a new IOConnection object. */
IOConnection::IOConnection(const IOConnectionId &id, const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState, bool inverted):
m_id(id),
m_inputThingId(inputThing),
m_inputStateTypeId(inputState),
m_outputThingId(outputThing),
m_outputStateTypeId(outputState),
m_inverted(inverted)
{
}
/*! Returns the ID of this connection object. */
IOConnectionId IOConnection::id() const
{
return m_id;
}
/*! Returns the ID of the input thing for this connection. */
ThingId IOConnection::inputThingId() const
{
return m_inputThingId;
}
/*! Returns the input state type ID for this connection. */
StateTypeId IOConnection::inputStateTypeId() const
{
return m_inputStateTypeId;
}
/*! Returns the ID of the output thing for this connection. */
ThingId IOConnection::outputThingId() const
{
return m_outputThingId;
}
/*! Returns the output state type ID for this connection. */
StateTypeId IOConnection::outputStateTypeId() const
{
return m_outputStateTypeId;
}
/*! Returns whether the connection is inverted or not. */
bool IOConnection::inverted() const
{
return m_inverted;
}
QVariant IOConnections::get(int index) const
{
return QVariant::fromValue(at(index));
}
void IOConnections::put(const QVariant &variant)
{
append(variant.value<IOConnection>());
}

View File

@ -0,0 +1,95 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU 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 IOCONNECTION_H
#define IOCONNECTION_H
#include <QObject>
#include <QList>
#include <QVariant>
#include "typeutils.h"
#include "thing.h"
struct IOConnectionResult {
Thing::ThingError error = Thing::ThingErrorNoError;
IOConnectionId ioConnectionId;
};
class IOConnection
{
Q_GADGET
Q_PROPERTY(QUuid id READ id)
Q_PROPERTY(QUuid inputThingId READ inputThingId)
Q_PROPERTY(QUuid inputStateTypeId READ inputStateTypeId)
Q_PROPERTY(QUuid outputThingId READ outputThingId)
Q_PROPERTY(QUuid outputStateTypeId READ outputStateTypeId)
Q_PROPERTY(bool inverted READ inverted)
public:
IOConnection();
IOConnection(const IOConnectionId &id, const ThingId &inputThingId, const StateTypeId &inputStateTypeId, const ThingId &outputThingId, const StateTypeId &outputStateTypeId, bool inverted = false);
IOConnectionId id() const;
ThingId inputThingId() const;
StateTypeId inputStateTypeId() const;
ThingId outputThingId() const;
StateTypeId outputStateTypeId() const;
bool inverted() const;
private:
IOConnectionId m_id;
ThingId m_inputThingId;
StateTypeId m_inputStateTypeId;
ThingId m_outputThingId;
StateTypeId m_outputStateTypeId;
bool m_inverted = false;
};
class IOConnections: public QList<IOConnection>
{
Q_GADGET
Q_PROPERTY(int count READ count)
public:
IOConnections() {}
inline IOConnections(std::initializer_list<IOConnection> args): QList(args) {}
IOConnections(const QList<IOConnection> &other): QList<IOConnection>(other) {}
Q_INVOKABLE QVariant get(int index) const;
Q_INVOKABLE void put(const QVariant &variant);
};
Q_DECLARE_METATYPE(IOConnection)
Q_DECLARE_METATYPE(IOConnections)
#endif // IOCONNECTION_H

View File

@ -381,6 +381,68 @@ void PluginMetadata::parse(const QJsonObject &jsonObject)
if (st.contains("cached")) {
stateType.setCached(st.value("cached").toBool());
}
if (st.contains("writable")) {
stateType.setWritable(st.value("writable").toBool());
}
if (st.contains("ioType")) {
QString ioTypeString = st.value("ioType").toString();
Types::IOType ioType = Types::IOTypeNone;
if (ioTypeString == "digitalInput") {
if (stateType.type() != QVariant::Bool) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as digital input but type is not \"bool\"");
hasError = true;
break;
}
ioType = Types::IOTypeDigitalInput;
} else if (ioTypeString == "digitalOutput") {
if (stateType.type() != QVariant::Bool) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as digital output but type is not \"bool\"");
hasError = true;
break;
}
if (!stateType.writable()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as digital output but is not writable");
hasError = true;
break;
}
ioType = Types::IOTypeDigitalOutput;
} else if (ioTypeString == "analogInput") {
if (stateType.type() != QVariant::Double) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog input but type is not \"double\"");
hasError = true;
break;
}
if (stateType.minValue().isNull() || stateType.maxValue().isNull()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog input but it does not define \"minValue\" and \"maxValue\"");
hasError = true;
break;
}
ioType = Types::IOTypeAnalogInput;
} else if (ioTypeString == "analogOutput") {
if (stateType.type() != QVariant::Double) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog output but type is not \"double\"");
hasError = true;
break;
}
if (!stateType.writable()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog output but is not writable");
hasError = true;
break;
}
if (stateType.minValue().isNull() || stateType.maxValue().isNull()) {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" is marked as analog output but it does not define \"minValue\" and \"maxValue\"");
hasError = true;
break;
}
ioType = Types::IOTypeAnalogOutput;
} else {
m_validationErrors.append("Thing class \"" + thingClass.name() + "\" state type \"" + stateTypeName + "\" has invalid ioType value \"" + ioType + "\" which is not any of \"digitalInput\", \"digitalOutput\", \"analogInput\" or \"analogOutput\"");
hasError = true;
break;
}
stateType.setIOType(ioType);
}
stateTypes.append(stateType);
// Events for state changed (Not checking for duplicate UUID, this is expected to be the same as the state!)

View File

@ -50,3 +50,21 @@ ThingManager::ThingManager(QObject *parent) : QObject(parent)
qRegisterMetaType<ParamType>();
qRegisterMetaType<ParamTypes>();
}
/*! Connect two states.
When two states are connected, any state changes will be synced between those. A connection
is made from an \a inputThing and its \a inputState to an \a outputThing and its \a outputState.
Whenever the input state changes, the output state is set accordingly. If the input state is
writable, the connection will be bidirectional, that is, a change of the output state will also
reflect on the input state.
Connections can be logically inverted.
Connections need to be compatible. This means, only states which have a defined ioState of type "input"
can be connected to states which habe a defined ioState of type "output". Additionally, the digital/analog
type needs to match. In other words, states with ioType "digitalInput" can be connected to states with ioType
"digitaOutput" and states with ioType "analogInput" can be connected to states with ioType "analogOutput".
*/
IOConnectionResult ThingManager::connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState, bool inverted)
{
IOConnection connection(IOConnectionId::createIOConnectionId(), inputThing, inputState, outputThing, outputState, inverted);
return connectIO(connection);
}

View File

@ -35,6 +35,7 @@
#include "thing.h"
#include "integrationplugin.h"
#include "ioconnection.h"
#include "types/interface.h"
#include "types/vendor.h"
#include "types/browseritem.h"
@ -89,10 +90,18 @@ public:
virtual BrowserActionInfo* executeBrowserItem(const BrowserAction &browserAction) = 0;
virtual BrowserItemActionInfo* executeBrowserItemAction(const BrowserItemAction &browserItemAction) = 0;
virtual IOConnections ioConnections(const ThingId &thingId = ThingId()) const = 0;
IOConnectionResult connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState, bool inverted = false);
virtual Thing::ThingError disconnectIO(const IOConnectionId &ioConnectionId) = 0;
virtual QString translate(const PluginId &pluginId, const QString &string, const QLocale &locale) = 0;
virtual ParamType translateParamType(const PluginId &pluginId, const ParamType &paramType, const QLocale &locale) = 0;
virtual ThingClass translateThingClass(const ThingClass &thingClass, const QLocale &locale) = 0;
virtual Vendor translateVendor(const Vendor &vendor, const QLocale &locale) = 0;
protected:
virtual IOConnectionResult connectIO(const IOConnection &connection) = 0;
signals:
void pluginConfigChanged(const PluginId &id, const ParamList &config);
void eventTriggered(const Event &event);
@ -102,6 +111,8 @@ signals:
void thingAdded(Thing *thing);
void thingChanged(Thing *device);
void thingSettingChanged(const ThingId &thingId, const ParamTypeId &settingParamTypeId, const QVariant &value);
void ioConnectionAdded(const IOConnection &ioConnection);
void ioConnectionRemoved(const IOConnectionId &ioConnectionId);
};
#endif // THINGMANAGER_H

View File

@ -3,7 +3,8 @@
"states": [
{
"name": "temperature",
"type": "double"
"type": "double",
"unit": "DegreeCelsius"
}
]
}

View File

@ -14,6 +14,7 @@ HEADERS += \
integrations/browseritemactioninfo.h \
integrations/browseritemresult.h \
integrations/integrationplugin.h \
integrations/ioconnection.h \
integrations/pluginmetadata.h \
integrations/browseresult.h \
integrations/thing.h \
@ -97,6 +98,7 @@ SOURCES += \
integrations/browseritemactioninfo.cpp \
integrations/browseritemresult.cpp \
integrations/integrationplugin.cpp \
integrations/ioconnection.cpp \
integrations/pluginmetadata.cpp \
integrations/browseresult.cpp \
integrations/thing.cpp \

View File

@ -60,6 +60,10 @@
This role will create the \b{device-states.conf} file and is used to store the configured \l{Device} \l{State}{States}.
\value SettingsRoleTags
This role will create the \b{tags.conf} file and is used to store the \l{Tag}{Tags}.
\value SettingsRoleMqttPolicies
This role will create the \b{mqttpolicies.conf} file and is used to store the \l{MqttPolicy}{MqttPolicies}.
\value SettingsRoleIOConnections
This role will create the \b{ioconnections.conf} file and is used to store the \l{IOConnection}{IOConnections}.
*/
@ -115,6 +119,9 @@ NymeaSettings::NymeaSettings(const SettingsRole &role, QObject *parent):
case SettingsRoleMqttPolicies:
fileName = "mqttpolicies.conf";
break;
case SettingsRoleIOConnections:
fileName = "ioconnections.conf";
break;
}
m_settings = new QSettings(basePath + settingsPrefix + fileName, QSettings::IniFormat, this);
}

View File

@ -51,6 +51,7 @@ public:
SettingsRoleThingStates,
SettingsRoleTags,
SettingsRoleMqttPolicies,
SettingsRoleIOConnections,
};
explicit NymeaSettings(const SettingsRole &role = SettingsRoleNone, QObject *parent = nullptr);

View File

@ -47,7 +47,6 @@
/*! Construct an Action with the given \a deviceId and \a actionTypeId. */
Action::Action(const ActionTypeId &actionTypeId, const ThingId &thingId) :
m_id(ActionId::createActionId()),
m_actionTypeId(actionTypeId),
m_thingId(thingId)
{
@ -55,19 +54,12 @@ Action::Action(const ActionTypeId &actionTypeId, const ThingId &thingId) :
/*! Construct a copy of an \a other Action. */
Action::Action(const Action &other):
m_id(other.id()),
m_actionTypeId(other.actionTypeId()),
m_thingId(other.thingId()),
m_params(other.params())
{
}
/*! Returns the actionId for this Action. */
ActionId Action::id() const
{
return m_id;
}
/*! An Action is valid if actionTypeId and deviceId are valid uuids. Returns true if valid, false if not. */
bool Action::isValid() const
{
@ -123,7 +115,6 @@ Param Action::param(const ParamTypeId &paramTypeId) const
/*! Copy the data to an \l{Action} from an \a other action. */
void Action::operator =(const Action &other)
{
m_id = other.id();
m_actionTypeId = other.actionTypeId();
m_params = other.params();
}

View File

@ -49,8 +49,6 @@ public:
explicit Action(const ActionTypeId &actionTypeId = ActionTypeId(), const ThingId &thingId = ThingId());
Action(const Action &other);
ActionId id() const;
bool isValid() const;
ActionTypeId actionTypeId() const;
@ -64,7 +62,6 @@ public:
void operator=(const Action &other);
private:
ActionId m_id;
ActionTypeId m_actionTypeId;
ThingId m_thingId;
ParamList m_params;

View File

@ -31,7 +31,6 @@
#include "browseraction.h"
BrowserAction::BrowserAction(const ThingId &thingId, const QString &itemId):
m_id(ActionId::createActionId()),
m_thingId(thingId),
m_itemId(itemId)
{
@ -39,21 +38,15 @@ BrowserAction::BrowserAction(const ThingId &thingId, const QString &itemId):
}
BrowserAction::BrowserAction(const BrowserAction &other):
m_id(other.id()),
m_thingId(other.thingId()),
m_itemId(other.itemId())
{
}
ActionId BrowserAction::id() const
{
return m_id;
}
bool BrowserAction::isValid() const
{
return !m_id.isNull() && !m_thingId.isNull() && !m_itemId.isNull();
return !m_thingId.isNull() && !m_itemId.isNull();
}
ThingId BrowserAction::thingId() const
@ -68,7 +61,6 @@ QString BrowserAction::itemId() const
void BrowserAction::operator=(const BrowserAction &other)
{
m_id = other.id();
m_thingId = other.thingId();
m_itemId = other.itemId();
}

View File

@ -39,8 +39,6 @@ public:
explicit BrowserAction(const ThingId &thingId = ThingId(), const QString &itemId = QString());
BrowserAction(const BrowserAction &other);
ActionId id() const;
bool isValid() const;
ThingId thingId() const;
@ -48,7 +46,6 @@ public:
void operator=(const BrowserAction &other);
private:
ActionId m_id;
ThingId m_thingId;
QString m_itemId;
};

View File

@ -32,7 +32,6 @@
BrowserItemAction::BrowserItemAction(const ThingId &thingId, const QString &itemId, const ActionTypeId &actionTypeId, const ParamList &params):
m_id(ActionId::createActionId()),
m_thingId(thingId),
m_itemId(itemId),
m_actionTypeId(actionTypeId),
@ -42,7 +41,6 @@ BrowserItemAction::BrowserItemAction(const ThingId &thingId, const QString &item
}
BrowserItemAction::BrowserItemAction(const BrowserItemAction &other):
m_id(other.id()),
m_thingId(other.thingId()),
m_itemId(other.itemId()),
m_actionTypeId(other.actionTypeId()),
@ -51,14 +49,9 @@ BrowserItemAction::BrowserItemAction(const BrowserItemAction &other):
}
ActionId BrowserItemAction::id() const
{
return m_id;
}
bool BrowserItemAction::isValid() const
{
return !m_id.isNull() && !m_thingId.isNull() && !m_itemId.isNull();
return !m_thingId.isNull() && !m_itemId.isNull();
}
ThingId BrowserItemAction::thingId() const
@ -83,7 +76,6 @@ ParamList BrowserItemAction::params() const
void BrowserItemAction::operator=(const BrowserItemAction &other)
{
m_id = other.id();
m_thingId = other.thingId();
m_itemId = other.itemId();
m_actionTypeId = other.actionTypeId();

View File

@ -40,8 +40,6 @@ public:
explicit BrowserItemAction(const ThingId &thingId = ThingId(), const QString &itemId = QString(), const ActionTypeId &actionTypeId = ActionTypeId(), const ParamList &params = ParamList());
BrowserItemAction(const BrowserItemAction &other);
ActionId id() const;
bool isValid() const;
ThingId thingId() const;
@ -54,7 +52,6 @@ public:
void operator=(const BrowserItemAction &other);
private:
ActionId m_id;
ThingId m_thingId;
QString m_itemId;
ActionTypeId m_actionTypeId;

View File

@ -46,8 +46,7 @@
#include "event.h"
/*! Constructs an Event. */
Event::Event():
m_id(EventId::createEventId())
Event::Event()
{
}
@ -56,7 +55,6 @@ Event::Event():
* specifies if the \l{Event} will be autogeneratet or not. The parameters must
* match the description in the reflecting \l{Event}. */
Event::Event(const EventTypeId &eventTypeId, const ThingId &thingId, const ParamList &params, bool isStateChangeEvent):
m_id(EventId::createEventId()),
m_eventTypeId(eventTypeId),
m_thingId(thingId),
m_params(params),
@ -64,13 +62,6 @@ Event::Event(const EventTypeId &eventTypeId, const ThingId &thingId, const Param
{
}
/*! Returns the Id of this Event. Each newly created Event will have a new UUID generated. The id will be copied
* in the copy constructor. */
EventId Event::eventId() const
{
return m_id;
}
/*! Returns the id of the \l{EventType} which describes this Event. */
EventTypeId Event::eventTypeId() const
{

View File

@ -50,8 +50,6 @@ public:
Event();
Event(const EventTypeId &eventTypeId, const ThingId &thingId, const ParamList &params = ParamList(), bool isStateChangeEvent = false);
EventId eventId() const;
EventTypeId eventTypeId() const;
void setEventTypeId(const EventTypeId &eventTypeId);
@ -67,7 +65,6 @@ public:
bool isStateChangeEvent() const;
private:
EventId m_id;
EventTypeId m_eventTypeId;
ThingId m_thingId;
ParamList m_params;

View File

@ -51,18 +51,11 @@ State::State()
/*! Constructs a State reflecting the \l{StateType} given by \a stateTypeId
* and associated with the \l{Device} given by \a deviceId */
State::State(const StateTypeId &stateTypeId, const ThingId &deviceId):
m_id(StateId::createStateId()),
m_stateTypeId(stateTypeId),
m_thingId(deviceId)
{
}
/*! Returns the id of this State. */
StateId State::id() const
{
return m_id;
}
/*! Returns the id of the StateType describing this State. */
StateTypeId State::stateTypeId() const
{

View File

@ -47,8 +47,6 @@ public:
State();
State(const StateTypeId &stateTypeId, const ThingId &deviceId);
StateId id() const;
StateTypeId stateTypeId() const;
ThingId thingId() const;
@ -56,7 +54,6 @@ public:
void setValue(const QVariant &value);
private:
StateId m_id;
StateTypeId m_stateTypeId;
ThingId m_thingId;
QVariant m_value;

View File

@ -172,6 +172,30 @@ void StateType::setUnit(const Types::Unit &unit)
m_unit = unit;
}
/*! Returns the IO type of this StateType. */
Types::IOType StateType::ioType() const
{
return m_ioType;
}
/*! Sets the IO type of this StateType. */
void StateType::setIOType(Types::IOType ioType)
{
m_ioType = ioType;
}
/*! Returns whether the StateType is writable or not. A writable StateType will have an according ActionType defined.*/
bool StateType::writable() const
{
return m_writable;
}
/*! Sets the writable property to true */
void StateType::setWritable(bool writable)
{
m_writable = writable;
}
/*! Returns true if this StateType is to be cached. This means, the last state value will be stored to disk upon shutdown and restored on reboot. If this is false, states will be initialized with the default value on each boot. By default all states are cached by the system. */
bool StateType::cached() const
{
@ -189,7 +213,7 @@ QStringList StateType::typeProperties()
{
return QStringList() << "id" << "name" << "displayName" << "displayNameEvent" << "type" << "defaultValue"
<< "cached" << "unit" << "minValue" << "maxValue" << "possibleValues" << "writable"
<< "displayNameAction";
<< "displayNameAction" << "ioType";
}
/*! Returns a list of mandatory properties a DeviceClass definition must have. */
@ -205,6 +229,16 @@ StateTypes::StateTypes(const QList<StateType> &other)
}
}
bool StateTypes::contains(const StateTypeId &stateTypeId)
{
foreach (const StateType &stateType, *this) {
if (stateType.id() == stateTypeId) {
return true;
}
}
return false;
}
QVariant StateTypes::get(int index) const
{
return QVariant::fromValue(at(index));

View File

@ -46,6 +46,7 @@ class LIBNYMEA_EXPORT StateType
Q_PROPERTY(int index READ index WRITE setIndex)
Q_PROPERTY(QVariant defaultValue READ defaultValue WRITE setDefaultValue)
Q_PROPERTY(Types::Unit unit READ unit WRITE setUnit USER true)
Q_PROPERTY(Types::IOType ioType READ ioType WRITE setIOType USER true)
Q_PROPERTY(QVariant minValue READ minValue WRITE setMinValue USER true)
Q_PROPERTY(QVariant maxValue READ maxValue WRITE setMaxValue USER true)
Q_PROPERTY(QVariantList possibleValues READ possibleValues WRITE setPossibleValues USER true)
@ -83,6 +84,12 @@ public:
Types::Unit unit() const;
void setUnit(const Types::Unit &unit);
Types::IOType ioType() const;
void setIOType(Types::IOType ioType);
bool writable() const;
void setWritable(bool writable);
bool cached() const;
void setCached(bool cached);
@ -100,6 +107,8 @@ private:
QVariant m_maxValue;
QVariantList m_possibleValues;
Types::Unit m_unit = Types::UnitNone;
Types::IOType m_ioType = Types::IOTypeNone;
bool m_writable = false;
bool m_cached = true;
};
Q_DECLARE_METATYPE(StateType)
@ -111,6 +120,7 @@ class StateTypes: public QList<StateType>
public:
StateTypes() = default;
StateTypes(const QList<StateType> &other);
bool contains(const StateTypeId &stateTypeId);
Q_INVOKABLE QVariant get(int index) const;
Q_INVOKABLE void put(const QVariant &variant);
StateType findByName(const QString &name);

View File

@ -56,14 +56,12 @@ DECLARE_TYPE_ID(ThingDescriptor)
DECLARE_TYPE_ID(ParamType)
DECLARE_TYPE_ID(Param)
DECLARE_TYPE_ID(EventType)
DECLARE_TYPE_ID(Event)
DECLARE_TYPE_ID(StateType)
DECLARE_TYPE_ID(State)
DECLARE_TYPE_ID(ActionType)
DECLARE_TYPE_ID(Action)
DECLARE_TYPE_ID(Plugin)
DECLARE_TYPE_ID(Rule)
DECLARE_TYPE_ID(Browser)
DECLARE_TYPE_ID(IOConnection)
DECLARE_TYPE_ID(PairingTransaction)
@ -165,6 +163,15 @@ public:
BrowserTypeGeneric,
};
Q_ENUM(BrowserType)
enum IOType {
IOTypeNone,
IOTypeDigitalInput,
IOTypeDigitalOutput,
IOTypeAnalogInput,
IOTypeAnalogOutput
};
Q_ENUM(IOType)
};
Q_DECLARE_METATYPE(Types::InputType)

View File

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

View File

@ -246,5 +246,57 @@ extern ParamTypeId inputTypeMockWritableTimestampUIntActionWritableTimestampUInt
extern ThingClassId oAuthGoogleMockThingClassId;
extern ThingClassId oAuthSonosMockThingClassId;
extern ThingClassId userAndPassMockThingClassId;
extern ThingClassId genericIoMockThingClassId;
extern StateTypeId genericIoMockDigitalInput1StateTypeId;
extern StateTypeId genericIoMockDigitalInput2StateTypeId;
extern StateTypeId genericIoMockDigitalOutput1StateTypeId;
extern StateTypeId genericIoMockDigitalOutput2StateTypeId;
extern StateTypeId genericIoMockAnalogInput1StateTypeId;
extern StateTypeId genericIoMockAnalogInput2StateTypeId;
extern StateTypeId genericIoMockAnalogOutput1StateTypeId;
extern StateTypeId genericIoMockAnalogOutput2StateTypeId;
extern EventTypeId genericIoMockDigitalInput1EventTypeId;
extern ParamTypeId genericIoMockDigitalInput1EventDigitalInput1ParamTypeId;
extern EventTypeId genericIoMockDigitalInput2EventTypeId;
extern ParamTypeId genericIoMockDigitalInput2EventDigitalInput2ParamTypeId;
extern EventTypeId genericIoMockDigitalOutput1EventTypeId;
extern ParamTypeId genericIoMockDigitalOutput1EventDigitalOutput1ParamTypeId;
extern EventTypeId genericIoMockDigitalOutput2EventTypeId;
extern ParamTypeId genericIoMockDigitalOutput2EventDigitalOutput2ParamTypeId;
extern EventTypeId genericIoMockAnalogInput1EventTypeId;
extern ParamTypeId genericIoMockAnalogInput1EventAnalogInput1ParamTypeId;
extern EventTypeId genericIoMockAnalogInput2EventTypeId;
extern ParamTypeId genericIoMockAnalogInput2EventAnalogInput2ParamTypeId;
extern EventTypeId genericIoMockAnalogOutput1EventTypeId;
extern ParamTypeId genericIoMockAnalogOutput1EventAnalogOutput1ParamTypeId;
extern EventTypeId genericIoMockAnalogOutput2EventTypeId;
extern ParamTypeId genericIoMockAnalogOutput2EventAnalogOutput2ParamTypeId;
extern ActionTypeId genericIoMockDigitalOutput1ActionTypeId;
extern ParamTypeId genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId;
extern ActionTypeId genericIoMockDigitalOutput2ActionTypeId;
extern ParamTypeId genericIoMockDigitalOutput2ActionDigitalOutput2ParamTypeId;
extern ActionTypeId genericIoMockAnalogInput1ActionTypeId;
extern ParamTypeId genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId;
extern ActionTypeId genericIoMockAnalogOutput1ActionTypeId;
extern ParamTypeId genericIoMockAnalogOutput1ActionAnalogOutput1ParamTypeId;
extern ActionTypeId genericIoMockAnalogOutput2ActionTypeId;
extern ParamTypeId genericIoMockAnalogOutput2ActionAnalogOutput2ParamTypeId;
extern ThingClassId virtualIoLightMockThingClassId;
extern StateTypeId virtualIoLightMockPowerStateTypeId;
extern EventTypeId virtualIoLightMockPowerEventTypeId;
extern ParamTypeId virtualIoLightMockPowerEventPowerParamTypeId;
extern ActionTypeId virtualIoLightMockPowerActionTypeId;
extern ParamTypeId virtualIoLightMockPowerActionPowerParamTypeId;
extern ThingClassId virtualIoTemperatureSensorMockThingClassId;
extern ParamTypeId virtualIoTemperatureSensorMockSettingsMinTempParamTypeId;
extern ParamTypeId virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId;
extern StateTypeId virtualIoTemperatureSensorMockInputStateTypeId;
extern StateTypeId virtualIoTemperatureSensorMockTemperatureStateTypeId;
extern EventTypeId virtualIoTemperatureSensorMockInputEventTypeId;
extern ParamTypeId virtualIoTemperatureSensorMockInputEventInputParamTypeId;
extern EventTypeId virtualIoTemperatureSensorMockTemperatureEventTypeId;
extern ParamTypeId virtualIoTemperatureSensorMockTemperatureEventTemperatureParamTypeId;
extern ActionTypeId virtualIoTemperatureSensorMockInputActionTypeId;
extern ParamTypeId virtualIoTemperatureSensorMockInputActionInputParamTypeId;
#endif // EXTERNPLUGININFO_H

View File

@ -233,6 +233,24 @@ void IntegrationPluginMock::setupThing(ThingSetupInfo *info)
return;
}
if (info->thing()->thingClassId() == genericIoMockThingClassId) {
qCDebug(dcMock()) << "Generic IO mock setup complete";
info->finish(Thing::ThingErrorNoError);
return;
}
if (info->thing()->thingClassId() == virtualIoLightMockThingClassId) {
qCDebug(dcMock()) << "Virtual IO mock light setup complete";
info->finish(Thing::ThingErrorNoError);
return;
}
if (info->thing()->thingClassId() == virtualIoTemperatureSensorMockThingClassId) {
qCDebug(dcMock()) << "Virtual IO mock temperature sensor setup complete";
info->finish(Thing::ThingErrorNoError);
return;
}
qCWarning(dcMock()) << "Unhandled thing class" << info->thing()->thingClass();
info->finish(Thing::ThingErrorThingClassNotFound);
}
@ -689,9 +707,61 @@ void IntegrationPluginMock::executeAction(ThingActionInfo *info)
info->thing()->setStateValue(inputTypeMockWritableTimestampUIntStateTypeId, info->action().param(inputTypeMockWritableTimestampUIntActionWritableTimestampUIntParamTypeId).value().toULongLong());
}
return;
}
info->finish(Thing::ThingErrorThingClassNotFound);
if (info->thing()->thingClassId() == virtualIoLightMockThingClassId) {
if (info->action().actionTypeId() == virtualIoLightMockPowerActionTypeId) {
qCDebug(dcMock()) << "ExecuteAction for virtual light power action with param" << info->action().param(virtualIoLightMockPowerActionPowerParamTypeId).value();
info->thing()->setStateValue(virtualIoLightMockPowerStateTypeId, info->action().param(virtualIoLightMockPowerActionPowerParamTypeId).value());
info->finish(Thing::ThingErrorNoError);
return;
}
}
if (info->thing()->thingClassId() == genericIoMockThingClassId) {
if (info->action().actionTypeId() == genericIoMockDigitalOutput1ActionTypeId) {
qCDebug(dcMock()) << "Setting digital output 1 to" << info->action().param(genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId).value().toBool();
info->thing()->setStateValue(genericIoMockDigitalOutput1StateTypeId, info->action().param(genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId).value().toBool());
info->finish(Thing::ThingErrorNoError);
return;
}
if (info->action().actionTypeId() == genericIoMockDigitalOutput2ActionTypeId) {
info->thing()->setStateValue(genericIoMockDigitalOutput2StateTypeId, info->action().param(genericIoMockDigitalOutput2ActionDigitalOutput2ParamTypeId).value().toBool());
info->finish(Thing::ThingErrorNoError);
return;
}
if (info->action().actionTypeId() == genericIoMockAnalogInput1ActionTypeId) {
qCDebug(dcMock()) << "ExecuteAction for virtual io analog in 1 action with param" << info->action().param(genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId).value();
info->thing()->setStateValue(genericIoMockAnalogInput1StateTypeId, info->action().param(genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId).value());
info->finish(Thing::ThingErrorNoError);
return;
}
if (info->action().actionTypeId() == genericIoMockAnalogOutput1ActionTypeId) {
info->thing()->setStateValue(genericIoMockAnalogOutput1StateTypeId, info->action().param(genericIoMockAnalogOutput1ActionAnalogOutput1ParamTypeId).value());
info->finish(Thing::ThingErrorNoError);
return;
}
if (info->action().actionTypeId() == genericIoMockAnalogOutput2ActionTypeId) {
info->thing()->setStateValue(genericIoMockAnalogOutput2StateTypeId, info->action().param(genericIoMockAnalogOutput2ActionAnalogOutput2ParamTypeId).value());
info->finish(Thing::ThingErrorNoError);
return;
}
}
if (info->thing()->thingClassId() == virtualIoTemperatureSensorMockThingClassId) {
if (info->action().actionTypeId() == virtualIoTemperatureSensorMockInputActionTypeId) {
double value = info->action().param(virtualIoTemperatureSensorMockInputActionInputParamTypeId).value().toDouble();
info->thing()->setStateValue(virtualIoTemperatureSensorMockInputStateTypeId, value);
double minTemp = info->thing()->setting(virtualIoTemperatureSensorMockSettingsMinTempParamTypeId).toDouble();
double maxTemp = info->thing()->setting(virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId).toDouble();
double temp = minTemp + (maxTemp - minTemp) * value;
info->thing()->setStateValue(virtualIoTemperatureSensorMockTemperatureStateTypeId, temp);
info->finish(Thing::ThingErrorNoError);
return;
}
}
qCWarning(dcMock()) << "Unhandled executeAction call in mock plugin!";
}
void IntegrationPluginMock::executeBrowserItem(BrowserActionInfo *info)
@ -728,7 +798,6 @@ void IntegrationPluginMock::executeBrowserItem(BrowserActionInfo *info)
void IntegrationPluginMock::executeBrowserItemAction(BrowserItemActionInfo *info)
{
qCDebug(dcMock()) << "TODO" << info << info->browserItemAction().id();
if (info->browserItemAction().actionTypeId() == mockAddToFavoritesBrowserItemActionTypeId) {
VirtualFsNode *node = m_virtualFs->findNode(info->browserItemAction().itemId());

View File

@ -868,6 +868,176 @@
"displayName": "Mocked Thing (User & Password)",
"createMethods": ["discovery", "user"],
"setupMethod": "userandpassword"
},
{
"id": "7cbd729a-465b-4ccb-b59c-5733039dbbed",
"name": "genericIoMock",
"displayName": "Generic IO pins",
"createMethods": ["user"],
"setupMethod": "justAdd",
"stateTypes": [
{
"id": "07165c12-4d53-45c0-8bf1-34618443b706",
"name": "digitalInput1",
"displayName": "Digital input 1",
"displayNameEvent": "Digital input 1 changed",
"type": "bool",
"defaultValue": false,
"ioType": "digitalInput"
},
{
"id": "0a4362ba-a086-4540-84ba-107ef7b99ed8",
"name": "digitalInput2",
"displayName": "Digital input 2",
"displayNameEvent": "Digital input 2 changed",
"type": "bool",
"defaultValue": false,
"ioType": "digitalInput"
},
{
"id": "d6fcdb52-f7c3-423b-b9f5-1e29f164c42e",
"name": "digitalOutput1",
"displayName": "Digital Output 1",
"displayNameEvent": "Digital Output 1 changed",
"displayNameAction": "Set Digital Output 1",
"type": "bool",
"defaultValue": false,
"ioType": "digitalOutput",
"writable": true
},
{
"id": "35de8b68-0cf3-4850-a27d-cf9c4a26921f",
"name": "digitalOutput2",
"displayName": "Digital Output 2",
"displayNameEvent": "Digital Output 2 changed",
"displayNameAction": "Set Digital Output 2",
"type": "bool",
"defaultValue": false,
"ioType": "digitalOutput",
"writable": true
},
{
"id": "ac56977c-cbba-47c6-a827-5735d8b0aed6",
"name": "analogInput1",
"displayName": "Analog Input 1",
"displayNameEvent": "Analog Input 1 changed",
"type": "double",
"defaultValue": 0,
"ioType": "analogInput",
"minValue": 0,
"maxValue": 3.3,
"writable": true,
"displayNameAction": "Set analog input 1"
},
{
"id": "8e07e57e-ba4e-42df-81ee-5b72ed074532",
"name": "analogInput2",
"displayName": "Analog Input 2",
"displayNameEvent": "Analog Input 2 changed",
"type": "double",
"defaultValue": 0,
"ioType": "analogInput",
"minValue": 0,
"maxValue": 5
},
{
"id": "70cf053e-4abc-4d88-8e1e-2bd9a62256c7",
"name": "analogOutput1",
"displayName": "Analog Output 1",
"displayNameEvent": "Analog Output 1 changed",
"displayNameAction": "Set Output Input 1",
"type": "double",
"defaultValue": 0,
"ioType": "analogOutput",
"minValue": 0,
"maxValue": 3.3,
"writable": true
},
{
"id": "e40bcf7d-47b8-41fa-b213-3652a905b376",
"name": "analogOutput2",
"displayName": "Analog Output 2",
"displayNameEvent": "Analog Output 2 changed",
"displayNameAction": "Set Output Input 2",
"type": "double",
"defaultValue": 0,
"ioType": "analogOutput",
"minValue": 0,
"maxValue": 5,
"writable": true
}
]
},
{
"id": "98ab137e-757e-43f8-9d9b-5d50d990242a",
"name": "virtualIoLightMock",
"displayName": "Generic Light (Mock)",
"createMethods": ["user"],
"setupMethod": "justAdd",
"interfaces": ["light"],
"stateTypes": [
{
"id": "d1917b3d-1530-4cf9-90f7-263ee88e714b",
"name": "power",
"displayName": "Power",
"displayNameEvent": "Power changed",
"displayNameAction": "Set power",
"type": "bool",
"defaultValue": false,
"ioType": "digitalInput",
"writable": true
}
]
},
{
"id": "f8917e12-c9cb-4ea1-a06e-1ce6db2194f3",
"name": "virtualIoTemperatureSensorMock",
"displayName": "Generic Temperature Sensor (Mock)",
"createMethods": ["user"],
"setupMethod": "justAdd",
"interfaces": ["temperaturesensor"],
"settingsTypes": [
{
"id": "803cddbf-94c7-4f35-bc7a-18698b03b942",
"name": "minTemp",
"displayName": "Minimum temperature",
"type": "double",
"defaultValue": -20,
"unit": "DegreeCelsius"
},
{
"id": "7077c56f-c35b-4252-8c15-8fb549be04ce",
"name": "maxTemp",
"displayName": "Maximum temperature",
"type": "double",
"defaultValue": 50,
"unit": "DegreeCelsius"
}
],
"stateTypes": [
{
"id": "fd341f72-6d9a-4812-9f66-47197c48a935",
"name": "input",
"displayName": "Input",
"displayNameEvent": "Input changed",
"displayNameAction": "Set input",
"type": "double",
"defaultValue": 0,
"ioType": "analogOutput",
"writable": true,
"minValue": 0,
"maxValue": 1
},
{
"id": "db9cc518-1012-47e2-8212-6e616fed07a6",
"name": "temperature",
"displayName": "Temperature",
"displayNameEvent": "Temperature changed",
"type": "double",
"unit": "DegreeCelsius",
"defaultValue": -20
}
]
}
]
}

View File

@ -9,7 +9,7 @@
#include <QLoggingCategory>
#include <QObject>
extern "C" const QString libnymea_api_version() { return QString("5.0.0");}
extern "C" const QString libnymea_api_version() { return QString("6.0.0");}
Q_DECLARE_LOGGING_CATEGORY(dcMock)
Q_LOGGING_CATEGORY(dcMock, "Mock")
@ -250,11 +250,108 @@ ParamTypeId inputTypeMockWritableTimestampUIntActionWritableTimestampUIntParamTy
ThingClassId oAuthGoogleMockThingClassId = ThingClassId("{805d1692-7bd0-449a-9d5c-43a332ff58f4}");
ThingClassId oAuthSonosMockThingClassId = ThingClassId("{783c615b-7bd6-49a4-98b0-8d1deb3c7156}");
ThingClassId userAndPassMockThingClassId = ThingClassId("{6fe07a77-9c07-4736-81e2-d504314bbcb9}");
ThingClassId genericIoMockThingClassId = ThingClassId("{7cbd729a-465b-4ccb-b59c-5733039dbbed}");
StateTypeId genericIoMockDigitalInput1StateTypeId = StateTypeId("{07165c12-4d53-45c0-8bf1-34618443b706}");
StateTypeId genericIoMockDigitalInput2StateTypeId = StateTypeId("{0a4362ba-a086-4540-84ba-107ef7b99ed8}");
StateTypeId genericIoMockDigitalOutput1StateTypeId = StateTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}");
StateTypeId genericIoMockDigitalOutput2StateTypeId = StateTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}");
StateTypeId genericIoMockAnalogInput1StateTypeId = StateTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}");
StateTypeId genericIoMockAnalogInput2StateTypeId = StateTypeId("{8e07e57e-ba4e-42df-81ee-5b72ed074532}");
StateTypeId genericIoMockAnalogOutput1StateTypeId = StateTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}");
StateTypeId genericIoMockAnalogOutput2StateTypeId = StateTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}");
EventTypeId genericIoMockDigitalInput1EventTypeId = EventTypeId("{07165c12-4d53-45c0-8bf1-34618443b706}");
ParamTypeId genericIoMockDigitalInput1EventDigitalInput1ParamTypeId = ParamTypeId("{07165c12-4d53-45c0-8bf1-34618443b706}");
EventTypeId genericIoMockDigitalInput2EventTypeId = EventTypeId("{0a4362ba-a086-4540-84ba-107ef7b99ed8}");
ParamTypeId genericIoMockDigitalInput2EventDigitalInput2ParamTypeId = ParamTypeId("{0a4362ba-a086-4540-84ba-107ef7b99ed8}");
EventTypeId genericIoMockDigitalOutput1EventTypeId = EventTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}");
ParamTypeId genericIoMockDigitalOutput1EventDigitalOutput1ParamTypeId = ParamTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}");
EventTypeId genericIoMockDigitalOutput2EventTypeId = EventTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}");
ParamTypeId genericIoMockDigitalOutput2EventDigitalOutput2ParamTypeId = ParamTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}");
EventTypeId genericIoMockAnalogInput1EventTypeId = EventTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}");
ParamTypeId genericIoMockAnalogInput1EventAnalogInput1ParamTypeId = ParamTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}");
EventTypeId genericIoMockAnalogInput2EventTypeId = EventTypeId("{8e07e57e-ba4e-42df-81ee-5b72ed074532}");
ParamTypeId genericIoMockAnalogInput2EventAnalogInput2ParamTypeId = ParamTypeId("{8e07e57e-ba4e-42df-81ee-5b72ed074532}");
EventTypeId genericIoMockAnalogOutput1EventTypeId = EventTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}");
ParamTypeId genericIoMockAnalogOutput1EventAnalogOutput1ParamTypeId = ParamTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}");
EventTypeId genericIoMockAnalogOutput2EventTypeId = EventTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}");
ParamTypeId genericIoMockAnalogOutput2EventAnalogOutput2ParamTypeId = ParamTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}");
ActionTypeId genericIoMockDigitalOutput1ActionTypeId = ActionTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}");
ParamTypeId genericIoMockDigitalOutput1ActionDigitalOutput1ParamTypeId = ParamTypeId("{d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}");
ActionTypeId genericIoMockDigitalOutput2ActionTypeId = ActionTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}");
ParamTypeId genericIoMockDigitalOutput2ActionDigitalOutput2ParamTypeId = ParamTypeId("{35de8b68-0cf3-4850-a27d-cf9c4a26921f}");
ActionTypeId genericIoMockAnalogInput1ActionTypeId = ActionTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}");
ParamTypeId genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId = ParamTypeId("{ac56977c-cbba-47c6-a827-5735d8b0aed6}");
ActionTypeId genericIoMockAnalogOutput1ActionTypeId = ActionTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}");
ParamTypeId genericIoMockAnalogOutput1ActionAnalogOutput1ParamTypeId = ParamTypeId("{70cf053e-4abc-4d88-8e1e-2bd9a62256c7}");
ActionTypeId genericIoMockAnalogOutput2ActionTypeId = ActionTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}");
ParamTypeId genericIoMockAnalogOutput2ActionAnalogOutput2ParamTypeId = ParamTypeId("{e40bcf7d-47b8-41fa-b213-3652a905b376}");
ThingClassId virtualIoLightMockThingClassId = ThingClassId("{98ab137e-757e-43f8-9d9b-5d50d990242a}");
StateTypeId virtualIoLightMockPowerStateTypeId = StateTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}");
EventTypeId virtualIoLightMockPowerEventTypeId = EventTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}");
ParamTypeId virtualIoLightMockPowerEventPowerParamTypeId = ParamTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}");
ActionTypeId virtualIoLightMockPowerActionTypeId = ActionTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}");
ParamTypeId virtualIoLightMockPowerActionPowerParamTypeId = ParamTypeId("{d1917b3d-1530-4cf9-90f7-263ee88e714b}");
ThingClassId virtualIoTemperatureSensorMockThingClassId = ThingClassId("{f8917e12-c9cb-4ea1-a06e-1ce6db2194f3}");
ParamTypeId virtualIoTemperatureSensorMockSettingsMinTempParamTypeId = ParamTypeId("{803cddbf-94c7-4f35-bc7a-18698b03b942}");
ParamTypeId virtualIoTemperatureSensorMockSettingsMaxTempParamTypeId = ParamTypeId("{7077c56f-c35b-4252-8c15-8fb549be04ce}");
StateTypeId virtualIoTemperatureSensorMockInputStateTypeId = StateTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}");
StateTypeId virtualIoTemperatureSensorMockTemperatureStateTypeId = StateTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}");
EventTypeId virtualIoTemperatureSensorMockInputEventTypeId = EventTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}");
ParamTypeId virtualIoTemperatureSensorMockInputEventInputParamTypeId = ParamTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}");
EventTypeId virtualIoTemperatureSensorMockTemperatureEventTypeId = EventTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}");
ParamTypeId virtualIoTemperatureSensorMockTemperatureEventTemperatureParamTypeId = ParamTypeId("{db9cc518-1012-47e2-8212-6e616fed07a6}");
ActionTypeId virtualIoTemperatureSensorMockInputActionTypeId = ActionTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}");
ParamTypeId virtualIoTemperatureSensorMockInputActionInputParamTypeId = ParamTypeId("{fd341f72-6d9a-4812-9f66-47197c48a935}");
const QString translations[] {
//: The name of the Browser Item ActionType ({00b8f0a8-99ca-4aa4-833d-59eb8d4d6de3}) of ThingClass mock
QT_TRANSLATE_NOOP("mock", "Add to favorites"),
//: The name of the ParamType (ThingClass: genericIoMock, ActionType: analogInput1, ID: {ac56977c-cbba-47c6-a827-5735d8b0aed6})
QT_TRANSLATE_NOOP("mock", "Analog Input 1"),
//: The name of the ParamType (ThingClass: genericIoMock, EventType: analogInput1, ID: {ac56977c-cbba-47c6-a827-5735d8b0aed6})
QT_TRANSLATE_NOOP("mock", "Analog Input 1"),
//: The name of the StateType ({ac56977c-cbba-47c6-a827-5735d8b0aed6}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Analog Input 1"),
//: The name of the EventType ({ac56977c-cbba-47c6-a827-5735d8b0aed6}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Analog Input 1 changed"),
//: The name of the ParamType (ThingClass: genericIoMock, EventType: analogInput2, ID: {8e07e57e-ba4e-42df-81ee-5b72ed074532})
QT_TRANSLATE_NOOP("mock", "Analog Input 2"),
//: The name of the StateType ({8e07e57e-ba4e-42df-81ee-5b72ed074532}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Analog Input 2"),
//: The name of the EventType ({8e07e57e-ba4e-42df-81ee-5b72ed074532}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Analog Input 2 changed"),
//: The name of the ParamType (ThingClass: genericIoMock, ActionType: analogOutput1, ID: {70cf053e-4abc-4d88-8e1e-2bd9a62256c7})
QT_TRANSLATE_NOOP("mock", "Analog Output 1"),
//: The name of the ParamType (ThingClass: genericIoMock, EventType: analogOutput1, ID: {70cf053e-4abc-4d88-8e1e-2bd9a62256c7})
QT_TRANSLATE_NOOP("mock", "Analog Output 1"),
//: The name of the StateType ({70cf053e-4abc-4d88-8e1e-2bd9a62256c7}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Analog Output 1"),
//: The name of the EventType ({70cf053e-4abc-4d88-8e1e-2bd9a62256c7}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Analog Output 1 changed"),
//: The name of the ParamType (ThingClass: genericIoMock, ActionType: analogOutput2, ID: {e40bcf7d-47b8-41fa-b213-3652a905b376})
QT_TRANSLATE_NOOP("mock", "Analog Output 2"),
//: The name of the ParamType (ThingClass: genericIoMock, EventType: analogOutput2, ID: {e40bcf7d-47b8-41fa-b213-3652a905b376})
QT_TRANSLATE_NOOP("mock", "Analog Output 2"),
//: The name of the StateType ({e40bcf7d-47b8-41fa-b213-3652a905b376}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Analog Output 2"),
//: The name of the EventType ({e40bcf7d-47b8-41fa-b213-3652a905b376}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Analog Output 2 changed"),
//: The name of the ParamType (ThingClass: inputTypeMock, EventType: bool, ID: {3bad3a09-5826-4ed7-a832-10e3e2ee2a7d})
QT_TRANSLATE_NOOP("mock", "Bool"),
@ -273,6 +370,48 @@ const QString translations[] {
//: The name of the EventType ({4507d5c6-b692-4bd6-87f2-00364bc0cb4d}) of ThingClass inputTypeMock
QT_TRANSLATE_NOOP("mock", "Color changed"),
//: The name of the ParamType (ThingClass: genericIoMock, ActionType: digitalOutput1, ID: {d6fcdb52-f7c3-423b-b9f5-1e29f164c42e})
QT_TRANSLATE_NOOP("mock", "Digital Output 1"),
//: The name of the ParamType (ThingClass: genericIoMock, EventType: digitalOutput1, ID: {d6fcdb52-f7c3-423b-b9f5-1e29f164c42e})
QT_TRANSLATE_NOOP("mock", "Digital Output 1"),
//: The name of the StateType ({d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Digital Output 1"),
//: The name of the EventType ({d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Digital Output 1 changed"),
//: The name of the ParamType (ThingClass: genericIoMock, ActionType: digitalOutput2, ID: {35de8b68-0cf3-4850-a27d-cf9c4a26921f})
QT_TRANSLATE_NOOP("mock", "Digital Output 2"),
//: The name of the ParamType (ThingClass: genericIoMock, EventType: digitalOutput2, ID: {35de8b68-0cf3-4850-a27d-cf9c4a26921f})
QT_TRANSLATE_NOOP("mock", "Digital Output 2"),
//: The name of the StateType ({35de8b68-0cf3-4850-a27d-cf9c4a26921f}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Digital Output 2"),
//: The name of the EventType ({35de8b68-0cf3-4850-a27d-cf9c4a26921f}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Digital Output 2 changed"),
//: The name of the ParamType (ThingClass: genericIoMock, EventType: digitalInput1, ID: {07165c12-4d53-45c0-8bf1-34618443b706})
QT_TRANSLATE_NOOP("mock", "Digital input 1"),
//: The name of the StateType ({07165c12-4d53-45c0-8bf1-34618443b706}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Digital input 1"),
//: The name of the EventType ({07165c12-4d53-45c0-8bf1-34618443b706}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Digital input 1 changed"),
//: The name of the ParamType (ThingClass: genericIoMock, EventType: digitalInput2, ID: {0a4362ba-a086-4540-84ba-107ef7b99ed8})
QT_TRANSLATE_NOOP("mock", "Digital input 2"),
//: The name of the StateType ({0a4362ba-a086-4540-84ba-107ef7b99ed8}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Digital input 2"),
//: The name of the EventType ({0a4362ba-a086-4540-84ba-107ef7b99ed8}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Digital input 2 changed"),
//: The name of the ParamType (ThingClass: inputTypeMock, EventType: double, ID: {f7d2063d-959e-46ac-8568-8b99722d3b22})
QT_TRANSLATE_NOOP("mock", "Double"),
@ -327,12 +466,33 @@ const QString translations[] {
//: The name of the EventType ({80baec19-54de-4948-ac46-31eabfaceb83}) of ThingClass mock
QT_TRANSLATE_NOOP("mock", "Dummy int state changed"),
//: The name of the ThingClass ({7cbd729a-465b-4ccb-b59c-5733039dbbed})
QT_TRANSLATE_NOOP("mock", "Generic IO pins"),
//: The name of the ThingClass ({98ab137e-757e-43f8-9d9b-5d50d990242a})
QT_TRANSLATE_NOOP("mock", "Generic Light (Mock)"),
//: The name of the ThingClass ({f8917e12-c9cb-4ea1-a06e-1ce6db2194f3})
QT_TRANSLATE_NOOP("mock", "Generic Temperature Sensor (Mock)"),
//: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {9e5f86a0-4bb3-4892-bff8-3fc4032af6e2})
QT_TRANSLATE_NOOP("mock", "IPv4 address"),
//: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {43bf3832-dd48-4090-a836-656e8b60216e})
QT_TRANSLATE_NOOP("mock", "IPv6 address"),
//: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, ActionType: input, ID: {fd341f72-6d9a-4812-9f66-47197c48a935})
QT_TRANSLATE_NOOP("mock", "Input"),
//: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, EventType: input, ID: {fd341f72-6d9a-4812-9f66-47197c48a935})
QT_TRANSLATE_NOOP("mock", "Input"),
//: The name of the StateType ({fd341f72-6d9a-4812-9f66-47197c48a935}) of ThingClass virtualIoTemperatureSensorMock
QT_TRANSLATE_NOOP("mock", "Input"),
//: The name of the EventType ({fd341f72-6d9a-4812-9f66-47197c48a935}) of ThingClass virtualIoTemperatureSensorMock
QT_TRANSLATE_NOOP("mock", "Input changed"),
//: The name of the ParamType (ThingClass: inputTypeMock, EventType: int, ID: {d0fc56ae-5791-4e91-b76c-dadfbc7e7dbb})
QT_TRANSLATE_NOOP("mock", "Int"),
@ -348,6 +508,12 @@ const QString translations[] {
//: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {a8494faf-3a0f-4cf3-84b7-4b39148a838d})
QT_TRANSLATE_NOOP("mock", "Mail address"),
//: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, Type: settings, ID: {7077c56f-c35b-4252-8c15-8fb549be04ce})
QT_TRANSLATE_NOOP("mock", "Maximum temperature"),
//: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, Type: settings, ID: {803cddbf-94c7-4f35-bc7a-18698b03b942})
QT_TRANSLATE_NOOP("mock", "Minimum temperature"),
//: The name of the ActionType ({07cd8d5f-2f65-4955-b1f9-05d7f4da488a}) of ThingClass autoMock
QT_TRANSLATE_NOOP("mock", "Mock Action 1 (with params)"),
@ -426,12 +592,36 @@ const QString translations[] {
//: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {e5c0d14b-c9f1-4aca-a56e-85bfa6977150})
QT_TRANSLATE_NOOP("mock", "Password text"),
//: The name of the ParamType (ThingClass: virtualIoLightMock, ActionType: power, ID: {d1917b3d-1530-4cf9-90f7-263ee88e714b})
QT_TRANSLATE_NOOP("mock", "Power"),
//: The name of the ParamType (ThingClass: virtualIoLightMock, EventType: power, ID: {d1917b3d-1530-4cf9-90f7-263ee88e714b})
QT_TRANSLATE_NOOP("mock", "Power"),
//: The name of the StateType ({d1917b3d-1530-4cf9-90f7-263ee88e714b}) of ThingClass virtualIoLightMock
QT_TRANSLATE_NOOP("mock", "Power"),
//: The name of the EventType ({d1917b3d-1530-4cf9-90f7-263ee88e714b}) of ThingClass virtualIoLightMock
QT_TRANSLATE_NOOP("mock", "Power changed"),
//: The name of the Browser Item ActionType ({da6faef8-2816-430e-93bb-57e8f9582d29}) of ThingClass mock
QT_TRANSLATE_NOOP("mock", "Remove from favorites"),
//: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {22add8c9-ee4f-43ad-8931-58e999313ac3})
QT_TRANSLATE_NOOP("mock", "Search text"),
//: The name of the ActionType ({d6fcdb52-f7c3-423b-b9f5-1e29f164c42e}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Set Digital Output 1"),
//: The name of the ActionType ({35de8b68-0cf3-4850-a27d-cf9c4a26921f}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Set Digital Output 2"),
//: The name of the ActionType ({70cf053e-4abc-4d88-8e1e-2bd9a62256c7}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Set Output Input 1"),
//: The name of the ActionType ({e40bcf7d-47b8-41fa-b213-3652a905b376}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Set Output Input 2"),
//: The name of the ActionType ({a7c11774-f31f-4d64-99d1-e0ae5fb35a5c}) of ThingClass inputTypeMock
QT_TRANSLATE_NOOP("mock", "Set Writable Bool"),
@ -477,6 +667,9 @@ const QString translations[] {
//: The name of the ActionType ({05f63f9c-f61e-4dcf-ad55-3f13fde2765b}) of ThingClass pushButtonMock
QT_TRANSLATE_NOOP("mock", "Set allowed values"),
//: The name of the ActionType ({ac56977c-cbba-47c6-a827-5735d8b0aed6}) of ThingClass genericIoMock
QT_TRANSLATE_NOOP("mock", "Set analog input 1"),
//: The name of the ActionType ({80ba1449-b485-47d4-a067-6bf306e2a568}) of ThingClass childMock
QT_TRANSLATE_NOOP("mock", "Set bool value"),
@ -501,12 +694,18 @@ const QString translations[] {
//: The name of the ActionType ({53cd7c55-49b7-441b-b970-9048f20f0e2c}) of ThingClass pushButtonMock
QT_TRANSLATE_NOOP("mock", "Set double value"),
//: The name of the ActionType ({fd341f72-6d9a-4812-9f66-47197c48a935}) of ThingClass virtualIoTemperatureSensorMock
QT_TRANSLATE_NOOP("mock", "Set input"),
//: The name of the ActionType ({527f0687-0b28-4c26-852c-25b8f83e4797}) of ThingClass displayPinMock
QT_TRANSLATE_NOOP("mock", "Set percentage"),
//: The name of the ActionType ({72981c04-267a-4ba0-a59e-9921d2f3af9c}) of ThingClass pushButtonMock
QT_TRANSLATE_NOOP("mock", "Set percentage"),
//: The name of the ActionType ({d1917b3d-1530-4cf9-90f7-263ee88e714b}) of ThingClass virtualIoLightMock
QT_TRANSLATE_NOOP("mock", "Set power"),
//: The name of the ParamType (ThingClass: mock, Type: settings, ID: {367f7ba4-5039-47be-abd8-59cc8eaf4b9a})
QT_TRANSLATE_NOOP("mock", "Setting 1"),
@ -519,6 +718,15 @@ const QString translations[] {
//: The name of the EventType ({27f69ca9-a321-40ff-bfee-4b0272a671b4}) of ThingClass inputTypeMock
QT_TRANSLATE_NOOP("mock", "String changed"),
//: The name of the ParamType (ThingClass: virtualIoTemperatureSensorMock, EventType: temperature, ID: {db9cc518-1012-47e2-8212-6e616fed07a6})
QT_TRANSLATE_NOOP("mock", "Temperature"),
//: The name of the StateType ({db9cc518-1012-47e2-8212-6e616fed07a6}) of ThingClass virtualIoTemperatureSensorMock
QT_TRANSLATE_NOOP("mock", "Temperature"),
//: The name of the EventType ({db9cc518-1012-47e2-8212-6e616fed07a6}) of ThingClass virtualIoTemperatureSensorMock
QT_TRANSLATE_NOOP("mock", "Temperature changed"),
//: The name of the ParamType (ThingClass: inputTypeMock, Type: thing, ID: {716f0994-bc01-42b0-b64d-59236f7320d2})
QT_TRANSLATE_NOOP("mock", "Text area"),

View File

@ -1,4 +1,4 @@
5.0
5.1
{
"enums": {
"BasicType": [
@ -82,6 +82,13 @@
"DeviceSetupStatusComplete",
"DeviceSetupStatusFailed"
],
"IOType": [
"IOTypeNone",
"IOTypeDigitalInput",
"IOTypeDigitalOutput",
"IOTypeAnalogInput",
"IOTypeAnalogOutput"
],
"InputType": [
"InputTypeNone",
"InputTypeTextLine",
@ -937,6 +944,29 @@
"thingError": "$ref:ThingError"
}
},
"Integrations.ConnectIO": {
"description": "Connect two generic IO states. Input and output need to be compatible, that is, either a digital input and a digital output, or an analog input and an analog output. If successful, the connectionId will be returned.",
"params": {
"inputStateTypeId": "Uuid",
"inputThingId": "Uuid",
"o:inverted": "Bool",
"outputStateTypeId": "Uuid",
"outputThingId": "Uuid"
},
"returns": {
"o:ioConnectionId": "Uuid",
"thingError": "$ref:ThingError"
}
},
"Integrations.DisconnectIO": {
"description": "Disconnect an existing IO connection.",
"params": {
"ioConnectionId": "Uuid"
},
"returns": {
"thingError": "$ref:ThingError"
}
},
"Integrations.DiscoverThings": {
"description": "Performs a thing discovery for things of the given thingClassId and returns the results. This function may take a while to return. Note that this method will include all the found things, that is, including things that may already have been added. Those things will have thingId set to the id of the already added thing. Such results may be used to reconfigure existing things and might be filtered in cases where only unknown things are of interest.",
"params": {
@ -1025,6 +1055,15 @@
"eventTypes": "$ref:EventTypes"
}
},
"Integrations.GetIOConnections": {
"description": "Fetch IO connections. Optionally filtered by thingId and stateTypeId.",
"params": {
"o:thingId": "Uuid"
},
"returns": {
"ioConnections": "$ref:IOConnections"
}
},
"Integrations.GetPluginConfiguration": {
"description": "Get a plugin's params.",
"params": {
@ -2015,6 +2054,18 @@
"event": "$ref:Event"
}
},
"Integrations.IOConnectionAdded": {
"description": "Emitted whenever an IO connection has been added.",
"params": {
"ioConnection": "$ref:IOConnection"
}
},
"Integrations.IOConnectionRemoved": {
"description": "Emitted whenever an IO connection has been removed.",
"params": {
"ioConnectionId": "Uuid"
}
},
"Integrations.PluginConfigurationChanged": {
"description": "Emitted whenever a plugin's configuration is changed.",
"params": {
@ -2402,6 +2453,17 @@
"name": "String",
"version": "String"
},
"IOConnection": {
"r:id": "Uuid",
"r:inputStateTypeId": "Uuid",
"r:inputThingId": "Uuid",
"r:inverted": "Bool",
"r:outputStateTypeId": "Uuid",
"r:outputThingId": "Uuid"
},
"IOConnections": [
"$ref:IOConnection"
],
"IntegrationPlugin": {
"r:displayName": "String",
"r:id": "Uuid",
@ -2586,6 +2648,7 @@
"displayName": "String",
"index": "Int",
"name": "String",
"o:ioType": "$ref:IOType",
"o:maxValue": "Variant",
"o:minValue": "Variant",
"o:possibleValues": [

View File

@ -1,26 +1,27 @@
TEMPLATE = subdirs
SUBDIRS = \
versioning \
devices \
integrations \
jsonrpc \
events \
states \
actions \
rules \
plugins \
webserver \
websocketserver \
configurations \
devices \
events \
integrations \
ioconnections \
jsonrpc \
logging \
loggingdirect \
loggingloading \
#coap \ # temporary removed until fixed
configurations \
mqttbroker \
plugins \
rules \
scripts \
states \
tags \
timemanager \
userloading \
usermanager \
mqttbroker \
tags \
scripts \
versioning \
webserver \
websocketserver \
#coap \ # temporary removed until fixed

View File

@ -287,8 +287,8 @@ void TestIntegrations::getThingClasses_data()
QTest::addColumn<VendorId>("vendorId");
QTest::addColumn<int>("resultCount");
QTest::newRow("vendor nymea") << nymeaVendorId << 11;
QTest::newRow("no filter") << VendorId() << 11;
QTest::newRow("vendor nymea") << nymeaVendorId << 14;
QTest::newRow("no filter") << VendorId() << 14;
QTest::newRow("invalid vendor") << VendorId("93e7d361-8025-4354-b17e-b68406c800bc") << 0;
}

View File

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

View File

@ -0,0 +1,335 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, GNU version 3. This project is distributed in the hope that it
* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "nymeatestbase.h"
#include "nymeacore.h"
#include "nymeasettings.h"
#include "integrations/thingdiscoveryinfo.h"
#include "integrations/thingsetupinfo.h"
#include "servers/mocktcpserver.h"
#include "jsonrpc/integrationshandler.h"
using namespace nymeaserver;
class TestIOConnections : public NymeaTestBase
{
Q_OBJECT
private:
ThingId m_ioThingId;
ThingId m_lightThingId;
ThingId m_tempSensorThingId;
inline void verifyThingError(const QVariant &response, Thing::ThingError error = Thing::ThingErrorNoError) {
verifyError(response, "thingError", enumValueName(error));
}
private slots:
void initTestCase();
void testConnectionCompatibility_data();
void testConnectionCompatibility();
void testDigitalIO_data();
void testDigitalIO();
void testAnalogIO_data();
void testAnalogIO();
};
void TestIOConnections::initTestCase()
{
NymeaTestBase::initTestCase();
QLoggingCategory::setFilterRules("*.debug=false\n"
"Tests.debug=true\n"
"Mock.debug=true\n"
"ThingManager.debug=true\n"
);
// Adding generic IO mock
QVariantMap params;
params.insert("thingClassId", genericIoMockThingClassId);
params.insert("name", "Generic IO mock");
QVariant response = injectAndWait("Integrations.AddThing", params);
m_ioThingId = ThingId(response.toMap().value("params").toMap().value("thingId").toString());
QVERIFY2(!m_ioThingId.isNull(), "Creating generic IO mock failed");
qCDebug(dcTests()) << "Created IO mock with ID" << m_ioThingId;
// Adding virtual light (digital input)
params.clear();
params.insert("thingClassId", virtualIoLightMockThingClassId);
params.insert("name", "light");
response = injectAndWait("Integrations.AddThing", params);
m_lightThingId = ThingId(response.toMap().value("params").toMap().value("thingId").toUuid());
QVERIFY2(!m_lightThingId.isNull(), "Creating virtual light failed");
// Adding virtual temp sensor (analog output)
params.clear();
params.insert("thingClassId", virtualIoTemperatureSensorMockThingClassId);
params.insert("name", "temp sensor");
response = injectAndWait("Integrations.AddThing", params);
m_tempSensorThingId = ThingId(response.toMap().value("params").toMap().value("thingId").toUuid());
QVERIFY2(!m_tempSensorThingId.isNull(), "Creating virtual temp sensor failed");
}
void TestIOConnections::testConnectionCompatibility_data()
{
QTest::addColumn<ThingId>("inputThingId");
QTest::addColumn<StateTypeId>("inputStateTypeId");
QTest::addColumn<ThingId>("outputThingId");
QTest::addColumn<StateTypeId>("outputStateTypeId");
QTest::addColumn<Thing::ThingError>("expectedError");
QTest::newRow("digital in, digital in") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockDigitalInput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("digital in, digital out") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorNoError;
QTest::newRow("digital in, analog in") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockAnalogInput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("digital in, analog out") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("digital out, digital in") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << m_ioThingId << genericIoMockDigitalInput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("digital out, digital out") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("digital out, analog in") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << m_ioThingId << genericIoMockAnalogInput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("digital out, analot out") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("analog in, digital in") << m_ioThingId << genericIoMockAnalogInput1StateTypeId << m_ioThingId << genericIoMockDigitalInput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("analog in, digital out") << m_ioThingId << genericIoMockAnalogInput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("analog in, analog in") << m_ioThingId << genericIoMockAnalogInput1StateTypeId << m_ioThingId << genericIoMockAnalogInput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("analog in, analog out") << m_ioThingId << genericIoMockAnalogInput1StateTypeId << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << Thing::ThingErrorNoError;
QTest::newRow("analog out, digital in") << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << m_ioThingId << genericIoMockDigitalInput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("analog out, digital out") << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("analog out, analog in") << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << m_ioThingId << genericIoMockAnalogInput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("analog out, analog out") << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << m_ioThingId << genericIoMockAnalogOutput1StateTypeId << Thing::ThingErrorInvalidParameter;
QTest::newRow("valid input, invalid output thing") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << ThingId("707d5093-4915-499e-8e69-10c11972bb34") << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorThingNotFound;
QTest::newRow("valid input, invalid output stateType") << m_ioThingId << genericIoMockDigitalInput1StateTypeId << m_ioThingId << StateTypeId("51534cd7-8adf-4bdc-a4c1-042d0a9d4faa") << Thing::ThingErrorStateTypeNotFound;
QTest::newRow("invalid input thing, valid output") << ThingId("99843693-8615-416a-8e59-a47b050f5c1a") << genericIoMockDigitalInput1StateTypeId << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorThingNotFound;
QTest::newRow("invalid input stateType, valid output") << m_ioThingId << StateTypeId("04657948-e349-4f43-bf20-13f9986ad1b4") << m_ioThingId << genericIoMockDigitalOutput1StateTypeId << Thing::ThingErrorStateTypeNotFound;
}
void TestIOConnections::testConnectionCompatibility()
{
QFETCH(ThingId, inputThingId);
QFETCH(StateTypeId, inputStateTypeId);
QFETCH(ThingId, outputThingId);
QFETCH(StateTypeId, outputStateTypeId);
QFETCH(Thing::ThingError, expectedError);
QVariantMap params;
params.insert("inputThingId", inputThingId);
params.insert("inputStateTypeId", inputStateTypeId);
params.insert("outputThingId", outputThingId);
params.insert("outputStateTypeId", outputStateTypeId);
QVariant response = injectAndWait("Integrations.ConnectIO", params);
verifyThingError(response, expectedError);
}
void TestIOConnections::testDigitalIO_data()
{
QTest::addColumn<bool>("inverted");
QTest::newRow("normal") << false;
QTest::newRow("inverted") << true;
}
void TestIOConnections::testDigitalIO()
{
QFETCH(bool, inverted);
QVariantMap params;
params.insert("inputThingId", m_lightThingId);
params.insert("inputStateTypeId", virtualIoLightMockPowerStateTypeId);
params.insert("outputThingId", m_ioThingId);
params.insert("outputStateTypeId", genericIoMockDigitalOutput1StateTypeId);
params.insert("inverted", inverted);
QVariant response = injectAndWait("Integrations.ConnectIO", params);
verifyThingError(response);
IOConnectionId ioConnectionId = response.toMap().value("params").toMap().value("ioConnectionId").toUuid();
// verify input is off
bool expectedValue = false;
params.clear();
params.insert("thingId", m_lightThingId);
params.insert("stateTypeId", virtualIoLightMockPowerStateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == expectedValue, "Light isn't turned off");
// verify output is off (or inverted)
params.clear();
params.insert("thingId", m_ioThingId);
params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == (expectedValue xor inverted), "Digital output isn't turned off");
// Turn on light and verify digital output went on
expectedValue = true;
params.clear();
params.insert("thingId", m_lightThingId);
params.insert("actionTypeId", virtualIoLightMockPowerActionTypeId);
QVariantMap actionParam;
actionParam.insert("paramTypeId", virtualIoLightMockPowerActionPowerParamTypeId);
actionParam.insert("value", true);
params.insert("params", QVariantList() << actionParam);
response = injectAndWait("Integrations.ExecuteAction", params);
verifyThingError(response);
params.clear();
params.insert("thingId", m_ioThingId);
params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == (expectedValue xor inverted), "Digital output isn't turned on");
// Disconnect IO again
params.clear();
params.insert("ioConnectionId", ioConnectionId);
response = injectAndWait("Integrations.DisconnectIO", params);
verifyThingError(response);
// Turn off the light and verify digital output is still on
expectedValue = true;
params.clear();
params.insert("thingId", m_lightThingId);
params.insert("actionTypeId", virtualIoLightMockPowerActionTypeId);
actionParam.clear();
actionParam.insert("paramTypeId", virtualIoLightMockPowerActionPowerParamTypeId);
actionParam.insert("value", false);
params.insert("params", QVariantList() << actionParam);
response = injectAndWait("Integrations.ExecuteAction", params);
verifyThingError(response);
params.clear();
params.insert("thingId", m_ioThingId);
params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == (expectedValue xor inverted), "Digital output turned off while it should not");
}
void TestIOConnections::testAnalogIO_data()
{
QTest::addColumn<bool>("inverted");
QTest::newRow("normal") << false;
QTest::newRow("inverted") << true;
}
void TestIOConnections::testAnalogIO()
{
QFETCH(bool, inverted);
// Set input to 0
QVariantMap params;
params.insert("thingId", m_ioThingId);
params.insert("actionTypeId", genericIoMockAnalogInput1StateTypeId);
QVariantMap actionParam;
actionParam.insert("paramTypeId", genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId);
actionParam.insert("value", 0); // goes from 0 to 3.3
params.insert("params", QVariantList() << actionParam);
QVariant response = injectAndWait("Integrations.ExecuteAction", params);
verifyThingError(response);
// Connect IO to it
params.clear();
params.insert("inputThingId", m_ioThingId);
params.insert("inputStateTypeId", genericIoMockAnalogInput1StateTypeId);
params.insert("outputThingId", m_tempSensorThingId);
params.insert("outputStateTypeId", virtualIoTemperatureSensorMockInputStateTypeId);
params.insert("inverted", inverted);
response = injectAndWait("Integrations.ConnectIO", params);
verifyThingError(response);
IOConnectionId ioConnectionId = response.toMap().value("params").toMap().value("ioConnectionId").toUuid();
// and check temp senser
double expectedTemp = inverted ? 50 : -20;
params.clear();
params.insert("thingId", m_tempSensorThingId);
params.insert("stateTypeId", virtualIoTemperatureSensorMockTemperatureStateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), expectedTemp), QString("Temp sensor is not at %1 but at %2").arg(expectedTemp).arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8());
// set analog input to 0.5 and verify temp aligned
params.clear();
params.insert("thingId", m_ioThingId);
params.insert("actionTypeId", genericIoMockAnalogInput1StateTypeId);
actionParam.clear();
actionParam.insert("paramTypeId", genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId);
actionParam.insert("value", 1.65); // goes from 0 to 3.3
params.insert("params", QVariantList() << actionParam);
response = injectAndWait("Integrations.ExecuteAction", params);
verifyThingError(response);
params.clear();
params.insert("thingId", m_tempSensorThingId);
params.insert("stateTypeId", virtualIoTemperatureSensorMockTemperatureStateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
// generic IO output goes from 0 to 3.3. We're setting 1.65V which 50%
// temp goes from -20 to 50. A input of 1.65 should output a temperature of 15°C
expectedTemp = 70.0 / 2 - 20;
QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), expectedTemp), QString("Temp sensor is not at %1 but at %2").arg(expectedTemp).arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8());
// Disconnect IO again
params.clear();
params.insert("ioConnectionId", ioConnectionId);
response = injectAndWait("Integrations.DisconnectIO", params);
verifyThingError(response);
// set analog input to 3 and verify temp is still at the old expectedTemp
params.clear();
params.insert("thingId", m_ioThingId);
params.insert("actionTypeId", genericIoMockAnalogInput1StateTypeId);
actionParam.clear();
actionParam.insert("paramTypeId", genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId);
actionParam.insert("value", 3); // goes from 0 to 3.3
params.insert("params", QVariantList() << actionParam);
response = injectAndWait("Integrations.ExecuteAction", params);
verifyThingError(response);
params.clear();
params.insert("thingId", m_tempSensorThingId);
params.insert("stateTypeId", virtualIoTemperatureSensorMockTemperatureStateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), expectedTemp), QString("Temp sensor is not at %1 but at %2").arg(expectedTemp).arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8());
}
#include "testioconnections.moc"
QTEST_MAIN(TestIOConnections)