New Plugin: PushNotifications

This plugin allows to send push notifications to nymea:app without the need to
go through nymea:cloud.

This is useful for users that prefer to not have a cloud account at all and
improves privacy and server costs as it cuts out one server hop of the messages.
master
Michael Zanetti 2020-03-18 19:12:31 +01:00
parent 6e781113e5
commit 5ccc29a205
10 changed files with 585 additions and 0 deletions

16
debian/control vendored
View File

@ -638,6 +638,21 @@ Description: nymea.io plugin for Pushbullet
This package will install the nymea.io plugin for sending messages via Pushbullet.
Package: nymea-plugin-pushnotifications
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Description: nymea.io plugin to send Push notifications
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
allows them to interact.
With the powerful rule engine you are able to connect any device available
in the system and create individual scenes and behaviors for your environment.
.
This package will install the nymea.io plugin for sending messages via GCM, APNs and UBPorts
Package: nymea-plugin-solarlog
Architecture: any
Depends: ${shlibs:Depends},
@ -1071,6 +1086,7 @@ Depends: nymea-plugin-anel,
nymea-plugin-openweathermap,
nymea-plugin-philipshue,
nymea-plugin-pushbullet,
nymea-plugin-pushnotifications,
nymea-plugin-wakeonlan,
nymea-plugin-tasmota,
nymea-plugin-tplink,

View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginpushnotifications.so

View File

@ -42,6 +42,7 @@ PLUGIN_DIRS = \
osdomotics \
philipshue \
pushbullet \
pushnotifications \
shelly \
solarlog \
systemmonitor \

View File

@ -0,0 +1,31 @@
# Push Notifications
This plugin allows to send push notifications to mobile devices.
## Supported platforms
* Android (using Firebase Cloud Messaging)
* iOS (using Firebase Cloud Messaging)
* Ubuntu/UBPorts Phone
## Requirements
A push notification device token is required during setup. For Android *and* iOS, a Firebase
token is required. Note that native iOS push tokens are not supported as the Apple Push Notification
Service (APNs) does not allow sending messages in such a distributed manner, however, Firebase is available
for iOS too. On Ubuntu, the UBPorts push services are used.
## More
During setup, the token, the push service system and a client id needs to be provided. The token is normally
obtained by the operating system. The push service should be selected from GCM, APNs or UBPorts. The client id
must a unique per client and as persistent as possible.
> Note: Even when using APNs, the token must be obtained using the Firebase SDK as plain APNs does not support sending push notifications from a distributed setup like nymea, but always requires a centralized server on the internet handling all messages.
As it is impossible for an end user to obtain this token, a client app should prefill all the parameters
when setting up a push notification thing. Normally, push tokens expire after a while (for instance when
the user clears app data, when the operating system decides to cycle tokens etc). In this case, the client
app should reconfigure its own push notification thing, updating the token with the new one. The client ID
parameter schould be used to find the appropriate thing to reconfigure.

View File

@ -0,0 +1,256 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginpushnotifications.h"
#include "plugininfo.h"
#include "network/networkaccessmanager.h"
#include "nymeasettings.h"
#include <QJsonDocument>
// Example payload for Firebase + GCM
//{
// "android": {
// "notification": {
// "sound": "default"
// },
// "priority": "high"
// },
// "data": {
// "body": "text",
// "title": "title"
// },
// "to": "<client token>"
//}
// Example payload for Firebase + APNs
//{
// "apns": {
// "headers": {
// "apns-priority": "10"
// }
// },
// "notification": {
// "body": "text",
// "sound": "default",
// "title": "title"
// },
// "to": "<client token>"
//}
IntegrationPluginPushNotifications::IntegrationPluginPushNotifications(QObject* parent): IntegrationPlugin (parent)
{
}
IntegrationPluginPushNotifications::~IntegrationPluginPushNotifications()
{
}
void IntegrationPluginPushNotifications::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
QString token = thing->paramValue(pushNotificationsThingTokenParamTypeId).toString();
QString pushService = thing->paramValue(pushNotificationsThingServiceParamTypeId).toString();
QString clientId = thing->paramValue(pushNotificationsThingClientIdParamTypeId).toString();
qCDebug(dcPushNotifications()) << "Setting up push notifications" << thing->name() << "(" << clientId << ") for service" << pushService << "with token" << (token.mid(0, 5) + "******");
if (token.isEmpty()) {
//: Error setting up thing
info->finish(Thing::ThingErrorMissingParameter, QT_TR_NOOP("The token must not be empty."));
return;
}
QStringList availablePushServices = {"FB-GCM", "FB-APNs", "UBPorts"};
if (!availablePushServices.contains(pushService)) {
//: Error setting up thing
info->finish(Thing::ThingErrorMissingParameter, QT_TR_NOOP("The push service must not be empty."));
return;
}
// In case of Firebase, check if we have the required API key
if (pushService.startsWith("FB") && apiKeyStorage()->requestKey("firebase").data("apiKey").isEmpty()) {
//: Error setting up thing
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Firebase server API key not installed."));
return;
}
info->finish(Thing::ThingErrorNoError);
}
void IntegrationPluginPushNotifications::executeAction(ThingActionInfo *info)
{
Thing *thing = info->thing();
Action action = info->action();
qCDebug(dcPushNotifications()) << "Executing action" << action.actionTypeId() << "for" << thing->name() << thing->id().toString();
QString token = thing->paramValue(pushNotificationsThingTokenParamTypeId).toString();
QString pushService = thing->paramValue(pushNotificationsThingServiceParamTypeId).toString();
QString title = action.param(pushNotificationsNotifyActionTitleParamTypeId).value().toString();
QString body = action.param(pushNotificationsNotifyActionBodyParamTypeId).value().toString();
if (token.isEmpty()) {
return info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Push notifications need to be reconfigured."));
}
QNetworkRequest request;
QVariantMap payload;
if (pushService.startsWith("FB")) {
ApiKey apiKey = apiKeyStorage()->requestKey("firebase");
if (apiKey.data("apiKey").isEmpty()) {
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Firebase server API key not installed."));
return;
}
request = QNetworkRequest(QUrl("https://fcm.googleapis.com/fcm/send"));
request.setRawHeader("Authorization", "key=" + apiKey.data("apiKey"));
request.setRawHeader("Content-Type", "application/json");
payload.insert("to", token.toUtf8().trimmed());
QVariantMap notification;
notification.insert("title", title);
notification.insert("body", body);
if (pushService == "FB-GCM") {
QVariantMap soundMap;
soundMap.insert("sound", "default");
QVariantMap android;
android.insert("priority", "high");
android.insert("notification", soundMap);
payload.insert("android", android);
payload.insert("data", notification);
} else if (pushService == "FB-APNs") {
notification.insert("sound", "default");
QVariantMap headers;
headers.insert("apns-priority", "10");
QVariantMap apns;
apns.insert("headers", headers);
payload.insert("notification", notification);
payload.insert("apns", apns);
}
} else if (pushService == "UBPorts") {
request = QNetworkRequest(QUrl("https://push.ubports.com/notify"));
request.setRawHeader("Content-Type", "application/json");
QVariantMap card;
card.insert("icon", "notification");
card.insert("summary", title);
card.insert("body", body);
card.insert("popup", true);
card.insert("persist", true);
QVariantMap notification;
notification.insert("card", card);
notification.insert("vibrate", true);
notification.insert("sound", true);
QVariantMap data;
data.insert("notification", notification);
payload.insert("data", data);
payload.insert("appid", "io.guh.nymeaapp_nymea-app");
payload.insert("expire_on", QDateTime::currentDateTime().toUTC().addMSecs(1000 * 60 * 10).toString(Qt::ISODate));
payload.insert("token", token.toUtf8().trimmed());
}
qCDebug(dcPushNotifications()) << "Sending notification" << request.url().toString() << qUtf8Printable(QJsonDocument::fromVariant(payload).toJson());
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(payload).toJson(QJsonDocument::Compact));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [reply, pushService, info, this]{
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcPushNotifications()) << "Push message sending failed for" << info->thing()->name() << info->thing()->id() << reply->errorString() << reply->error();
emit info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcPushNotifications()) << "Error reading reply from server for" << info->thing()->name() << info->thing()->id().toString() << error.errorString();
qCWarning(dcPushNotifications()) << qUtf8Printable(data);
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
QVariantMap replyMap = jsonDoc.toVariant().toMap();
// qDebug(dcPushNotifications) << qUtf8Printable(jsonDoc.toJson());
if (pushService == "FB-GCM" || pushService == "FB-APNs") {
if (replyMap.value("success").toInt() != 1) {
// While GCM seems rock solid, APNs fails rather often with Internal Server Error.
// According to Firebase support this is "expected" and one should retry with a exponential back-off timer.
// As we only have 30 secs until the info times out, let's try repeatedly until the info object dies.
// In my tests, so far it succeeded every time on the second attempt.
// https://stackoverflow.com/questions/63382257/firebase-messaging-fails-sporadically-with-internal-error
if (replyMap.value("results").toList().count() > 0 && replyMap.value("results").toList().first().toMap().value("error").toString() == "InternalServerError") {
qCDebug(dcPushNotifications()) << "Sending push message failed. Retrying...";
executeAction(info);
return;
}
// On any other error, bail out...
qCWarning(dcPushNotifications()) << "Error sending push notification:" << qUtf8Printable(jsonDoc.toJson());
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
} else if (pushService == "UBPorts") {
if (!replyMap.value("ok").toBool()) {
qCWarning(dcPushNotifications()) << "Error sending push notification:" << qUtf8Printable(jsonDoc.toJson());
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
}
qCDebug(dcPushNotifications()) << "Message sent successfully";
info->finish(Thing::ThingErrorNoError);
});
}

View File

@ -0,0 +1,55 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 INTEGRATIONPLUGINPUSHNOTIFICATIONS_H
#define INTEGRATIONPLUGINPUSHNOTIFICATIONS_H
#include "integrations/integrationplugin.h"
class IntegrationPluginPushNotifications: public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginpushnotifications.json")
Q_INTERFACES(IntegrationPlugin)
public:
explicit IntegrationPluginPushNotifications(QObject *parent = nullptr);
~IntegrationPluginPushNotifications() override;
void setupThing(ThingSetupInfo *info) override;
void executeAction(ThingActionInfo *info) override;
private:
QHash<ThingClassId, ParamTypeId> m_tokenParamTypeIds;
QByteArray m_firebaseServerToken;
};
#endif

View File

@ -0,0 +1,68 @@
{
"displayName": "Push Notifications",
"name": "pushNotifications",
"id": "fdcea3b3-8d79-4fbb-8101-1ab45d71cedb",
"apiKeys": [ "firebase" ],
"vendors": [
{
"displayName": "nymea GmbH",
"name": "nymea",
"id": "2062d64d-3232-433c-88bc-0d33c0ba2ba6",
"thingClasses": [
{
"id": "f0dd4c03-0aca-42cc-8f34-9902457b05de",
"name": "pushNotifications",
"displayName": "Push Notifications",
"createMethods": ["user"],
"interfaces": ["notifications"],
"paramTypes": [
{
"id": "3cb8e30e-2ec5-4b4b-8c8c-03eaf7876839",
"name": "service",
"displayName": "Push service",
"type": "QString",
"allowedValues": ["FB-GCM", "FB-APNs", "UBPorts"]
},
{
"id": "12ec06b2-44e7-486a-9169-31c684b91c8f",
"name": "token",
"displayName": "access token",
"type": "QString"
},
{
"id": "d76da367-64e3-4b7d-aa84-c96b3acfb65e",
"name": "clientId",
"displayName": "Client ID",
"type": "QString"
}
],
"actionTypes": [
{
"id": "ed9a0196-6c24-4e05-9cbc-c6834de38005",
"name": "notify",
"displayName": "notify",
"paramTypes": [
{
"id": "3b5e195c-9f96-4f2f-a654-f17ed77c02ee",
"name": "title",
"displayName": "title",
"type": "QString",
"inputType": "TextLine",
"defaultValue": ""
},
{
"id": "074c4c7c-037f-45ac-ab2f-f7aea3f84bde",
"name": "body",
"displayName": "body",
"type": "QString",
"inputType": "TextArea",
"defaultValue": ""
}
]
}
]
}
]
}
]
}

View File

@ -0,0 +1,13 @@
include(../plugins.pri)
TARGET = $$qtLibraryTarget(nymea_integrationpluginpushnotifications)
QT+= network
SOURCES += \
integrationpluginpushnotifications.cpp
HEADERS += \
integrationpluginpushnotifications.h

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginPushNotifications</name>
<message>
<location filename="../integrationpluginpushnotifications.cpp" line="71"/>
<source>The token must not be empty.</source>
<extracomment>Error setting up thing</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginpushnotifications.cpp" line="76"/>
<source>The push service must not be empty.</source>
<extracomment>Error setting up thing</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginpushnotifications.cpp" line="96"/>
<source>Push notifications need to be reconfigured.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>pushNotifications</name>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="28"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="31"/>
<source>Push Notifications</source>
<extracomment>The name of the ThingClass ({f0dd4c03-0aca-42cc-8f34-9902457b05de})
----------
The name of the plugin pushNotifications ({fdcea3b3-8d79-4fbb-8101-1ab45d71cedb})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="34"/>
<source>Push service</source>
<extracomment>The name of the ParamType (ThingClass: pushNotifications, Type: thing, ID: {3cb8e30e-2ec5-4b4b-8c8c-03eaf7876839})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="37"/>
<source>access token</source>
<extracomment>The name of the ParamType (ThingClass: pushNotifications, Type: thing, ID: {12ec06b2-44e7-486a-9169-31c684b91c8f})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="40"/>
<source>body</source>
<extracomment>The name of the ParamType (ThingClass: pushNotifications, ActionType: notify, ID: {074c4c7c-037f-45ac-ab2f-f7aea3f84bde})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="43"/>
<source>notify</source>
<extracomment>The name of the ActionType ({ed9a0196-6c24-4e05-9cbc-c6834de38005}) of ThingClass pushNotifications</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="46"/>
<source>nymea GmbH</source>
<extracomment>The name of the vendor ({2062d64d-3232-433c-88bc-0d33c0ba2ba6})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="49"/>
<source>title</source>
<extracomment>The name of the ParamType (ThingClass: pushNotifications, ActionType: notify, ID: {3b5e195c-9f96-4f2f-a654-f17ed77c02ee})</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>IntegrationPluginPushNotifications</name>
<message>
<location filename="../integrationpluginpushnotifications.cpp" line="71"/>
<source>The token must not be empty.</source>
<extracomment>Error setting up thing</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginpushnotifications.cpp" line="76"/>
<source>The push service must not be empty.</source>
<extracomment>Error setting up thing</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginpushnotifications.cpp" line="96"/>
<source>Push notifications need to be reconfigured.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>pushNotifications</name>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="28"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="31"/>
<source>Push Notifications</source>
<extracomment>The name of the ThingClass ({f0dd4c03-0aca-42cc-8f34-9902457b05de})
----------
The name of the plugin pushNotifications ({fdcea3b3-8d79-4fbb-8101-1ab45d71cedb})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="34"/>
<source>Push service</source>
<extracomment>The name of the ParamType (ThingClass: pushNotifications, Type: thing, ID: {3cb8e30e-2ec5-4b4b-8c8c-03eaf7876839})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="37"/>
<source>access token</source>
<extracomment>The name of the ParamType (ThingClass: pushNotifications, Type: thing, ID: {12ec06b2-44e7-486a-9169-31c684b91c8f})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="40"/>
<source>body</source>
<extracomment>The name of the ParamType (ThingClass: pushNotifications, ActionType: notify, ID: {074c4c7c-037f-45ac-ab2f-f7aea3f84bde})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="43"/>
<source>notify</source>
<extracomment>The name of the ActionType ({ed9a0196-6c24-4e05-9cbc-c6834de38005}) of ThingClass pushNotifications</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="46"/>
<source>nymea GmbH</source>
<extracomment>The name of the vendor ({2062d64d-3232-433c-88bc-0d33c0ba2ba6})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/pushnotifications/plugininfo.h" line="49"/>
<source>title</source>
<extracomment>The name of the ParamType (ThingClass: pushNotifications, ActionType: notify, ID: {3b5e195c-9f96-4f2f-a654-f17ed77c02ee})</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
</TS>