diff --git a/debian/control b/debian/control
index 87f28ea3..5a20fe66 100644
--- a/debian/control
+++ b/debian/control
@@ -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,
diff --git a/debian/nymea-plugin-pushnotifications.install.in b/debian/nymea-plugin-pushnotifications.install.in
new file mode 100644
index 00000000..514c1c06
--- /dev/null
+++ b/debian/nymea-plugin-pushnotifications.install.in
@@ -0,0 +1 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginpushnotifications.so
diff --git a/nymea-plugins.pro b/nymea-plugins.pro
index 5605b2b5..8df8b2d7 100644
--- a/nymea-plugins.pro
+++ b/nymea-plugins.pro
@@ -42,6 +42,7 @@ PLUGIN_DIRS = \
osdomotics \
philipshue \
pushbullet \
+ pushnotifications \
shelly \
solarlog \
systemmonitor \
diff --git a/pushnotifications/README.md b/pushnotifications/README.md
new file mode 100644
index 00000000..b5a6d620
--- /dev/null
+++ b/pushnotifications/README.md
@@ -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.
diff --git a/pushnotifications/integrationpluginpushnotifications.cpp b/pushnotifications/integrationpluginpushnotifications.cpp
new file mode 100644
index 00000000..27779fcb
--- /dev/null
+++ b/pushnotifications/integrationpluginpushnotifications.cpp
@@ -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 .
+*
+* 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
+
+// Example payload for Firebase + GCM
+//{
+// "android": {
+// "notification": {
+// "sound": "default"
+// },
+// "priority": "high"
+// },
+// "data": {
+// "body": "text",
+// "title": "title"
+// },
+// "to": ""
+//}
+
+
+// Example payload for Firebase + APNs
+//{
+// "apns": {
+// "headers": {
+// "apns-priority": "10"
+// }
+// },
+// "notification": {
+// "body": "text",
+// "sound": "default",
+// "title": "title"
+// },
+// "to": ""
+//}
+
+
+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);
+ });
+}
+
diff --git a/pushnotifications/integrationpluginpushnotifications.h b/pushnotifications/integrationpluginpushnotifications.h
new file mode 100644
index 00000000..76285663
--- /dev/null
+++ b/pushnotifications/integrationpluginpushnotifications.h
@@ -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 .
+*
+* 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 m_tokenParamTypeIds;
+ QByteArray m_firebaseServerToken;
+};
+
+#endif
diff --git a/pushnotifications/integrationpluginpushnotifications.json b/pushnotifications/integrationpluginpushnotifications.json
new file mode 100644
index 00000000..ef1b916a
--- /dev/null
+++ b/pushnotifications/integrationpluginpushnotifications.json
@@ -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": ""
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/pushnotifications/pushnotifications.pro b/pushnotifications/pushnotifications.pro
new file mode 100644
index 00000000..ac1ed1f7
--- /dev/null
+++ b/pushnotifications/pushnotifications.pro
@@ -0,0 +1,13 @@
+include(../plugins.pri)
+
+TARGET = $$qtLibraryTarget(nymea_integrationpluginpushnotifications)
+
+QT+= network
+
+SOURCES += \
+ integrationpluginpushnotifications.cpp
+
+HEADERS += \
+ integrationpluginpushnotifications.h
+
+
diff --git a/pushnotifications/translations/fdcea3b3-8d79-4fbb-8101-1ab45d71cedb-de.ts b/pushnotifications/translations/fdcea3b3-8d79-4fbb-8101-1ab45d71cedb-de.ts
new file mode 100644
index 00000000..405fd946
--- /dev/null
+++ b/pushnotifications/translations/fdcea3b3-8d79-4fbb-8101-1ab45d71cedb-de.ts
@@ -0,0 +1,72 @@
+
+
+
+
+ IntegrationPluginPushNotifications
+
+
+ The token must not be empty.
+ Error setting up thing
+
+
+
+
+ The push service must not be empty.
+ Error setting up thing
+
+
+
+
+ Push notifications need to be reconfigured.
+
+
+
+
+ pushNotifications
+
+
+
+ Push Notifications
+ The name of the ThingClass ({f0dd4c03-0aca-42cc-8f34-9902457b05de})
+----------
+The name of the plugin pushNotifications ({fdcea3b3-8d79-4fbb-8101-1ab45d71cedb})
+
+
+
+
+ Push service
+ The name of the ParamType (ThingClass: pushNotifications, Type: thing, ID: {3cb8e30e-2ec5-4b4b-8c8c-03eaf7876839})
+
+
+
+
+ access token
+ The name of the ParamType (ThingClass: pushNotifications, Type: thing, ID: {12ec06b2-44e7-486a-9169-31c684b91c8f})
+
+
+
+
+ body
+ The name of the ParamType (ThingClass: pushNotifications, ActionType: notify, ID: {074c4c7c-037f-45ac-ab2f-f7aea3f84bde})
+
+
+
+
+ notify
+ The name of the ActionType ({ed9a0196-6c24-4e05-9cbc-c6834de38005}) of ThingClass pushNotifications
+
+
+
+
+ nymea GmbH
+ The name of the vendor ({2062d64d-3232-433c-88bc-0d33c0ba2ba6})
+
+
+
+
+ title
+ The name of the ParamType (ThingClass: pushNotifications, ActionType: notify, ID: {3b5e195c-9f96-4f2f-a654-f17ed77c02ee})
+
+
+
+
diff --git a/pushnotifications/translations/fdcea3b3-8d79-4fbb-8101-1ab45d71cedb-en_US.ts b/pushnotifications/translations/fdcea3b3-8d79-4fbb-8101-1ab45d71cedb-en_US.ts
new file mode 100644
index 00000000..405fd946
--- /dev/null
+++ b/pushnotifications/translations/fdcea3b3-8d79-4fbb-8101-1ab45d71cedb-en_US.ts
@@ -0,0 +1,72 @@
+
+
+
+
+ IntegrationPluginPushNotifications
+
+
+ The token must not be empty.
+ Error setting up thing
+
+
+
+
+ The push service must not be empty.
+ Error setting up thing
+
+
+
+
+ Push notifications need to be reconfigured.
+
+
+
+
+ pushNotifications
+
+
+
+ Push Notifications
+ The name of the ThingClass ({f0dd4c03-0aca-42cc-8f34-9902457b05de})
+----------
+The name of the plugin pushNotifications ({fdcea3b3-8d79-4fbb-8101-1ab45d71cedb})
+
+
+
+
+ Push service
+ The name of the ParamType (ThingClass: pushNotifications, Type: thing, ID: {3cb8e30e-2ec5-4b4b-8c8c-03eaf7876839})
+
+
+
+
+ access token
+ The name of the ParamType (ThingClass: pushNotifications, Type: thing, ID: {12ec06b2-44e7-486a-9169-31c684b91c8f})
+
+
+
+
+ body
+ The name of the ParamType (ThingClass: pushNotifications, ActionType: notify, ID: {074c4c7c-037f-45ac-ab2f-f7aea3f84bde})
+
+
+
+
+ notify
+ The name of the ActionType ({ed9a0196-6c24-4e05-9cbc-c6834de38005}) of ThingClass pushNotifications
+
+
+
+
+ nymea GmbH
+ The name of the vendor ({2062d64d-3232-433c-88bc-0d33c0ba2ba6})
+
+
+
+
+ title
+ The name of the ParamType (ThingClass: pushNotifications, ActionType: notify, ID: {3b5e195c-9f96-4f2f-a654-f17ed77c02ee})
+
+
+
+