Allow more granular notification subscriptions in API

pull/222/head
Michael Zanetti 2019-10-15 14:18:40 +02:00
parent 4b890429fa
commit 9b1b6d86e6
16 changed files with 135 additions and 63 deletions

View File

@ -116,9 +116,18 @@ JsonRPCServer::JsonRPCServer(const QSslConfiguration &sslConfiguration, QObject
setReturns("Version", returns);
params.clear(); returns.clear();
setDescription("SetNotificationStatus", "Enable/Disable notifications for this connections.");
params.insert("enabled", JsonTypes::basicTypeToString(JsonTypes::Bool));
setDescription("SetNotificationStatus", "Enable/Disable notifications for this connections. Either \"enabled\" or or """
"\"namespaces\" needs to be given but not both of them. The boolean based "
"\"enabled\" parameter will enable/disable all notifications at once. If "
"instead the list-based \"namespaces\" parameter is provided, all given namespaces"
"will be enabled, the others will be disabled. The return value of \"success\" will "
"indicate success of the operation. The \"enabled\" property in the return value is "
"deprecated and used for legacy compatibilty only. It will be set to true if at least "
"one namespace has been enabled.");
params.insert("o:enabled", JsonTypes::basicTypeToString(JsonTypes::Bool));
params.insert("o:namespaces", QVariantList() << QStringLiteral("$ref:Namespace"));
setParams("SetNotificationStatus", params);
returns.insert("namespaces", QVariantList() << QStringLiteral("$ref:Namespace"));
returns.insert("enabled", JsonTypes::basicTypeToString(JsonTypes::Bool));
setReturns("SetNotificationStatus", returns);
@ -261,8 +270,18 @@ JsonReply* JsonRPCServer::Introspect(const QVariantMap &params) const
{
Q_UNUSED(params)
// We need to add dynamic stuff ourselves
QVariantMap allTypes = JsonTypes::allTypes();
QStringList namespaces;
foreach (const QString &namespaceString, m_handlers.keys()) {
namespaces.append(namespaceString);
}
// We need to sort them to have a predictable ordering
std::sort(namespaces.begin(), namespaces.end());
allTypes.insert("Namespace", namespaces);
QVariantMap data;
data.insert("types", JsonTypes::allTypes());
data.insert("types", allTypes);
QVariantMap methods;
foreach (JsonHandler *handler, m_handlers)
methods.unite(handler->introspect(QMetaMethod::Method));
@ -291,10 +310,27 @@ JsonReply* JsonRPCServer::Version(const QVariantMap &params) const
JsonReply* JsonRPCServer::SetNotificationStatus(const QVariantMap &params)
{
QUuid clientId = this->property("clientId").toUuid();
Q_ASSERT_X(m_clientNotifications.contains(clientId), "JsonRPCServer", "SetNotificationStatus for an unknown client called");
m_clientNotifications[clientId] = params.value("enabled").toBool();
Q_ASSERT_X(m_clientTransports.contains(clientId), "JsonRPCServer", "Invalid client ID.");
QStringList enabledNamespaces;
foreach (const QString &namespaceName, m_handlers.keys()) {
if (params.contains("enabled")) {
if (params.value("enabled").toBool()) {
enabledNamespaces.append(namespaceName);
}
} else {
if (params.value("namespaces").toList().contains(namespaceName)) {
enabledNamespaces.append(namespaceName);
}
}
}
qCDebug(dcJsonRpc()) << "Notification settings for client" << clientId << ":" << enabledNamespaces;
m_clientNotifications[clientId] = enabledNamespaces;
QVariantMap returns;
returns.insert("enabled", m_clientNotifications[clientId]);
returns.insert("namespaces", m_clientNotifications[clientId]);
// legacy, deprecated
returns.insert("enabled", m_clientNotifications[clientId].count() > 0);
return createReply(returns);
}
@ -690,8 +726,10 @@ void JsonRPCServer::sendNotification(const QVariantMap &params)
qCDebug(dcJsonRpc()) << "Sending notification:" << handler->name() + "." + method.name();
qCDebug(dcJsonRpcTraffic()) << "Notification content:" << data;
foreach (const QUuid &clientId, m_clientNotifications.keys(true)) {
m_clientTransports.value(clientId)->sendData(clientId, data);
foreach (const QUuid &clientId, m_clientNotifications.keys()) {
if (m_clientNotifications.value(clientId).contains(handler->name())) {
m_clientTransports.value(clientId)->sendData(clientId, data);
}
}
}
@ -784,9 +822,6 @@ void JsonRPCServer::clientConnected(const QUuid &clientId)
m_clientTransports.insert(clientId, interface);
// If authentication is required, notifications are disabled by default. Clients must enable them with a valid token
m_clientNotifications.insert(clientId, !interface->configuration().authenticationEnabled);
// Initialize the connection locale to the settings default
m_clientLocales.insert(clientId, NymeaCore::instance()->configuration()->locale());

View File

@ -104,7 +104,7 @@ private:
QHash<QUuid, TransportInterface*> m_clientTransports;
QHash<QUuid, QByteArray> m_clientBuffers;
QHash<QUuid, bool> m_clientNotifications;
QHash<QUuid, QStringList> m_clientNotifications;
QHash<QUuid, QLocale> m_clientLocales;
QHash<int, QUuid> m_pushButtonTransactions;
QHash<QUuid, QTimer*> m_newConnectionWaitTimers;

View File

@ -2305,6 +2305,11 @@ QPair<bool, QString> JsonTypes::validateVariant(const QVariant &templateVariant,
qCWarning(dcJsonRpc()) << QString("Value %1 not allowed in %2").arg(variant.toString()).arg(mediaBrowserIconRef());
return result;
}
} else if (refName == "$ref:Namespace") {
// This is quite hacky, but unless we explicitly propagate the namespace info in here we can't know.
// Let's assume jsonrpcserver handles this properly...
// Actually this entire jsontypes file should probably be split up into the handlers themselves but that's a different story.
return qMakePair<bool, QString>(true, QString());
} else {
Q_ASSERT_X(false, "JsonTypes", QString("Unhandled ref: %1").arg(refName).toLatin1().data());
return report(false, QString("Unhandled ref %1. Server implementation incomplete.").arg(refName));

View File

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

View File

@ -1,4 +1,4 @@
3.0
3.1
{
"methods": {
"Actions.ExecuteAction": {
@ -625,12 +625,18 @@
}
},
"JSONRPC.SetNotificationStatus": {
"description": "Enable/Disable notifications for this connections.",
"description": "Enable/Disable notifications for this connections. Either \"enabled\" or or \"namespaces\" needs to be given but not both of them. The boolean based \"enabled\" parameter will enable/disable all notifications at once. If instead the list-based \"namespaces\" parameter is provided, all given namespaceswill be enabled, the others will be disabled. The return value of \"success\" will indicate success of the operation. The \"enabled\" property in the return value is deprecated and used for legacy compatibilty only. It will be set to true if at least one namespace has been enabled.",
"params": {
"enabled": "Bool"
"o:enabled": "Bool",
"o:namespaces": [
"$ref:Namespace"
]
},
"returns": {
"enabled": "Bool"
"enabled": "Bool",
"namespaces": [
"$ref:Namespace"
]
}
},
"JSONRPC.SetupCloudConnection": {
@ -1655,6 +1661,19 @@
"password": "String",
"username": "String"
},
"Namespace": [
"Actions",
"Configuration",
"Devices",
"Events",
"JSONRPC",
"Logging",
"NetworkManager",
"Rules",
"States",
"System",
"Tags"
],
"NetworkDeviceState": [
"NetworkDeviceStateUnknown",
"NetworkDeviceStateUnmanaged",

View File

@ -78,7 +78,7 @@ void TestConfigurations::getConfigurations()
void TestConfigurations::testTimeZones()
{
enableNotifications();
enableNotifications({"Configuration"});
QVariantMap params; QVariant response; QVariantMap configurations; QVariantList configurationChangedNotifications;
@ -173,7 +173,7 @@ void TestConfigurations::testTimeZones()
void TestConfigurations::testServerName()
{
enableNotifications();
enableNotifications({"Configuration"});
// Get current configurations
QVariantMap basicConfigurationMap = loadBasicConfiguration();
@ -234,7 +234,7 @@ void TestConfigurations::testServerName()
void TestConfigurations::testLanguages()
{
enableNotifications();
enableNotifications({"Configuration"});
// Get current configurations
QVariantMap basicConfigurationMap = loadBasicConfiguration();
@ -287,7 +287,7 @@ void TestConfigurations::testLanguages()
// Restart the server and check if the language will be loaded correctly
restartServer();
enableNotifications();
enableNotifications({"Configuration"});
// Get configuration
@ -310,7 +310,7 @@ void TestConfigurations::testLanguages()
void TestConfigurations::testDebugServerConfiguration()
{
enableNotifications();
enableNotifications({"Configuration"});
// Get current configurations
QVariantMap basicConfigurationMap = loadBasicConfiguration();

View File

@ -46,8 +46,8 @@ private slots:
void introspect();
void enableDisableNotifications_data();
void enableDisableNotifications();
void enableDisableNotifications_legacy_data();
void enableDisableNotifications_legacy();
void deviceAddedRemovedNotifications();
void ruleAddedRemovedNotifications();
@ -621,7 +621,7 @@ void TestJSONRPC::introspect()
}
}
void TestJSONRPC::enableDisableNotifications_data()
void TestJSONRPC::enableDisableNotifications_legacy_data()
{
QTest::addColumn<QString>("enabled");
@ -629,7 +629,7 @@ void TestJSONRPC::enableDisableNotifications_data()
QTest::newRow("disabled") << "false";
}
void TestJSONRPC::enableDisableNotifications()
void TestJSONRPC::enableDisableNotifications_legacy()
{
QFETCH(QString, enabled);
@ -638,12 +638,27 @@ void TestJSONRPC::enableDisableNotifications()
QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", params);
QCOMPARE(response.toMap().value("params").toMap().value("enabled").toString(), enabled);
qCDebug(dcTests()) << "Enabled notifications:" << response.toMap().value("params").toMap().value("namespaces").toList();
QStringList expectedNamespaces;
if (enabled == "true") {
expectedNamespaces << "Actions" << "NetworkManager" << "Devices" << "System" << "Rules" << "States" << "Logging" << "Tags" << "JSONRPC" << "Configuration" << "Events";
}
std::sort(expectedNamespaces.begin(), expectedNamespaces.end());
QStringList actualNamespaces;
foreach (const QVariant &ns, response.toMap().value("params").toMap().value("namespaces").toList()) {
actualNamespaces << ns.toString();
}
std::sort(actualNamespaces.begin(), actualNamespaces.end());
QCOMPARE(expectedNamespaces, actualNamespaces);
}
void TestJSONRPC::deviceAddedRemovedNotifications()
{
// enable notificartions
QCOMPARE(enableNotifications(), true);
enableNotifications({"Devices"});
// Setup connection to mock client
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
@ -689,8 +704,7 @@ void TestJSONRPC::deviceAddedRemovedNotifications()
void TestJSONRPC::ruleAddedRemovedNotifications()
{
// enable notificartions
QCOMPARE(enableNotifications(), true);
enableNotifications({"Rules"});
// Setup connection to mock client
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
@ -934,7 +948,7 @@ void TestJSONRPC::deviceChangedNotifications()
void TestJSONRPC::stateChangeEmitsNotifications()
{
QCOMPARE(enableNotifications(), true);
enableNotifications({"Devices"});
bool found = false;
// Setup connection to mock client

View File

@ -217,8 +217,7 @@ void TestLogging::eventLogs()
QVERIFY2(devices.count() > 0, "There needs to be at least one configured Mock Device for this test");
Device *device = devices.first();
// enable notifications
QCOMPARE(enableNotifications(), true);
enableNotifications({"Events"});
// Setup connection to mock client
QNetworkAccessManager nam;
@ -295,8 +294,7 @@ void TestLogging::actionLog()
params.insert("deviceId", m_mockDeviceId);
params.insert("params", actionParams);
// enable notifications
QCOMPARE(enableNotifications(), true);
enableNotifications({"Logging"});
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
@ -472,7 +470,7 @@ void TestLogging::deviceLogs()
void TestLogging::testDoubleValues()
{
QCOMPARE(enableNotifications(), true);
enableNotifications({"Logging"});
// Add display pin device which contains a double value
@ -666,7 +664,7 @@ void TestLogging::testLimits()
void TestLogging::removeDevice()
{
// enable notifications
QCOMPARE(enableNotifications(), true);
enableNotifications({"Logging"});
// get this logentry with filter
QVariantMap params;

View File

@ -58,7 +58,7 @@ void TestMqttBroker::initTestCase()
void TestMqttBroker::testServerConfigurationAPI()
{
// Set up notifications spy
enableNotifications();
enableNotifications({"Configuration"});
QSignalSpy notificationsSpy(m_mockTcpServer, &MockTcpServer::outgoingData);
// Get server instances
@ -168,7 +168,7 @@ void TestMqttBroker::testServerConfigurationAPI()
void TestMqttBroker::testPolicyConfigurationAPI()
{
// Set up notifications spy
enableNotifications();
enableNotifications({"Configuration"});
QSignalSpy notificationsSpy(m_mockTcpServer, &MockTcpServer::outgoingData);
// Get polcies

View File

@ -142,8 +142,7 @@ void TestRestLogging::eventLogs()
QVERIFY2(devices.count() > 0, "There needs to be at least one configured Mock Device for this test");
Device *device = devices.first();
// enable notifications
QCOMPARE(enableNotifications(), true);
enableNotifications({"Logging"});
// Setup connection to mock client
QNetworkAccessManager nam;
@ -216,7 +215,7 @@ void TestRestLogging::actionLog()
params.insert("params", actionParams);
// enable notifications
QCOMPARE(enableNotifications(), true);
enableNotifications({"Logging"});
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
@ -341,7 +340,7 @@ void TestRestLogging::actionLog()
void TestRestLogging::removeDevice()
{
// enable notifications
QCOMPARE(enableNotifications(), true);
enableNotifications({"Logging"});
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));

View File

@ -890,7 +890,7 @@ void TestRules::editRules()
verifyRuleError(response);
// enable notifications
QCOMPARE(enableNotifications(), true);
enableNotifications({"Rules"});
// now create the new rule and edit the original one
params.clear();
@ -1856,7 +1856,7 @@ void TestRules::testChildEvaluator_data()
DeviceId testDeviceId = addDisplayPinDevice();
QVERIFY2(!testDeviceId.isNull(), "Could not add push button device for child evaluators");
enableNotifications();
enableNotifications({"Rules"});
// Create child evaluators
// Action

View File

@ -89,8 +89,7 @@ void TestTags::addTag()
QFETCH(QString, value);
QFETCH(TagsStorage::TagError, expectedError);
// enable notificartions
QCOMPARE(enableNotifications(), true);
enableNotifications({"Tags"});
// Setup connection to mock client
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
@ -124,8 +123,7 @@ void TestTags::addTag()
void TestTags::updateTagValue()
{
// enable notificartions
QCOMPARE(enableNotifications(), true);
enableNotifications({"Tags"});
// Setup connection to mock client
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));
@ -180,8 +178,7 @@ void TestTags::updateTagValue()
void TestTags::removeTag()
{
// enable notificartions
QCOMPARE(enableNotifications(), true);
enableNotifications({"Tags"});
// Setup connection to mock client
QSignalSpy clientSpy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray)));

View File

@ -2011,7 +2011,7 @@ void TestTimeManager::initTimeManager()
{
cleanupMockHistory();
removeAllRules();
enableNotifications();
enableNotifications({"Rules"});
NymeaCore::instance()->timeManager()->stopTimer();
qDebug() << NymeaCore::instance()->timeManager()->currentTime().toString();
qDebug() << NymeaCore::instance()->timeManager()->currentDate().toString();

View File

@ -3,5 +3,10 @@
if [ -z $1 ]; then
echo "usage: $0 host"
else
(echo '{"id":1, "method": "JSONRPC.Introspect"}'; sleep 1) | nc $1 2222
cat <<EOD | nc $1 2222
{"id":1, "method": "JSONRPC.Hello"}
{"id":2, "method": "JSONRPC.Introspect"}
EOD
fi

View File

@ -202,7 +202,7 @@ QVariantList NymeaTestBase::checkNotifications(const QSignalSpy &spy, const QStr
QVariant NymeaTestBase::getAndWait(const QNetworkRequest &request, const int &expectedStatus)
{
QNetworkAccessManager nam;
connect(&nam, &QNetworkAccessManager::sslErrors, [this, &nam](QNetworkReply *reply, const QList<QSslError> &) {
connect(&nam, &QNetworkAccessManager::sslErrors, [&nam](QNetworkReply *reply, const QList<QSslError> &) {
reply->ignoreSslErrors();
});
QSignalSpy clientSpy(&nam, SIGNAL(finished(QNetworkReply*)));
@ -363,16 +363,16 @@ void NymeaTestBase::verifyReply(QNetworkReply *reply, const QByteArray &data, co
// }
}
bool NymeaTestBase::enableNotifications()
void NymeaTestBase::enableNotifications(const QStringList &namespaces)
{
QVariantMap notificationParams;
notificationParams.insert("enabled", true);
QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", notificationParams);
if (response.toMap().value("params").toMap().value("enabled").toBool() != true) {
return false;
QVariantList variantList;
foreach (const QString &ns, namespaces) {
variantList << ns;
}
qDebug() << "Notifications enabled.";
return true;
QVariantMap notificationParams;
notificationParams.insert("namespaces", variantList);
QVariant response = injectAndWait("JSONRPC.SetNotificationStatus", notificationParams);
QCOMPARE(response.toMap().value("params").toMap().value("namespaces").toList(), variantList);
}
bool NymeaTestBase::disableNotifications()

View File

@ -59,7 +59,7 @@ protected:
void verifyReply(QNetworkReply *reply, const QByteArray &data, const int &expectedStatus);
bool enableNotifications();
void enableNotifications(const QStringList &namespaces);
bool disableNotifications();
inline void verifyError(const QVariant &response, const QString &fieldName, const QString &error)