Add support for inverting the connections

pull/282/head
Michael Zanetti 2020-05-09 15:14:11 +02:00
parent 4e509d75f8
commit 322bcf56a6
12 changed files with 112 additions and 41 deletions

View File

@ -1005,6 +1005,11 @@ IOConnectionResult ThingManagerImplementation::connectIO(const IOConnection &con
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;
@ -1736,9 +1741,18 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
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!";
@ -1749,7 +1763,7 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
qCWarning(dcThingManager()) << "Plugin not found for IO connection's output action.";
continue;
}
StateType inputStateType = thing->thingClass().getStateType(stateTypeId);
StateType inputStateType = inputThing->thingClass().getStateType(stateTypeId);
StateType outputStateType = outputThing->thingClass().getStateType(ioConnection.outputStateTypeId());
if (outputStateType.id().isNull()) {
@ -1759,7 +1773,7 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
QVariant outputValue;
if (outputStateType.ioType() == Types::IOTypeDigitalOutput) {
// Digital IOs are mapped as-is
outputValue = value;
outputValue = ioConnection.inverted() xor inputValue.toBool();
// We're already in sync! Skipping action.
if (outputThing->stateValue(outputStateType.id()) == outputValue) {
@ -1767,10 +1781,10 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
}
} else {
// Analog IOs are mapped within the according min/max ranges
outputValue = mapValue(value, inputStateType, outputStateType);
outputValue = mapValue(inputValue, inputStateType, outputStateType, ioConnection.inverted());
// We're already in sync (fuzzy, good enough)! Skipping action.
if (qFuzzyCompare(outputThing->stateValue(outputStateType.id()).toDouble(), outputValue.toDouble())) {
if (qFuzzyCompare(1.0 + outputThing->stateValue(outputStateType.id()).toDouble(), 1.0 + outputValue.toDouble())) {
continue;
}
}
@ -1785,9 +1799,9 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
// 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) {
thing->setStateValue(inputStateType.id(), outputThing->stateValue(outputStateType.id()));
inputThing->setStateValue(inputStateType.id(), outputThing->stateValue(outputStateType.id()));
} else {
thing->setStateValue(inputStateType.id(), mapValue(outputThing->stateValue(outputStateType.id()), outputStateType, inputStateType));
inputThing->setStateValue(inputStateType.id(), mapValue(outputThing->stateValue(outputStateType.id()), outputStateType, inputStateType, ioConnection.inverted()));
}
}
});
@ -1795,6 +1809,9 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
// 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!";
@ -1805,7 +1822,7 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
qCWarning(dcThingManager()) << "Plugin not found for IO connection's input action.";
continue;
}
StateType outputStateType = thing->thingClass().getStateType(stateTypeId);
StateType outputStateType = outputThing->thingClass().getStateType(stateTypeId);
StateType inputStateType = inputThing->thingClass().getStateType(ioConnection.inputStateTypeId());
if (inputStateType.id().isNull()) {
@ -1821,7 +1838,7 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
QVariant inputValue;
if (inputStateType.ioType() == Types::IOTypeDigitalInput) {
// Digital IOs are mapped as-is
inputValue = value;
inputValue = ioConnection.inverted() xor outputValue.toBool();
// Prevent looping
if (inputThing->stateValue(inputStateType.id()) == inputValue) {
@ -1829,10 +1846,10 @@ void ThingManagerImplementation::slotThingStateValueChanged(const StateTypeId &s
}
} else {
// Analog IOs are mapped within the according min/max ranges
inputValue = mapValue(value, outputStateType, inputStateType);
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(inputThing->stateValue(inputStateType.id()).toDouble(), inputValue.toDouble())) {
if (qFuzzyCompare(1.0 + inputThing->stateValue(inputStateType.id()).toDouble(), 1.0 + inputValue.toDouble())) {
continue;
}
}
@ -2000,6 +2017,7 @@ void ThingManagerImplementation::storeIOConnections()
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();
}
@ -2017,14 +2035,15 @@ void ThingManagerImplementation::loadIOConnections()
StateTypeId inputStateTypeId = connectionSettings.value("inputStateTypeId").toUuid();
ThingId outputThingId = connectionSettings.value("outputThingId").toUuid();
StateTypeId outputStateTypeId = connectionSettings.value("outputStateTypeId").toUuid();
IOConnection ioConnection(id, inputThingId, inputStateTypeId, outputThingId, outputStateTypeId);
bool inverted = connectionSettings.value("inverted").toBool();
IOConnection ioConnection(id, inputThingId, inputStateTypeId, outputThingId, outputStateTypeId, inverted);
m_ioConnections.insert(id, ioConnection);
connectionSettings.endGroup();
}
connectionSettings.endGroup();
}
QVariant ThingManagerImplementation::mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType) const
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();
@ -2032,6 +2051,7 @@ QVariant ThingManagerImplementation::mapValue(const QVariant &value, const State
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;
}

View File

@ -157,7 +157,8 @@ private:
void storeIOConnections();
void loadIOConnections();
QVariant mapValue(const QVariant &value, const StateType &fromStateType, const StateType &toStateType) const;
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;

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

@ -361,6 +361,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa
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);
@ -992,7 +993,8 @@ JsonReply *IntegrationsHandler::ConnectIO(const QVariantMap &params)
StateTypeId inputStateTypeId = params.value("inputStateTypeId").toUuid();
ThingId outputThingId = params.value("outputThingId").toUuid();
StateTypeId outputStateTypeId = params.value("outputStateTypeId").toUuid();
IOConnectionResult result = m_thingManager->connectIO(inputThingId, inputStateTypeId, outputThingId, outputStateTypeId);
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);

View File

@ -5,12 +5,13 @@ IOConnection::IOConnection()
}
IOConnection::IOConnection(const IOConnectionId &id, const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState):
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_outputStateTypeId(outputState),
m_inverted(inverted)
{
}
@ -40,6 +41,11 @@ StateTypeId IOConnection::outputStateTypeId() const
return m_outputStateTypeId;
}
bool IOConnection::inverted() const
{
return m_inverted;
}
QVariant IOConnections::get(int index) const
{
return QVariant::fromValue(at(index));

View File

@ -21,10 +21,11 @@ class IOConnection
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);
IOConnection(const IOConnectionId &id, const ThingId &inputThingId, const StateTypeId &inputStateTypeId, const ThingId &outputThingId, const StateTypeId &outputStateTypeId, bool inverted = false);
IOConnectionId id() const;
@ -34,12 +35,15 @@ public:
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>

View File

@ -51,8 +51,8 @@ ThingManager::ThingManager(QObject *parent) : QObject(parent)
qRegisterMetaType<ParamTypes>();
}
IOConnectionResult ThingManager::connectIO(const ThingId &inputThing, const StateTypeId &inputState, const ThingId &outputThing, const StateTypeId &outputState)
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);
IOConnection connection(IOConnectionId::createIOConnectionId(), inputThing, inputState, outputThing, outputState, inverted);
return connectIO(connection);
}

View File

@ -91,7 +91,7 @@ public:
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);
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;

View File

@ -749,11 +749,11 @@ void IntegrationPluginMock::executeAction(ThingActionInfo *info)
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 value = info->action().param(virtualIoTemperatureSensorMockInputActionInputParamTypeId).value().toDouble();
double temp = minTemp + (maxTemp - minTemp) * value;
qCDebug(dcMock()) << "Min:" << minTemp << "Max:" << maxTemp << "value:" << value << "temp:" << temp;
info->thing()->setStateValue(virtualIoTemperatureSensorMockTemperatureStateTypeId, temp);
info->finish(Thing::ThingErrorNoError);
return;

View File

@ -1035,7 +1035,7 @@
"displayNameEvent": "Temperature changed",
"type": "double",
"unit": "DegreeCelsius",
"defaultValue": 0
"defaultValue": -20
}
]
}

View File

@ -949,6 +949,7 @@
"params": {
"inputStateTypeId": "Uuid",
"inputThingId": "Uuid",
"o:inverted": "Bool",
"outputStateTypeId": "Uuid",
"outputThingId": "Uuid"
},
@ -2456,6 +2457,7 @@
"r:id": "Uuid",
"r:inputStateTypeId": "Uuid",
"r:inputThingId": "Uuid",
"r:inverted": "Bool",
"r:outputStateTypeId": "Uuid",
"r:outputThingId": "Uuid"
},

View File

@ -60,8 +60,10 @@ private slots:
void testConnectionCompatibility_data();
void testConnectionCompatibility();
void testDigitalIO_data();
void testDigitalIO();
void testAnalogIO_data();
void testAnalogIO();
};
@ -71,6 +73,7 @@ void TestIOConnections::initTestCase()
QLoggingCategory::setFilterRules("*.debug=false\n"
"Tests.debug=true\n"
"Mock.debug=true\n"
"ThingManager.debug=true\n"
);
// Adding generic IO mock
@ -151,33 +154,47 @@ void TestIOConnections::testConnectionCompatibility()
}
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 both, input and out are off
// 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() == false, "Light isn't turned off");
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() == false, "Digital output isn't turned off");
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);
@ -193,7 +210,7 @@ void TestIOConnections::testDigitalIO()
params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == true, "Digital output isn't turned on");
QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == (expectedValue xor inverted), "Digital output isn't turned on");
// Disconnect IO again
params.clear();
@ -202,6 +219,7 @@ void TestIOConnections::testDigitalIO()
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);
@ -217,41 +235,59 @@ void TestIOConnections::testDigitalIO()
params.insert("stateTypeId", genericIoMockDigitalOutput1StateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
QVERIFY2(response.toMap().value("params").toMap().value("value").toBool() == true, "Digital output turned off while it should not");
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);
QVariant response = injectAndWait("Integrations.ConnectIO", params);
params.insert("inverted", inverted);
response = injectAndWait("Integrations.ConnectIO", params);
verifyThingError(response);
IOConnectionId ioConnectionId = response.toMap().value("params").toMap().value("ioConnectionId").toUuid();
// verify input is at 0, temp at 0 too
params.clear();
params.insert("thingId", m_ioThingId);
params.insert("stateTypeId", genericIoMockAnalogInput1StateTypeId);
response = injectAndWait("Integrations.GetStateValue", params);
verifyThingError(response);
QVERIFY2(qFuzzyCompare(response.toMap().value("params").toMap().value("value").toDouble(), 0), "Input isn't at 0");
// 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(), 0), QString("Temp sensor is not at 0 but at %1").arg(response.toMap().value("params").toMap().value("value").toDouble()).toUtf8());
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);
QVariantMap actionParam;
actionParam.clear();
actionParam.insert("paramTypeId", genericIoMockAnalogInput1ActionAnalogInput1ParamTypeId);
actionParam.insert("value", 1.65); // goes from 0 to 3.3
params.insert("params", QVariantList() << actionParam);
@ -265,7 +301,7 @@ void TestIOConnections::testAnalogIO()
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
double expectedTemp = 70.0 / 2 - 20;
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