]> git.basschouten.com Git - openhab-addons.git/commitdiff
[pushsafer] Initial Contribution of Pushsafer Binding (#10790)
authorChristoph Weitkamp <github@christophweitkamp.de>
Sun, 11 Jul 2021 19:45:23 +0000 (21:45 +0200)
committerGitHub <noreply@github.com>
Sun, 11 Jul 2021 19:45:23 +0000 (21:45 +0200)
* Pushsafer binding

Signed-off-by: Pushsafer.com (Kevin Siml) <info@appzer.de>
* Improvements and comments from code review

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
* Incorporated comments from review

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
Co-authored-by: Pushsafer.com (Kevin Siml) <info@appzer.de>
25 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.pushsafer/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/README.md [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/PushsaferBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/actions/PushsaferActions.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/config/PushsaferAccountConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/config/PushsaferConfigOptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferAPIConnection.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferCommunicationException.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferConfigurationException.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferMessageBuilder.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/dto/Icon.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/dto/Sound.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/factory/PushsaferHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/handler/PushsaferAccountHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/i18n/pushsafer.properties [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/i18n/pushsafer_de.properties [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.pushsafer/src/test/java/org/openhab/binding/pushsafer/internal/actions/PushsaferActionsTest.java [new file with mode: 0644]
bundles/pom.xml

index 300354bcf8e56a98829d81fbd6489339cb344b7c..d51cdc1161627c33f523da1e2c1cc49c7fc7fd15 100644 (file)
 /bundles/org.openhab.binding.pulseaudio/ @peuter
 /bundles/org.openhab.binding.pushbullet/ @hakan42
 /bundles/org.openhab.binding.pushover/ @cweitkamp
+/bundles/org.openhab.binding.pushsafer/ @appzer @cweitkamp
 /bundles/org.openhab.binding.qbus/ @QbusKoen
 /bundles/org.openhab.binding.radiothermostat/ @mlobstein
 /bundles/org.openhab.binding.regoheatpump/ @crnjan
index 4c2c4ac9928c3a27f035cb65e1d517a7def37902..549382d56624d16a46e9458634bf512526102a6b 100644 (file)
       <artifactId>org.openhab.binding.pushover</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.pushsafer</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.radiothermostat</artifactId>
diff --git a/bundles/org.openhab.binding.pushsafer/NOTICE b/bundles/org.openhab.binding.pushsafer/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.pushsafer/README.md b/bundles/org.openhab.binding.pushsafer/README.md
new file mode 100644 (file)
index 0000000..220287f
--- /dev/null
@@ -0,0 +1,54 @@
+# Pushsafer Binding
+
+The Pushsafer binding allows you to notify mobile devices of a message using the [Pushsafer API](https://www.pushsafer.com/pushapi).
+To get started you first need to register (a free process) to get a Private Key.
+Initially you have to register a device with one of the [client apps](https://www.pushsafer.com/apps), to get a device id.
+
+## Supported Things
+
+There is only one Thing available - the `pushsafer-account`.
+You are able to create multiple instances of this Thing to broadcast to different devices or groups with push-notification content and setting.
+
+## Thing Configuration
+
+| Configuration Parameter | Type    | Description                                                                                                                                           |
+|-------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `apikey`                | text    | Your private-key to access the Pushsafer [Message API](https://www.pushsafer.com/pushapi). **mandatory**                                              |
+| `user`                  | text    | Your username or email address to validate against the Pushsafer Message API. **mandatory**                                                           |
+| `device`                | text    | Your device or group id to which device(s) you want to push notifications. **mandatory**                                                              |
+| `title`                 | text    | The default title of a message (default: `"openHAB"`).                                                                                                |
+| `format`                | text    | The default format (`"none"`, `"HTML"` or `"monospace"`) of a message (default: `none`).                                                              |
+| `sound`                 | text    | The default notification sound on target device (default: `1`) (see [supported notification sounds](https://www.pushsafer.com/pushapi#api-sound)).    |
+| `vibration`             | text    | How often the device should vibrate. empty=device default or a number 1-3.                                                                            |
+| `icon`                  | text    | The default notification icon on target device (default: `1`) (see [supported notification icons](https://www.pushsafer.com/pushapi#api-icon)).       |
+| `color`                 | text    | The color (hexadecimal) of notification icon (e.g. #FF0000).                                                                                          |
+| `url`                   | text    | URL or [URL Scheme](https://www.pushsafer.com/url_schemes) send with notification.                                                                    |
+| `urlTitle`              | text    | Title of URL.                                                                                                                                         |
+| `retry`                 | integer | The retry parameter specifies how often (in seconds) the Pushsafer servers will send the same notification to the user (default: `300`). **advanced** |
+| `expire`                | integer | The expire parameter specifies how long (in seconds) your notification will continue to be retried (default: `3600`). **advanced**                    |
+| `confirm`               | integer | Integer 10-10800 (10s steps) Time in seconds after which a message should be sent again before it is confirmed. (default: `0`). **advanced**          |
+| `time2live`             | integer | Time in minutes, after a message automatically gets purged (default: `0`). **advanced**                                                               |
+| `answer`                | integer | 1 = enables reply to push notifications (default: `0`). **advanced**                                                                                  |
+
+The `retry` and `expire` parameters are only used for emergency-priority notifications.
+
+## Channels
+
+Currently the binding does not support any Channels.
+
+## Thing Actions
+
+All actions return a `Boolean` value to indicate if the message was sent successfully or not.
+The parameter `message` is **mandatory**, the `title` parameter defaults to whatever value you defined in the `title` related configuration parameter.
+
+- `sendPushsaferMessage(String message, @Nullable String title)` - This method is used to send a plain text message.
+
+- `sendPushsaferHtmlMessage(String message, @Nullable String title)` - This method is used to send a HTML message.
+
+- `sendPushsaferMonospaceMessage(String message, @Nullable String title)` - This method is used to send a monospace message.
+
+- `sendPushsaferAttachmentMessage(String message, @Nullable String title, String attachment, @Nullable String contentType, @Nullable String authentication)` - This method is used to send a message with an image attachment. It takes a local path or url to the image attachment (parameter `attachment` **mandatory**), an optional `contentType` to define the content-type of the attachment (default: `"jpeg"`, possible values: `"jpeg"`, `"png"`, `"gif"`) and an optional `authentication` for the given URL to define the authentication if needed (default: `""`, example: `"user:password"`).
+
+- `sendPushsaferURLMessage(String message, @Nullable String title, String url, @Nullable String urlTitle)` - This method is used to send a message with an URL. A supplementary `url` to show with the message and a `urlTitle` for the URL, otherwise just the URL is shown.
+
+- `sendPushsaferPriorityMessage(String message, @Nullable String title, @Nullable Integer priority)` - This method is used to send a priority message. Parameter `priority` is the priority (`-2`, `-1`, `0`, `1`, `2`) to be used (default: `2`).
diff --git a/bundles/org.openhab.binding.pushsafer/pom.xml b/bundles/org.openhab.binding.pushsafer/pom.xml
new file mode 100644 (file)
index 0000000..971114e
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.pushsafer</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Pushsafer Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/feature/feature.xml b/bundles/org.openhab.binding.pushsafer/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..185aef5
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.pushsafer-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-pushsafer" description="Pushsafer Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.pushsafer/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/PushsaferBindingConstants.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/PushsaferBindingConstants.java
new file mode 100644 (file)
index 0000000..a498406
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link PushsaferBindingConstants} class defines common constants, which are used across the whole binding.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@NonNullByDefault
+public class PushsaferBindingConstants {
+
+    private static final String BINDING_ID = "pushsafer";
+
+    public static final ThingTypeUID PUSHSAFER_ACCOUNT = new ThingTypeUID(BINDING_ID, "pushsafer-account");
+
+    public static final String CONFIG_SOUND = "sound";
+    public static final String CONFIG_ICON = "icon";
+
+    public static final String ALL_DEVICES = "a";
+    public static final String DEFAULT_SOUND = "";
+    public static final String DEFAULT_ICON = "1";
+    public static final String DEFAULT_COLOR = "";
+    public static final String DEFAULT_URL = "";
+    public static final String DEFAULT_URLTITLE = "";
+    public static final String DEFAULT_VIBRATION = "1";
+    public static final int DEFAULT_CONFIRM = 0;
+    public static final boolean DEFAULT_ANSWER = false;
+    public static final int DEFAULT_TIME2LIVE = 0;
+    public static final String DEFAULT_TITLE = "openHAB";
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/actions/PushsaferActions.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/actions/PushsaferActions.java
new file mode 100644 (file)
index 0000000..5ed7045
--- /dev/null
@@ -0,0 +1,234 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.actions;
+
+import static org.openhab.binding.pushsafer.internal.PushsaferBindingConstants.DEFAULT_TITLE;
+import static org.openhab.binding.pushsafer.internal.connection.PushsaferMessageBuilder.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pushsafer.internal.connection.PushsaferConfigurationException;
+import org.openhab.binding.pushsafer.internal.connection.PushsaferMessageBuilder;
+import org.openhab.binding.pushsafer.internal.handler.PushsaferAccountHandler;
+import org.openhab.core.automation.annotation.ActionInput;
+import org.openhab.core.automation.annotation.ActionOutput;
+import org.openhab.core.automation.annotation.RuleAction;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingActionsScope;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Some automation actions to be used with a {@link PushsaferAccountHandler}.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@ThingActionsScope(name = "pushsafer")
+@NonNullByDefault
+public class PushsaferActions implements ThingActions {
+
+    private static final String DEFAULT_EMERGENCY_PRIORITY = "2";
+
+    private final Logger logger = LoggerFactory.getLogger(PushsaferActions.class);
+
+    private @NonNullByDefault({}) PushsaferAccountHandler accountHandler;
+
+    @RuleAction(label = "@text/sendPushsaferMessageActionLabel", description = "@text/sendPushsaferMessageActionDescription")
+    public @ActionOutput(name = "sent", label = "@text/sendPushsaferMessageActionOutputLabel", description = "@text/sendPushsaferMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendPushsaferMessage(
+            @ActionInput(name = "message", label = "@text/sendPushsaferMessageActionInputMessageLabel", description = "@text/sendPushsaferMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+            @ActionInput(name = "title", label = "@text/sendPushsaferMessageActionInputTitleLabel", description = "@text/sendPushsaferMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title) {
+        logger.trace("ThingAction 'sendPushsaferMessage' called with value(s): message='{}', title='{}'", message,
+                title);
+        return send(getDefaultPushsaferMessageBuilder(message), title);
+    }
+
+    public static Boolean sendPushsaferMessage(ThingActions actions, String message, @Nullable String title) {
+        return ((PushsaferActions) actions).sendPushsaferMessage(message, title);
+    }
+
+    @RuleAction(label = "@text/sendPushsaferURLMessageActionLabel", description = "@text/sendPushsaferURLMessageActionDescription")
+    public @ActionOutput(name = "sent", label = "@text/sendPushsaferMessageActionOutputLabel", description = "@text/sendPushsaferMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendPushsaferURLMessage(
+            @ActionInput(name = "message", label = "@text/sendPushsaferMessageActionInputMessageLabel", description = "@text/sendPushsaferMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+            @ActionInput(name = "title", label = "@text/sendPushsaferMessageActionInputTitleLabel", description = "@text/sendPushsaferMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title,
+            @ActionInput(name = "url", label = "@text/sendPushsaferMessageActionInputURLLabel", description = "@text/sendPushsaferMessageActionInputURLDescription", type = "java.lang.String", required = true) String url,
+            @ActionInput(name = "urlTitle", label = "@text/sendPushsaferMessageActionInputURLTitleLabel", description = "@text/sendPushsaferMessageActionInputURLTitleDescription", type = "java.lang.String") @Nullable String urlTitle) {
+        logger.trace(
+                "ThingAction 'sendPushsaferURLMessage' called with value(s): message='{}', url='{}', title='{}', urlTitle='{}'",
+                message, url, title, urlTitle);
+        if (url == null) {
+            throw new IllegalArgumentException("Skip sending message as 'url' is null.");
+        }
+
+        PushsaferMessageBuilder builder = getDefaultPushsaferMessageBuilder(message).withUrl(url);
+        if (urlTitle != null) {
+            builder.withUrl(urlTitle);
+        }
+        return send(builder, title);
+    }
+
+    public static Boolean sendPushsaferURLMessage(ThingActions actions, String message, @Nullable String title,
+            String url, @Nullable String urlTitle) {
+        return ((PushsaferActions) actions).sendPushsaferURLMessage(message, title, url, urlTitle);
+    }
+
+    @RuleAction(label = "@text/sendHTMLMessageActionLabel", description = "@text/sendHTMLMessageActionDescription")
+    public @ActionOutput(name = "sent", label = "@text/sendPushsaferMessageActionOutputLabel", description = "@text/sendPushsaferMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendPushsaferHtmlMessage(
+            @ActionInput(name = "message", label = "@text/sendPushsaferMessageActionInputMessageLabel", description = "@text/sendPushsaferMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+            @ActionInput(name = "title", label = "@text/sendPushsaferMessageActionInputTitleLabel", description = "@text/sendPushsaferMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title) {
+        logger.trace("ThingAction 'sendPushsaferHtmlMessage' called with value(s): message='{}', title='{}'", message,
+                title);
+        return send(getDefaultPushsaferMessageBuilder(message).withHtmlFormatting(), title);
+    }
+
+    public static Boolean sendPushsaferHtmlMessage(ThingActions actions, String message, @Nullable String title) {
+        return ((PushsaferActions) actions).sendPushsaferHtmlMessage(message, title);
+    }
+
+    @RuleAction(label = "@text/sendPushsaferMonospaceMessageActionLabel", description = "@text/sendPushsaferMonospaceMessageActionDescription")
+    public @ActionOutput(name = "sent", label = "@text/sendPushsaferMessageActionOutputLabel", description = "@text/sendPushsaferMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendPushsaferMonospaceMessage(
+            @ActionInput(name = "message", label = "@text/sendPushsaferMessageActionInputMessageLabel", description = "@text/sendPushsaferMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+            @ActionInput(name = "title", label = "@text/sendPushsaferMessageActionInputTitleLabel", description = "@text/sendPushsaferMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title) {
+        logger.trace("ThingAction 'sendPushsaferMonospaceMessage' called with value(s): message='{}', title='{}'",
+                message, title);
+        return send(getDefaultPushsaferMessageBuilder(message).withMonospaceFormatting(), title);
+    }
+
+    public static Boolean sendPushsaferMonospaceMessage(ThingActions actions, String message, @Nullable String title) {
+        return ((PushsaferActions) actions).sendPushsaferMonospaceMessage(message, title);
+    }
+
+    @RuleAction(label = "@text/sendPushsaferAttachmentMessageActionLabel", description = "@text/sendPushsaferAttachmentMessageActionDescription")
+    public @ActionOutput(name = "sent", label = "@text/sendPushsaferMessageActionOutputLabel", description = "@text/sendPushsaferMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendPushsaferAttachmentMessage(
+            @ActionInput(name = "message", label = "@text/sendPushsaferMessageActionInputMessageLabel", description = "@text/sendPushsaferMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+            @ActionInput(name = "title", label = "@text/sendPushsaferMessageActionInputTitleLabel", description = "@text/sendPushsaferMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title,
+            @ActionInput(name = "attachment", label = "@text/sendPushsaferMessageActionInputAttachmentLabel", description = "@text/sendPushsaferMessageActionInputAttachmentDescription", type = "java.lang.String", required = true) String attachment,
+            @ActionInput(name = "contentType", label = "@text/sendPushsaferMessageActionInputContentTypeLabel", description = "@text/sendPushsaferMessageActionInputContentTypeDescription", type = "java.lang.String", defaultValue = DEFAULT_CONTENT_TYPE) @Nullable String contentType,
+            @ActionInput(name = "authentication", label = "@text/sendPushsaferMessageActionInputAuthenticationLabel", description = "@text/sendPushsaferMessageActionInputAuthenticationDescription", type = "java.lang.String", defaultValue = DEFAULT_AUTH) @Nullable String authentication) {
+        logger.trace(
+                "ThingAction 'sendPushsaferAttachmentMessage' called with value(s): message='{}', title='{}', attachment='{}', contentType='{}', authentication='{}'",
+                message, title, attachment, contentType, authentication);
+        if (attachment == null) {
+            throw new IllegalArgumentException("Skip sending message as 'attachment' is null.");
+        }
+
+        PushsaferMessageBuilder builder = getDefaultPushsaferMessageBuilder(message).withAttachment(attachment);
+        if (contentType != null) {
+            builder.withContentType(contentType);
+        }
+        if (authentication != null) {
+            builder.withAuthentication(authentication);
+        }
+        return send(builder, title);
+    }
+
+    public static Boolean sendPushsaferAttachmentMessage(ThingActions actions, String message, @Nullable String title,
+            String attachment, @Nullable String contentType, @Nullable String authentication) {
+        return ((PushsaferActions) actions).sendPushsaferAttachmentMessage(message, title, attachment, contentType,
+                authentication);
+    }
+
+    @RuleAction(label = "@text/sendPushsaferPriorityMessageActionLabel", description = "@text/sendPushsaferPriorityMessageActionDescription")
+    public @ActionOutput(name = "receipt", label = "@text/sendPushsaferPriorityMessageActionOutputLabel", description = "@text/sendPushsaferPriorityMessageActionOutputDescription", type = "java.lang.String") String sendPushsaferPriorityMessage(
+            @ActionInput(name = "message", label = "@text/sendPushsaferMessageActionInputMessageLabel", description = "@text/sendPushsaferMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+            @ActionInput(name = "title", label = "@text/sendPushsaferMessageActionInputTitleLabel", description = "@text/sendPushsaferMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title,
+            @ActionInput(name = "priority", label = "@text/sendPushsaferMessageActionInputPriorityLabel", description = "@text/sendPushsaferMessageActionInputPriorityDescription", type = "java.lang.Integer", defaultValue = DEFAULT_EMERGENCY_PRIORITY) @Nullable Integer priority) {
+        logger.trace(
+                "ThingAction 'sendPushsaferPriorityMessage' called with value(s): message='{}', title='{}', priority='{}'",
+                message, title, priority);
+        PushsaferMessageBuilder builder = getDefaultPushsaferMessageBuilder(message)
+                .withPriority(priority == null ? EMERGENCY_PRIORITY : priority.intValue());
+
+        if (title != null) {
+            builder.withTitle(title);
+        }
+        return accountHandler.sendPushsaferPriorityMessage(builder);
+    }
+
+    public static String sendPushsaferPriorityMessage(ThingActions actions, String message, @Nullable String title,
+            @Nullable Integer priority) {
+        return ((PushsaferActions) actions).sendPushsaferPriorityMessage(message, title, priority);
+    }
+
+    @RuleAction(label = "@text/cancelPushsaferPriorityMessageActionLabel", description = "@text/cancelPushsaferPriorityMessageActionDescription")
+    public @ActionOutput(name = "canceled", label = "@text/cancelPushsaferPriorityMessageActionOutputLabel", description = "@text/cancelPushsaferPriorityMessageActionOutputDescription", type = "java.lang.Boolean") Boolean cancelPushsaferPriorityMessage(
+            @ActionInput(name = "receipt", label = "@text/cancelPushsaferPriorityMessageActionInputReceiptLabel", description = "@text/cancelPushsaferPriorityMessageActionInputReceiptDescription", type = "java.lang.String", required = true) String receipt) {
+        logger.trace("ThingAction 'cancelPushsaferPriorityMessage' called with value(s): '{}'", receipt);
+        if (accountHandler == null) {
+            throw new RuntimeException("PushsaferAccountHandler is null!");
+        }
+
+        if (receipt == null) {
+            throw new IllegalArgumentException("Skip sending message as 'receipt' is null.");
+        }
+
+        return accountHandler.cancelPushsaferPriorityMessage(receipt);
+    }
+
+    public static Boolean cancelPushsaferPriorityMessage(ThingActions actions, String receipt) {
+        return ((PushsaferActions) actions).cancelPushsaferPriorityMessage(receipt);
+    }
+
+    @RuleAction(label = "@text/sendPushsaferMessageToDeviceActionLabel", description = "@text/sendPushsaferMessageToDeviceActionDescription")
+    public @ActionOutput(name = "sent", label = "@text/sendPushsaferMessageActionOutputLabel", description = "@text/sendPushsaferMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendPushsaferMessageToDevice(
+            @ActionInput(name = "device", label = "@text/sendPushsaferMessageActionInputDeviceLabel", description = "@text/sendPushsaferMessageActionInputDeviceDescription", type = "java.lang.String", required = true) String device,
+            @ActionInput(name = "message", label = "@text/sendPushsaferMessageActionInputMessageLabel", description = "@text/sendPushsaferMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+            @ActionInput(name = "title", label = "@text/sendPushsaferMessageActionInputTitleLabel", description = "@text/sendPushsaferMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title) {
+        logger.trace(
+                "ThingAction 'sendPushsaferMessageToDevice' called with value(s): device='{}', message='{}', title='{}'",
+                device, message, title);
+        if (device == null) {
+            throw new IllegalArgumentException("Skip sending message as 'device' is null.");
+        }
+
+        return send(getDefaultPushsaferMessageBuilder(message).withDevice(device), title);
+    }
+
+    public static Boolean sendPushsaferMessageToDevice(ThingActions actions, String device, String message,
+            @Nullable String title) {
+        return ((PushsaferActions) actions).sendPushsaferMessageToDevice(device, message, title);
+    }
+
+    private PushsaferMessageBuilder getDefaultPushsaferMessageBuilder(String message) {
+        if (accountHandler == null) {
+            throw new RuntimeException("PushsaferAccountHandler is null!");
+        }
+
+        if (message == null) {
+            throw new IllegalArgumentException("Skip sending message as 'message' is null.");
+        }
+
+        try {
+            return accountHandler.getDefaultPushsaferMessageBuilder(message);
+        } catch (PushsaferConfigurationException e) {
+            throw new IllegalArgumentException(e.getCause());
+        }
+    }
+
+    private Boolean send(PushsaferMessageBuilder builder, @Nullable String title) {
+        if (title != null) {
+            builder.withTitle(title);
+        }
+        return accountHandler.sendPushsaferMessage(builder);
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        this.accountHandler = (PushsaferAccountHandler) handler;
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return accountHandler;
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/config/PushsaferAccountConfiguration.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/config/PushsaferAccountConfiguration.java
new file mode 100644 (file)
index 0000000..c6d2c1a
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.config;
+
+import static org.openhab.binding.pushsafer.internal.PushsaferBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link PushsaferAccountConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@NonNullByDefault
+public class PushsaferAccountConfiguration {
+    public @Nullable String apikey;
+    public @Nullable String user;
+    public String device = ALL_DEVICES;
+    public String title = DEFAULT_TITLE;
+    public String format = "none";
+    public String sound = DEFAULT_SOUND;
+    public String icon = DEFAULT_ICON;
+    public String color = DEFAULT_COLOR;
+    public String url = DEFAULT_URL;
+    public String urlTitle = DEFAULT_URLTITLE;
+    public boolean answer = DEFAULT_ANSWER;
+    public int confirm = DEFAULT_CONFIRM;
+    public int time2live = DEFAULT_TIME2LIVE;
+    public String vibration = DEFAULT_VIBRATION;
+    public int retry = 300;
+    public int expire = 3600;
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/config/PushsaferConfigOptionProvider.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/config/PushsaferConfigOptionProvider.java
new file mode 100644 (file)
index 0000000..f7de13d
--- /dev/null
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.config;
+
+import static org.openhab.binding.pushsafer.internal.PushsaferBindingConstants.*;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pushsafer.internal.dto.Icon;
+import org.openhab.binding.pushsafer.internal.dto.Sound;
+import org.openhab.binding.pushsafer.internal.handler.PushsaferAccountHandler;
+import org.openhab.core.config.core.ConfigOptionProvider;
+import org.openhab.core.config.core.ParameterOption;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link PushsaferConfigOptionProvider} class contains fields mapping thing configuration parameters.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@Component(service = ConfigOptionProvider.class)
+@NonNullByDefault
+public class PushsaferConfigOptionProvider implements ConfigOptionProvider, ThingHandlerService {
+
+    private @Nullable PushsaferAccountHandler accountHandler;
+
+    @Override
+    public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
+            @Nullable Locale locale) {
+        PushsaferAccountHandler localAccountHandler = accountHandler;
+        if (localAccountHandler != null) {
+            if (PUSHSAFER_ACCOUNT.getAsString().equals(uri.getSchemeSpecificPart()) && CONFIG_SOUND.equals(param)) {
+                List<Sound> sounds = localAccountHandler.getSounds();
+                if (!sounds.isEmpty()) {
+                    return sounds.stream().map(Sound::getAsParameterOption)
+                            .sorted(Comparator.comparing(ParameterOption::getLabel))
+                            .collect(Collectors.toUnmodifiableList());
+                }
+            }
+            if (PUSHSAFER_ACCOUNT.getAsString().equals(uri.getSchemeSpecificPart()) && CONFIG_ICON.equals(param)) {
+                List<Icon> icons = localAccountHandler.getIcons();
+                if (!icons.isEmpty()) {
+                    return icons.stream().map(Icon::getAsParameterOption)
+                            .sorted(Comparator.comparing(ParameterOption::getLabel))
+                            .collect(Collectors.toUnmodifiableList());
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void setThingHandler(@Nullable ThingHandler handler) {
+        this.accountHandler = (PushsaferAccountHandler) handler;
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return accountHandler;
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferAPIConnection.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferAPIConnection.java
new file mode 100644 (file)
index 0000000..a17ee51
--- /dev/null
@@ -0,0 +1,223 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.connection;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.pushsafer.internal.config.PushsaferAccountConfiguration;
+import org.openhab.binding.pushsafer.internal.dto.Icon;
+import org.openhab.binding.pushsafer.internal.dto.Sound;
+import org.openhab.core.cache.ExpiringCacheMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+/**
+ * The {@link PushsaferAPIConnection} is responsible for handling the connections to Pushsafer Messages API.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@NonNullByDefault
+public class PushsaferAPIConnection {
+
+    private final Logger logger = LoggerFactory.getLogger(PushsaferAPIConnection.class);
+
+    private static final String VALIDATE_URL = "https://www.pushsafer.com/api-k";
+    private static final String MESSAGE_URL = "https://www.pushsafer.com/api";
+    private static final String CANCEL_MESSAGE_URL = "https://www.pushsafer.com/api-m";
+    private static final String SOUNDS_URL = "https://www.pushsafer.com/api-s";
+    private static final String ICONS_URL = "https://www.pushsafer.com/api-i";
+
+    private final HttpClient httpClient;
+    private final PushsaferAccountConfiguration config;
+
+    private final ExpiringCacheMap<String, String> cache = new ExpiringCacheMap<>(TimeUnit.DAYS.toMillis(1));
+
+    public PushsaferAPIConnection(HttpClient httpClient, PushsaferAccountConfiguration config) {
+        this.httpClient = httpClient;
+        this.config = config;
+    }
+
+    public boolean validateUser() throws PushsaferCommunicationException, PushsaferConfigurationException {
+        final String localApikey = config.apikey;
+        if (localApikey == null || localApikey.isEmpty()) {
+            throw new PushsaferConfigurationException("@text/offline.conf-error-missing-apikey");
+        }
+        final String localUser = config.user;
+        if (localUser == null || localUser.isEmpty()) {
+            throw new PushsaferConfigurationException("@text/offline.conf-error-missing-user");
+        }
+
+        final String content = get(buildURL(VALIDATE_URL, Map.of(PushsaferMessageBuilder.MESSAGE_KEY_TOKEN, localApikey,
+                PushsaferMessageBuilder.MESSAGE_KEY_USER, localUser)));
+        final JsonObject json = content == null || content.isBlank() ? null
+                : JsonParser.parseString(content).getAsJsonObject();
+        return json == null ? false : getMessageStatus(json);
+    }
+
+    public boolean sendPushsaferMessage(PushsaferMessageBuilder message)
+            throws PushsaferCommunicationException, PushsaferConfigurationException {
+        return getMessageStatus(post(MESSAGE_URL, message.build()));
+    }
+
+    public String sendPushsaferPriorityMessage(PushsaferMessageBuilder message)
+            throws PushsaferCommunicationException, PushsaferConfigurationException {
+        final JsonObject json = JsonParser.parseString(post(MESSAGE_URL, message.build())).getAsJsonObject();
+        return getMessageStatus(json) && json.has("receipt") ? json.get("receipt").getAsString() : "";
+    }
+
+    public boolean cancelPushsaferPriorityMessage(String receipt)
+            throws PushsaferCommunicationException, PushsaferConfigurationException {
+        return getMessageStatus(post(CANCEL_MESSAGE_URL.replace("{receipt}", receipt),
+                PushsaferMessageBuilder.getInstance(config.apikey, config.device).build()));
+    }
+
+    public List<Sound> getSounds() throws PushsaferCommunicationException, PushsaferConfigurationException {
+        final String localApikey = config.apikey;
+        if (localApikey == null || localApikey.isEmpty()) {
+            throw new PushsaferConfigurationException("@text/offline.conf-error-missing-apikey");
+        }
+
+        final Map<String, String> params = new HashMap<>(1);
+        params.put(PushsaferMessageBuilder.MESSAGE_KEY_TOKEN, localApikey);
+
+        final String content = getFromCache(buildURL(SOUNDS_URL, params));
+        final JsonObject json = content == null || content.isBlank() ? null
+                : JsonParser.parseString(content).getAsJsonObject();
+        final JsonObject sounds = json == null || !json.has("sounds") ? null : json.get("sounds").getAsJsonObject();
+
+        return sounds == null ? List.of()
+                : sounds.entrySet().stream().map(entry -> new Sound(entry.getKey(), entry.getValue().getAsString()))
+                        .collect(Collectors.toUnmodifiableList());
+    }
+
+    public List<Icon> getIcons() throws PushsaferCommunicationException, PushsaferConfigurationException {
+        final String localApikey = config.apikey;
+        if (localApikey == null || localApikey.isEmpty()) {
+            throw new PushsaferConfigurationException("@text/offline.conf-error-missing-apikey");
+        }
+
+        final Map<String, String> params = new HashMap<>(1);
+        params.put(PushsaferMessageBuilder.MESSAGE_KEY_TOKEN, localApikey);
+
+        final String content = getFromCache(buildURL(ICONS_URL, params));
+        final JsonObject json = content == null || content.isBlank() ? null
+                : JsonParser.parseString(content).getAsJsonObject();
+        final JsonObject icons = json == null || !json.has("icons") ? null : json.get("icons").getAsJsonObject();
+
+        return icons == null ? List.of()
+                : icons.entrySet().stream().map(entry -> new Icon(entry.getKey(), entry.getValue().getAsString()))
+                        .collect(Collectors.toUnmodifiableList());
+    }
+
+    private String buildURL(String url, Map<String, String> requestParams) {
+        return requestParams.keySet().stream().map(key -> key + "=" + encodeParam(requestParams.get(key)))
+                .collect(Collectors.joining("&", url + "?", ""));
+    }
+
+    private String encodeParam(@Nullable String value) {
+        return value == null ? "" : URLEncoder.encode(value, StandardCharsets.UTF_8);
+    }
+
+    private @Nullable String getFromCache(String url)
+            throws PushsaferCommunicationException, PushsaferConfigurationException {
+        return cache.putIfAbsentAndGet(url, () -> get(url));
+    }
+
+    private String get(String url) throws PushsaferCommunicationException, PushsaferConfigurationException {
+        return executeRequest(HttpMethod.GET, url, null);
+    }
+
+    private String post(String url, ContentProvider body)
+            throws PushsaferCommunicationException, PushsaferConfigurationException {
+        return executeRequest(HttpMethod.POST, url, body);
+    }
+
+    private String executeRequest(HttpMethod httpMethod, String url, @Nullable ContentProvider body)
+            throws PushsaferCommunicationException, PushsaferConfigurationException {
+        logger.trace("Pushsafer request: {} - URL = '{}'", httpMethod, uglifyApikey(url));
+        try {
+            final Request request = httpClient.newRequest(url).method(httpMethod).timeout(10, TimeUnit.SECONDS);
+
+            if (body != null) {
+                if (logger.isTraceEnabled()) {
+                    logger.trace("Pushsafer request body: '{}'", body);
+                }
+                request.content(body);
+            }
+
+            final ContentResponse contentResponse = request.send();
+
+            final int httpStatus = contentResponse.getStatus();
+            final String content = contentResponse.getContentAsString();
+            logger.trace("Pushsafer response: status = {}, content = '{}'", httpStatus, content);
+            switch (httpStatus) {
+                case HttpStatus.OK_200:
+                    return content;
+                case 250:
+                case HttpStatus.BAD_REQUEST_400:
+                    logger.debug("Pushsafer server responded with status code {}: {}", httpStatus, content);
+                    throw new PushsaferConfigurationException(getMessageError(content));
+                default:
+                    logger.debug("Pushsafer server responded with status code {}: {}", httpStatus, content);
+                    throw new PushsaferCommunicationException(content);
+            }
+        } catch (ExecutionException e) {
+            logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
+            throw new PushsaferCommunicationException(e.getLocalizedMessage(), e.getCause());
+        } catch (InterruptedException | TimeoutException e) {
+            logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
+            throw new PushsaferCommunicationException(e.getLocalizedMessage());
+        }
+    }
+
+    private String uglifyApikey(String url) {
+        return url.replaceAll("(k=)+\\w+", "k=*****");
+    }
+
+    private String getMessageError(String content) {
+        final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
+        final JsonElement errorsElement = json.get("errors");
+        if (errorsElement != null && errorsElement.isJsonArray()) {
+            return errorsElement.getAsJsonArray().toString();
+        }
+        return "@text/offline.conf-error-unknown";
+    }
+
+    private boolean getMessageStatus(String content) {
+        return getMessageStatus(JsonParser.parseString(content).getAsJsonObject());
+    }
+
+    private boolean getMessageStatus(JsonObject json) {
+        return json.has("status") ? json.get("status").getAsInt() == 1 : false;
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferCommunicationException.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferCommunicationException.java
new file mode 100644 (file)
index 0000000..c57f560
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link PushsaferCommunicationException} is a configuration exception for the connections to Pushsafer Messages
+ * API.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@NonNullByDefault
+public class PushsaferCommunicationException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs a new exception with null as its detail message.
+     */
+    public PushsaferCommunicationException() {
+        super();
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message.
+     *
+     * @param message Detail message
+     */
+    public PushsaferCommunicationException(@Nullable String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new exception with the specified cause.
+     *
+     * @param cause The cause
+     */
+    public PushsaferCommunicationException(@Nullable Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message and cause.
+     *
+     * @param message Detail message
+     * @param cause The cause
+     */
+    public PushsaferCommunicationException(@Nullable String message, @Nullable Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferConfigurationException.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferConfigurationException.java
new file mode 100644 (file)
index 0000000..85c5769
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PushsaferConfigurationException} is a configuration exception for the connections to Pushsafer Messages
+ * API.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@NonNullByDefault
+public class PushsaferConfigurationException extends IllegalArgumentException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Constructs a new exception with null as its detail message.
+     */
+    public PushsaferConfigurationException() {
+        super();
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message.
+     *
+     * @param message Detail message
+     */
+    public PushsaferConfigurationException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new exception with the specified cause.
+     *
+     * @param cause The cause
+     */
+    public PushsaferConfigurationException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message and cause.
+     *
+     * @param message Detail message
+     * @param cause The cause
+     */
+    public PushsaferConfigurationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferMessageBuilder.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/connection/PushsaferMessageBuilder.java
new file mode 100644 (file)
index 0000000..958bedf
--- /dev/null
@@ -0,0 +1,355 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.connection;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Properties;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.util.MultiPartContentProvider;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.openhab.core.io.net.http.HttpUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PushsaferMessageBuilder} builds the body for Pushsafer Messages API requests.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@NonNullByDefault
+public class PushsaferMessageBuilder {
+
+    private final Logger logger = LoggerFactory.getLogger(PushsaferMessageBuilder.class);
+
+    public static final String MESSAGE_KEY_TOKEN = "k";
+    public static final String MESSAGE_KEY_USER = "u";
+    private static final String MESSAGE_KEY_MESSAGE = "m";
+    private static final String MESSAGE_KEY_TITLE = "t";
+    private static final String MESSAGE_KEY_DEVICE = "d";
+    private static final String MESSAGE_KEY_ICON = "i";
+    private static final String MESSAGE_KEY_COLOR = "c";
+    private static final String MESSAGE_KEY_VIBRATION = "v";
+    private static final String MESSAGE_KEY_PRIORITY = "pr";
+    private static final String MESSAGE_KEY_RETRY = "re";
+    private static final String MESSAGE_KEY_EXPIRE = "ex";
+    private static final String MESSAGE_KEY_URL = "u";
+    private static final String MESSAGE_KEY_URL_TITLE = "ut";
+    private static final String MESSAGE_KEY_SOUND = "s";
+    private static final String MESSAGE_KEY_TIME2LIVE = "l";
+    private static final String MESSAGE_KEY_ANSWER = "a";
+    private static final String MESSAGE_KEY_CONFIRM = "cr";
+    private static final String MESSAGE_KEY_ATTACHMENT = "p";
+    public static final String MESSAGE_KEY_HTML = "html";
+    public static final String MESSAGE_KEY_MONOSPACE = "monospace";
+
+    private static final int MAX_MESSAGE_LENGTH = 4096;
+    private static final int MAX_TITLE_LENGTH = 250;
+    private static final int MAX_DEVICE_LENGTH = 25;
+    private static final List<Integer> VALID_PRIORITY_LIST = Arrays.asList(-2, -1, 0, 1, 2);
+    private static final int DEFAULT_PRIORITY = 0;
+    public static final int EMERGENCY_PRIORITY = 2;
+    private static final int MIN_RETRY_SECONDS = 0;
+    private static final int MAX_EXPIRE_SECONDS = 10800;
+    private static final int MAX_URL_LENGTH = 512;
+    private static final int MAX_URL_TITLE_LENGTH = 100;
+    public static final String DEFAULT_CONTENT_TYPE = "jpeg";
+    public static final String DEFAULT_AUTH = "";
+
+    private final MultiPartContentProvider body = new MultiPartContentProvider();
+
+    private @Nullable String message;
+    private @Nullable String title;
+    private @Nullable String device;
+    private int priority = DEFAULT_PRIORITY;
+    private int retry = 300;
+    private int expire = 3600;
+    private @Nullable String url;
+    private @Nullable String urlTitle;
+    private @Nullable String sound;
+    private @Nullable String icon;
+    private int confirm;
+    private int time2live;
+    private boolean answer;
+    private @Nullable String color;
+    private @Nullable String vibration;
+    private @Nullable String attachment;
+    private String contentType = DEFAULT_CONTENT_TYPE;
+    private String authentication = DEFAULT_AUTH;
+    private boolean html = false;
+    private boolean monospace = false;
+
+    private PushsaferMessageBuilder(String apikey, String device) throws PushsaferConfigurationException {
+        body.addFieldPart(MESSAGE_KEY_TOKEN, new StringContentProvider(apikey), null);
+        body.addFieldPart(MESSAGE_KEY_DEVICE, new StringContentProvider(device), null);
+    }
+
+    public static PushsaferMessageBuilder getInstance(@Nullable String apikey, @Nullable String device)
+            throws PushsaferConfigurationException {
+        if (apikey == null || apikey.isEmpty()) {
+            throw new PushsaferConfigurationException("@text/offline.conf-error-missing-apikey");
+        }
+
+        if (device == null || device.isEmpty()) {
+            throw new PushsaferConfigurationException("@text/offline.conf-error-missing-device");
+        }
+
+        return new PushsaferMessageBuilder(apikey, device);
+    }
+
+    public PushsaferMessageBuilder withMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withTitle(String title) {
+        this.title = title;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withDevice(String device) {
+        this.device = device;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withPriority(int priority) {
+        this.priority = priority;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withRetry(int retry) {
+        this.retry = retry;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withExpire(int expire) {
+        this.expire = expire;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withUrl(String url) {
+        this.url = url;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withUrlTitle(String urlTitle) {
+        this.urlTitle = urlTitle;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withSound(String sound) {
+        this.sound = sound;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withIcon(String icon) {
+        this.icon = icon;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withColor(String color) {
+        this.color = color;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withVibration(String vibration) {
+        this.vibration = vibration;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withAnswer(boolean answer) {
+        this.answer = answer;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withTime2live(int time2live) {
+        this.time2live = time2live;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withConfirm(int confirm) {
+        this.confirm = confirm;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withAttachment(String attachment) {
+        this.attachment = attachment;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withContentType(String contentType) {
+        this.contentType = contentType;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withAuthentication(String authentication) {
+        this.authentication = authentication;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withHtmlFormatting() {
+        this.html = true;
+        return this;
+    }
+
+    public PushsaferMessageBuilder withMonospaceFormatting() {
+        this.monospace = true;
+        return this;
+    }
+
+    public ContentProvider build() throws PushsaferCommunicationException {
+        if (message != null) {
+            if (message.length() > MAX_MESSAGE_LENGTH) {
+                throw new IllegalArgumentException(String.format(
+                        "Skip sending the message as 'message' is longer than %d characters.", MAX_MESSAGE_LENGTH));
+            }
+            body.addFieldPart(MESSAGE_KEY_MESSAGE, new StringContentProvider(message), null);
+        }
+
+        if (title != null) {
+            if (title.length() > MAX_TITLE_LENGTH) {
+                throw new IllegalArgumentException(String
+                        .format("Skip sending the message as 'title' is longer than %d characters.", MAX_TITLE_LENGTH));
+            }
+            body.addFieldPart(MESSAGE_KEY_TITLE, new StringContentProvider(title), null);
+        }
+
+        if (device != null) {
+            if (device.length() > MAX_DEVICE_LENGTH) {
+                logger.warn("Skip 'device' as it is longer than {} characters. Got: {}.", MAX_DEVICE_LENGTH, device);
+            } else {
+                body.addFieldPart(MESSAGE_KEY_DEVICE, new StringContentProvider(device), null);
+            }
+        }
+
+        if (priority != DEFAULT_PRIORITY) {
+            if (VALID_PRIORITY_LIST.contains(priority)) {
+                body.addFieldPart(MESSAGE_KEY_PRIORITY, new StringContentProvider(String.valueOf(priority)), null);
+
+                if (priority == EMERGENCY_PRIORITY) {
+                    if (retry < MIN_RETRY_SECONDS) {
+                        logger.warn("Retry value of {} is too small. Using default value of {}.", retry,
+                                MIN_RETRY_SECONDS);
+                        body.addFieldPart(MESSAGE_KEY_RETRY,
+                                new StringContentProvider(String.valueOf(MIN_RETRY_SECONDS)), null);
+                    } else {
+                        body.addFieldPart(MESSAGE_KEY_RETRY, new StringContentProvider(String.valueOf(retry)), null);
+                    }
+
+                    if (0 < expire && expire <= MAX_EXPIRE_SECONDS) {
+                        body.addFieldPart(MESSAGE_KEY_EXPIRE, new StringContentProvider(String.valueOf(expire)), null);
+                    } else {
+                        logger.warn("Expire value of {} is invalid. Using default value of {}.", expire,
+                                MAX_EXPIRE_SECONDS);
+                        body.addFieldPart(MESSAGE_KEY_EXPIRE,
+                                new StringContentProvider(String.valueOf(MAX_EXPIRE_SECONDS)), null);
+                    }
+                }
+            } else {
+                logger.warn("Invalid 'priority', skipping. Expected: {}. Got: {}.",
+                        VALID_PRIORITY_LIST.stream().map(i -> i.toString()).collect(Collectors.joining(",")), priority);
+            }
+        }
+
+        if (url != null) {
+            if (url.length() > MAX_URL_LENGTH) {
+                throw new IllegalArgumentException(String
+                        .format("Skip sending the message as 'url' is longer than %d characters.", MAX_URL_LENGTH));
+            }
+            body.addFieldPart(MESSAGE_KEY_URL, new StringContentProvider(url), null);
+
+            if (urlTitle != null) {
+                if (urlTitle.length() > MAX_URL_TITLE_LENGTH) {
+                    throw new IllegalArgumentException(
+                            String.format("Skip sending the message as 'urlTitle' is longer than %d characters.",
+                                    MAX_URL_TITLE_LENGTH));
+                }
+                body.addFieldPart(MESSAGE_KEY_URL_TITLE, new StringContentProvider(urlTitle), null);
+            }
+        }
+
+        if (sound != null) {
+            body.addFieldPart(MESSAGE_KEY_SOUND, new StringContentProvider(sound), null);
+        }
+
+        if (icon != null) {
+            body.addFieldPart(MESSAGE_KEY_ICON, new StringContentProvider(icon), null);
+        }
+
+        if (color != null) {
+            body.addFieldPart(MESSAGE_KEY_COLOR, new StringContentProvider(color), null);
+        }
+
+        if (vibration != null) {
+            body.addFieldPart(MESSAGE_KEY_VIBRATION, new StringContentProvider(vibration), null);
+        }
+
+        body.addFieldPart(MESSAGE_KEY_CONFIRM, new StringContentProvider(String.valueOf(confirm)), null);
+
+        body.addFieldPart(MESSAGE_KEY_ANSWER, new StringContentProvider(String.valueOf(answer)), null);
+
+        body.addFieldPart(MESSAGE_KEY_TIME2LIVE, new StringContentProvider(String.valueOf(time2live)), null);
+
+        if (attachment != null) {
+            final String encodedString;
+            try {
+                if (attachment.startsWith("http")) {
+                    Properties headers = new Properties();
+                    headers.put("User-Agent", "Mozilla/5.0");
+                    if (!authentication.isBlank()) {
+                        headers.put("Authorization", "Basic "
+                                + Base64.getEncoder().encodeToString(authentication.getBytes(StandardCharsets.UTF_8)));
+                    }
+                    String content = HttpUtil.executeUrl("GET", attachment, headers, null, null, 10);
+                    if (content == null) {
+                        throw new IllegalArgumentException(
+                                String.format("Skip sending the message as content '%s' does not exist.", attachment));
+                    }
+                    encodedString = "data:" + contentType + ";base64," + content;
+                } else {
+                    File file = new File(attachment);
+                    if (!file.exists()) {
+                        throw new IllegalArgumentException(
+                                String.format("Skip sending the message as file '%s' does not exist.", attachment));
+                    }
+                    byte[] fileContent = Files.readAllBytes(file.toPath());
+                    encodedString = "data:image/" + contentType + ";base64,"
+                            + Base64.getEncoder().encodeToString(fileContent);
+                }
+                body.addFieldPart(MESSAGE_KEY_ATTACHMENT, new StringContentProvider(encodedString), null);
+            } catch (IOException e) {
+                logger.debug("IOException occurred - skip sending message: {}", e.getLocalizedMessage(), e);
+                throw new PushsaferCommunicationException(
+                        String.format("Skip sending the message: %s", e.getLocalizedMessage()), e);
+            }
+        }
+
+        if (html) {
+            body.addFieldPart(MESSAGE_KEY_HTML, new StringContentProvider("1"), null);
+        } else if (monospace) {
+            body.addFieldPart(MESSAGE_KEY_MONOSPACE, new StringContentProvider("1"), null);
+        }
+
+        body.close();
+        return body;
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/dto/Icon.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/dto/Icon.java
new file mode 100644 (file)
index 0000000..50c9df2
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.config.core.ParameterOption;
+
+/**
+ * The {@link Icons} is the Java class used to map the JSON response to an Pushsafer API request..
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@NonNullByDefault
+public class Icon {
+    public String icon;
+    public String label;
+
+    public Icon(String icon, String label) {
+        this.icon = icon;
+        this.label = label;
+    }
+
+    public ParameterOption getAsParameterOption() {
+        return new ParameterOption(icon, label);
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/dto/Sound.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/dto/Sound.java
new file mode 100644 (file)
index 0000000..42651ca
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.config.core.ParameterOption;
+
+/**
+ * The {@link Sound} is the Java class used to map the JSON response to an Pushsafer API request.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@NonNullByDefault
+public class Sound {
+    public String sound;
+    public String label;
+
+    public Sound(String sound, String label) {
+        this.sound = sound;
+        this.label = label;
+    }
+
+    public ParameterOption getAsParameterOption() {
+        return new ParameterOption(sound, label);
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/factory/PushsaferHandlerFactory.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/factory/PushsaferHandlerFactory.java
new file mode 100644 (file)
index 0000000..e1a32ec
--- /dev/null
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.factory;
+
+import static org.openhab.binding.pushsafer.internal.PushsaferBindingConstants.PUSHSAFER_ACCOUNT;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.pushsafer.internal.handler.PushsaferAccountHandler;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link PushsaferHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@Component(configurationPid = "binding.pushsafer", service = ThingHandlerFactory.class)
+@NonNullByDefault
+public class PushsaferHandlerFactory extends BaseThingHandlerFactory {
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(PUSHSAFER_ACCOUNT);
+
+    private final HttpClient httpClient;
+
+    @Activate
+    public PushsaferHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
+        this.httpClient = httpClientFactory.getCommonHttpClient();
+    }
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        if (PUSHSAFER_ACCOUNT.equals(thingTypeUID)) {
+            return new PushsaferAccountHandler(thing, httpClient);
+        }
+
+        return null;
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/handler/PushsaferAccountHandler.java b/bundles/org.openhab.binding.pushsafer/src/main/java/org/openhab/binding/pushsafer/internal/handler/PushsaferAccountHandler.java
new file mode 100644 (file)
index 0000000..8411937
--- /dev/null
@@ -0,0 +1,246 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.handler;
+
+import static org.openhab.binding.pushsafer.internal.PushsaferBindingConstants.*;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.pushsafer.internal.actions.PushsaferActions;
+import org.openhab.binding.pushsafer.internal.config.PushsaferAccountConfiguration;
+import org.openhab.binding.pushsafer.internal.config.PushsaferConfigOptionProvider;
+import org.openhab.binding.pushsafer.internal.connection.PushsaferAPIConnection;
+import org.openhab.binding.pushsafer.internal.connection.PushsaferCommunicationException;
+import org.openhab.binding.pushsafer.internal.connection.PushsaferConfigurationException;
+import org.openhab.binding.pushsafer.internal.connection.PushsaferMessageBuilder;
+import org.openhab.binding.pushsafer.internal.dto.Icon;
+import org.openhab.binding.pushsafer.internal.dto.Sound;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.openhab.core.types.Command;
+
+/**
+ * The {@link PushsaferAccountHandler} is responsible for handling commands, which are sent to one of the channels.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@NonNullByDefault
+public class PushsaferAccountHandler extends BaseThingHandler {
+
+    private static final Collection<Class<? extends ThingHandlerService>> SUPPORTED_THING_ACTIONS = Set
+            .of(PushsaferActions.class, PushsaferConfigOptionProvider.class);
+
+    private final HttpClient httpClient;
+
+    private PushsaferAccountConfiguration config = new PushsaferAccountConfiguration();
+    private @Nullable PushsaferAPIConnection connection;
+
+    public PushsaferAccountHandler(Thing thing, HttpClient httpClient) {
+        super(thing);
+        this.httpClient = httpClient;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        // nothing
+    }
+
+    @Override
+    public void initialize() {
+        config = getConfigAs(PushsaferAccountConfiguration.class);
+
+        boolean configValid = true;
+        final String apikey = config.apikey;
+        if (apikey == null || apikey.isEmpty()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/offline.conf-error-missing-apikey");
+            configValid = false;
+        }
+        final String user = config.user;
+        if (user == null || user.isEmpty()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/offline.conf-error-missing-user");
+            configValid = false;
+        }
+
+        if (configValid) {
+            updateStatus(ThingStatus.UNKNOWN);
+
+            connection = new PushsaferAPIConnection(httpClient, config);
+            scheduler.submit(this::asyncValidateUser);
+        }
+    }
+
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return SUPPORTED_THING_ACTIONS;
+    }
+
+    /**
+     * Retrieves the list of current sounds and their descriptions from the Pushsafer API.
+     *
+     * @return a list of {@link Sound}s
+     */
+    public List<Sound> getSounds() {
+        try {
+            return connection != null ? connection.getSounds() : List.of();
+        } catch (PushsaferCommunicationException e) {
+            // do nothing, causing exception is already logged
+        } catch (PushsaferConfigurationException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+        }
+        return List.of();
+    }
+
+    /**
+     * Retrieves the list of current icons from the Pushsafer API.
+     *
+     * @return a list of {@link Icon}s
+     */
+    public List<Icon> getIcons() {
+        try {
+            return connection != null ? connection.getIcons() : List.of();
+        } catch (PushsaferCommunicationException e) {
+            // do nothing, causing exception is already logged
+        } catch (PushsaferConfigurationException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+        }
+        return List.of();
+    }
+
+    /**
+     * Returns a preconfigured {@link PushsaferMessageBuilder}.
+     *
+     * @param message the message
+     * @return a {@link PushsaferMessageBuilder} instance
+     */
+    public PushsaferMessageBuilder getDefaultPushsaferMessageBuilder(String message)
+            throws PushsaferConfigurationException {
+        PushsaferMessageBuilder builder = PushsaferMessageBuilder.getInstance(config.apikey, config.device)
+                .withMessage(message) //
+                .withTitle(config.title) //
+                .withRetry(config.retry) //
+                .withExpire(config.expire);
+        // specify format if defined
+        switch (config.format) {
+            case PushsaferMessageBuilder.MESSAGE_KEY_HTML:
+                builder.withHtmlFormatting();
+                break;
+            case PushsaferMessageBuilder.MESSAGE_KEY_MONOSPACE:
+                builder.withMonospaceFormatting();
+            default:
+                break;
+        }
+        // add sound if defined
+        if (!DEFAULT_SOUND.equals(config.sound)) {
+            builder.withSound(config.sound);
+        }
+        // add icon if defined
+        if (!DEFAULT_ICON.equals(config.icon)) {
+            builder.withIcon(config.icon);
+        }
+        // add color if defined
+        if (!DEFAULT_COLOR.equals(config.color)) {
+            builder.withColor(config.color);
+        }
+        // add vibration if defined
+        if (!DEFAULT_VIBRATION.equals(config.vibration)) {
+            builder.withVibration(config.vibration);
+        }
+        // add url if defined
+        if (!DEFAULT_URL.equals(config.url)) {
+            builder.withUrl(config.url);
+        }
+        // add urlTitle if defined
+        if (!DEFAULT_URLTITLE.equals(config.urlTitle)) {
+            builder.withUrlTitle(config.urlTitle);
+        }
+        // add confirm if defined
+        if (DEFAULT_CONFIRM != config.confirm) {
+            builder.withConfirm(config.confirm);
+        }
+        // add answer if defined
+        if (DEFAULT_ANSWER != config.answer) {
+            builder.withAnswer(config.answer);
+        }
+        // add time2live if defined
+        if (DEFAULT_TIME2LIVE != config.time2live) {
+            builder.withTime2live(config.time2live);
+        }
+        return builder;
+    }
+
+    public boolean sendPushsaferMessage(PushsaferMessageBuilder messageBuilder) {
+        if (connection != null) {
+            try {
+                return connection.sendPushsaferMessage(messageBuilder);
+            } catch (PushsaferCommunicationException e) {
+                // do nothing, causing exception is already logged
+            } catch (PushsaferConfigurationException e) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+            }
+            return false;
+        } else {
+            throw new IllegalArgumentException("PushsaferAPIConnection is null!");
+        }
+    }
+
+    public String sendPushsaferPriorityMessage(PushsaferMessageBuilder messageBuilder) {
+        if (connection != null) {
+            try {
+                return connection.sendPushsaferPriorityMessage(messageBuilder);
+            } catch (PushsaferCommunicationException e) {
+                // do nothing, causing exception is already logged
+            } catch (PushsaferConfigurationException e) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+            }
+            return "";
+        } else {
+            throw new IllegalArgumentException("PushsaferAPIConnection is null!");
+        }
+    }
+
+    public boolean cancelPushsaferPriorityMessage(String receipt) {
+        if (connection != null) {
+            try {
+                return connection.cancelPushsaferPriorityMessage(receipt);
+            } catch (PushsaferCommunicationException e) {
+                // do nothing, causing exception is already logged
+            } catch (PushsaferConfigurationException e) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+            }
+            return false;
+        } else {
+            throw new IllegalArgumentException("PushsaferAPIConnection is null!");
+        }
+    }
+
+    @SuppressWarnings("null")
+    private void asyncValidateUser() {
+        try {
+            connection.validateUser();
+            updateStatus(ThingStatus.ONLINE);
+        } catch (PushsaferCommunicationException | PushsaferConfigurationException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..c329f4b
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="pushsafer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Pushsafer Binding</name>
+       <description>With Pushsafer you can send &amp; receive push notifications in real time, easily and securely on your
+               iPhone, iPad, Android, Windows mobile or Windows desktop device as well as on your browser (Chrome, Firefox, Opera
+               &amp; Yandex)!</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..6f362b8
--- /dev/null
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:pushsafer:pushsafer-account">
+               <parameter name="apikey" type="text" required="true">
+                       <context>password</context>
+                       <label>Private or Alias Key</label>
+                       <description>Your Private or Alias to access the Pushsafer Message API.</description>
+               </parameter>
+               <parameter name="user" type="text" required="true">
+                       <label>Username</label>
+                       <description>Your username or email address to validate against the Pushsafer Message API.</description>
+               </parameter>
+               <parameter name="device" type="text">
+                       <label>Device ID</label>
+                       <description>Device ID or Device Group ID to which devices you want to send push-notifications ("a" for all available
+                               devices).</description>
+                       <default>a</default>
+               </parameter>
+               <parameter name="title" type="text">
+                       <label>Title</label>
+                       <description>The default title of a message.</description>
+                       <default>openHAB</default>
+               </parameter>
+               <parameter name="format" type="text">
+                       <label>Format</label>
+                       <description>The default format of a message.</description>
+                       <default>none</default>
+                       <options>
+                               <option value="none">None</option>
+                               <option value="html">HTML</option>
+                               <option value="monospace">monospace</option>
+                       </options>
+               </parameter>
+               <parameter name="sound" type="text">
+                       <label>Notification Sound</label>
+                       <description>The default notification sound on target device.</description>
+                       <default>1</default>
+               </parameter>
+               <parameter name="vibration" type="text">
+                       <label>Vibration</label>
+                       <description>How often the device should vibrate. empty=device default or a number 1-3</description>
+                       <default>1</default>
+               </parameter>
+               <parameter name="icon" type="text">
+                       <label>Notification Icon</label>
+                       <description>The default notification icon on target device.</description>
+                       <default>1</default>
+               </parameter>
+               <parameter name="color" type="text">
+                       <label>Icon Color</label>
+                       <description>The color (hexadecimal) of notification icon (e.g. #FF0000).</description>
+                       <default></default>
+               </parameter>
+               <parameter name="url" type="text">
+                       <label>URL</label>
+                       <description>URL or URL Scheme send with notification.</description>
+               </parameter>
+               <parameter name="urlTitle" type="text">
+                       <label>URL Title</label>
+                       <description>Title of URL.</description>
+               </parameter>
+               <parameter name="retry" type="integer" min="0" max="43200" step="1" unit="m">
+                       <advanced>true</advanced>
+                       <label>Retry</label>
+                       <description>Integer 0-43200: Time in minutes, after a message automatically gets purged.</description>
+                       <default>0</default>
+               </parameter>
+               <parameter name="expire" type="integer" min="0" max="10800" step="60" unit="s">
+                       <advanced>true</advanced>
+                       <label>Expire</label>
+                       <description>Integer 60-10800 (60s steps): Time in seconds, after the retry/resend should stop.</description>
+                       <default>0</default>
+               </parameter>
+               <parameter name="confirm" type="integer" min="0" max="10800" step="10" unit="s">
+                       <advanced>true</advanced>
+                       <label>Confirm</label>
+                       <description>Integer 10-10800 (10s steps): Time in seconds after which a message should be sent again before it is
+                               confirmed.</description>
+                       <default>0</default>
+               </parameter>
+               <parameter name="time2live" type="integer" min="0" max="43200" step="1" unit="m">
+                       <advanced>true</advanced>
+                       <label>Time to Live</label>
+                       <description>Time in minutes, after a message automatically gets purged.</description>
+                       <default>0</default>
+               </parameter>
+               <parameter name="answer" type="boolean">
+                       <advanced>true</advanced>
+                       <label>Answer</label>
+                       <description>true = Enable reply to push notifications, false otherwise.</description>
+                       <default>false</default>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/i18n/pushsafer.properties b/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/i18n/pushsafer.properties
new file mode 100644 (file)
index 0000000..8b4177f
--- /dev/null
@@ -0,0 +1,56 @@
+# user defined messages
+offline.conf-error-missing-apikey = The 'Private Key' parameter must be configured.
+offline.conf-error-missing-user = The 'Username' parameter must be configured.
+offline.conf-error-missing-device = The 'Device ID' parameter must be configured.
+offline.conf-error-unknown = An unknown error occurred.
+
+# actions
+sendPushsaferMessageActionLabel = send a plain text message
+sendPushsaferMessageActionDescription = This method is used to send a plain text message.
+sendPushsaferMessageActionOutputLabel = Sent
+sendPushsaferMessageActionOutputDescription = true, if message has been sent successfully
+sendPushsaferMessageActionInputMessageLabel = Message
+sendPushsaferMessageActionInputMessageDescription = Message to be sent.
+sendPushsaferMessageActionInputTitleLabel = Title
+sendPushsaferMessageActionInputTitleDescription = The title of the message.
+
+sendPushsaferURLMessageActionLabel = send a plain text message with an URL
+sendPushsaferURLMessageActionDescription = This method is used to send a message with an URL.
+sendPushsaferMessageActionInputURLLabel = URL
+sendPushsaferMessageActionInputURLDescription = A supplementary URL to show with the message.
+sendPushsaferMessageActionInputURLTitleLabel = URL Title
+sendPushsaferMessageActionInputURLTitleDescription = A title for the URL, otherwise just the URL is shown.
+
+sendHTMLMessageActionLabel = send a HTML message
+sendHTMLMessageActionDescription = This method is used to send a HTML message.
+
+sendPushsaferMonospaceMessageActionLabel = send a monospace message
+sendPushsaferMonospaceMessageActionDescription = This method is used to send a monospace message.
+
+sendPushsaferAttachmentMessageActionLabel = send a plain text message with an image attachment
+sendPushsaferAttachmentMessageActionDescription = This method is used to send a message with an image attachment.
+sendPushsaferMessageActionInputAttachmentLabel = Image Attachment
+sendPushsaferMessageActionInputAttachmentDescription = A local path or url to the image.
+sendPushsaferMessageActionInputContentTypeLabel = Image Type
+sendPushsaferMessageActionInputContentTypeDescription = The image type of the attachment. Defaults to "jpeg", possible values "jpeg,png,gif".
+sendPushsaferMessageActionInputAuthenticationLabel = Authentication
+sendPushsaferMessageActionInputAuthenticationDescription = Basic access authentication for HTTP(S) requests. Default: "", Example: "user:password".
+
+sendPushsaferPriorityMessageActionLabel = send a priority message
+sendPushsaferPriorityMessageActionDescription = This method is used to send a priority message.
+sendPushsaferPriorityMessageActionOutputLabel = Receipt
+sendPushsaferPriorityMessageActionOutputDescription = Receipt, if priority message sent successfully.
+sendPushsaferMessageActionInputPriorityLabel = Priority
+sendPushsaferMessageActionInputPriorityDescription = Priority to be used. Defaults to 2.
+
+cancelPushsaferPriorityMessageActionLabel = cancel a priority message
+cancelPushsaferPriorityMessageActionDescription = This method is used to cancel a priority message.
+cancelPushsaferPriorityMessageActionOnputLabel = Cancelled
+cancelPushsaferPriorityMessageActionOnputDescription = true, if message has been cancelled successfully.
+cancelPushsaferPriorityMessageActionInputReceiptLabel = Receipt
+cancelPushsaferPriorityMessageActionInputReceiptDescription = Receipt of the message to be canceled.
+
+sendPushsaferMessageToDeviceActionLabel = send a plain text message to a specific device
+sendPushsaferMessageToDeviceActionDescription = This method is used to send a message to a specific device.
+sendPushsaferMessageActionInputDeviceLabel = Device
+sendPushsaferMessageActionInputDeviceDescription = The name of a specific device (multiple devices may be separated by a comma).
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/i18n/pushsafer_de.properties b/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/i18n/pushsafer_de.properties
new file mode 100644 (file)
index 0000000..2b84da4
--- /dev/null
@@ -0,0 +1,97 @@
+# binding
+binding.pushsafer.description = Mit Pushsafer kannst du in Echtzeit, einfach &amp; sicher, Push-Benachrichtigungen auf dein iPhone, iPad, Android, Windows mobile oder Windows Desktop Gerät sowie an deinen Browser (Chrome, Firefox, Opera &amp; Yandex) senden & empfangen!.
+
+# thing types
+thing-type.pushsafer.pushsafer-account.label = Pushsafer Konto
+thing-type.pushsafer.pushsafer-account.description = Ermöglicht den Zugriff auf die Pushsafer Message API.
+
+# thing type config description
+thing-type.config.pushsafer.pushsafer-account.apikey.label = Privater oder Alias Schlüssel
+thing-type.config.pushsafer.pushsafer-account.apikey.description = Privater oder Alias Schlüssel für den Zugriff auf die Pushsafer Message API.
+thing-type.config.pushsafer.pushsafer-account.user.label = Benutzername oder E-mAil Adresse für den Zugriff auf die Pushsafer Message API.
+thing-type.config.pushsafer.pushsafer-account.user.description = Benutzername oder E-mAil Adresse .
+thing-type.config.pushsafer.pushsafer-account.device.label = Geräte ID oder Geräte-Gruppen ID
+thing-type.config.pushsafer.pushsafer-account.device.description = Geräte ID oder Geräte-Gruppen ID an welche Geräte die Nachrichten gesendet werden sollen.
+thing-type.config.pushsafer.pushsafer-account.title.label = Titel
+thing-type.config.pushsafer.pushsafer-account.title.description = Standardtitel der Nachricht.
+thing-type.config.pushsafer.pushsafer-account.format.label = Format
+thing-type.config.pushsafer.pushsafer-account.format.description = Standardformat der Nachricht.
+thing-type.config.pushsafer.pushsafer-account.sound.label = Benachrichtigungs-Ton
+thing-type.config.pushsafer.pushsafer-account.sound.description = Standardbenachrichtigungs-Ton auf dem Endgerät.
+thing-type.config.pushsafer.pushsafer-account.vibration.label = Vibration
+thing-type.config.pushsafer.pushsafer-account.vibration.description = Wie oft das Gerät vibrieren soll. Leer=Geräte-Standard oder 0-3
+thing-type.config.pushsafer.pushsafer-account.icon.label = Benachrichtigungs-Icon
+thing-type.config.pushsafer.pushsafer-account.icon.description = Standardbenachrichtigungs-Icon auf dem Endgerät.
+thing-type.config.pushsafer.pushsafer-account.color.label = Icon Farbe
+thing-type.config.pushsafer.pushsafer-account.color.description = Standard Farbe des Icons (hexadezimal z.B. #FF0000)
+thing-type.config.pushsafer.pushsafer-account.url.label = URL
+thing-type.config.pushsafer.pushsafer-account.url.description = URL oder URL Schema welche mit der Benachrichtigung versendet wird.
+thing-type.config.pushsafer.pushsafer-account.urlTitle.label = URL Titel
+thing-type.config.pushsafer.pushsafer-account.urlTitle.description = Titel der URL.
+thing-type.config.pushsafer.pushsafer-account.retry.label = Wiederholungen
+thing-type.config.pushsafer.pushsafer-account.retry.description = Ganzzahl 60-10800 (60er Schritte): Zeit in Sekunden, nach der die Nachricht erneut versendet werden soll.
+thing-type.config.pushsafer.pushsafer-account.expire.label = Verfall
+thing-type.config.pushsafer.pushsafer-account.expire.description = Ganzzahl 60-10800: Zeit in Sekunden, nach der das erneute Versenden der Nachrichten gestoppt werden soll.
+thing-type.config.pushsafer.pushsafer-account.confirm.label = Bestätigung
+thing-type.config.pushsafer.pushsafer-account.confirm.description = Ganzzahl 10-10800 (10s Schritte) Zeit in Sekunden, nachdem eine Nachricht erneut gesendet werden soll, bis diese bestätigt wird.
+thing-type.config.pushsafer.pushsafer-account.answer.label = Antworten
+thing-type.config.pushsafer.pushsafer-account.answer.description = Ermöglicht das Antworten auf Push-Benachrichtigungen. 1 = auf diese Nachricht kann geantwortet werden, 0 = auf diese Nachricht kann nicht geantwortet werden.
+thing-type.config.pushsafer.pushsafer-account.time2live.label = Time to Live
+thing-type.config.pushsafer.pushsafer-account.time2live.description = Ganzzahl 0-43200: Zeit in Minuten, nach der die Nachricht automatisch gelöscht wird. 0 oder leer = nicht automatisch löschen.
+
+# user defined messages
+offline.conf-error-missing-apikey = Der Parameter 'apikey' muss konfiguriert werden.
+offline.conf-error-missing-user = Der Parameter 'user' muss konfiguriert werden.
+offline.conf-error-missing-device = Der Parameter 'device' muss konfiguriert werden.
+offline.conf-error-unknown = Ein unbekannter Fehler ist aufgetreten.
+
+# actions
+sendPushsaferMessageActionLabel = eine Textnachricht senden
+sendPushsaferMessageActionDescription = Action zum Versenden einer Textnachricht.
+sendPushsaferMessageActionOutputLabel = Gesendet
+sendPushsaferMessageActionOutputDescription = true, wenn die Nachricht erfolgreich versendet wurde.
+sendPushsaferMessageActionInputMessageLabel = Nachricht
+sendPushsaferMessageActionInputMessageDescription = Die Nachricht.
+sendPushsaferMessageActionInputTitleLabel = Titel
+sendPushsaferMessageActionInputTitleDescription = Titel der Nachricht.
+
+sendPushsaferURLMessageActionLabel = eine Textnachricht mit URL senden
+sendPushsaferURLMessageActionDescription = Action zum Versenden einer Textnachricht mit einer URL.
+sendPushsaferMessageActionInputURLLabel = URL
+sendPushsaferMessageActionInputURLDescription = Eine zusätzliche URL, die mit der Nachricht angezeigt werden soll.
+sendPushsaferMessageActionInputURLTitleLabel = URL Title
+sendPushsaferMessageActionInputURLTitleDescription = Ein Titel für die URL, andernfalls wird nur die URL angezeigt.
+
+sendHTMLMessageActionLabel = eine HTML-Nachricht senden
+sendHTMLMessageActionDescription = Action zum Versenden einer HTML-Nachricht.
+
+sendPushsaferMonospaceMessageActionLabel = eine monospace-Nachricht senden
+sendPushsaferMonospaceMessageActionDescription = Action zum Versenden einer monospace-Nachricht.
+
+sendPushsaferAttachmentMessageActionLabel = eine Textnachricht mit Bild-Anhang senden
+sendPushsaferAttachmentMessageActionDescription = Action zum Versenden einer Textnachricht mit Bild-Anhang.
+sendPushsaferMessageActionInputAttachmentLabel = Bild-Anhang
+sendPushsaferMessageActionInputAttachmentDescription = Lokaler Pfad oder URL zum Anhang.
+sendPushsaferMessageActionInputContentTypeLabel = Bild-Typ
+sendPushsaferMessageActionInputContentTypeDescription = Der Bild-Typ für den Anhang. Default: "jpeg", mögliche Werte "jpeg,png,gif".
+sendPushsaferMessageActionInputAuthenticationLabel = Authentifizierung
+sendPushsaferMessageActionInputAuthenticationDescription = Basisauthentifizierung für HTTP(S) Aufrufe. Default: "", Beispiel: "user:passwort".
+
+sendPushsaferPriorityMessageActionLabel = eine Prioritätsnachricht senden
+sendPushsaferPriorityMessageActionDescription = Action zum Versenden einer Prioritätsnachricht.
+sendPushsaferPriorityMessageActionOutputLabel = Receipt
+sendPushsaferPriorityMessageActionOutputDescription = ID der Prioritätsnachricht, wenn diese erfolgreich versendet wurde.
+sendPushsaferMessageActionInputPriorityLabel = Priorität
+sendPushsaferMessageActionInputPriorityDescription = Die Priorität. Default: 2.
+
+cancelPushsaferPriorityMessageActionLabel = eine Prioritätsnachricht annullieren
+cancelPushsaferPriorityMessageActionDescription = Action zum Annullieren einer Prioritätsnachricht.
+cancelPushsaferPriorityMessageActionOnputLabel = Annulliert
+cancelPushsaferPriorityMessageActionOnputDescription = true, wenn die Prioritätsnachricht erfolgreich annulliert wurde.
+cancelPushsaferPriorityMessageActionInputReceiptLabel = Receipt
+cancelPushsaferPriorityMessageActionInputReceiptDescription = Die ID der Prioritätsnachricht.
+
+sendPushsaferMessageToDeviceActionLabel = eine Nachricht an ein Endgerät
+sendPushsaferMessageToDeviceActionDescription = Action zum Versenden einer Nachricht an ein Endgerät.
+sendPushsaferMessageActionInputDeviceLabel = Endgerät
+sendPushsaferMessageActionInputDeviceDescription = Der Name des Endgeräts (mehrere Geräte können durch ein Komma getrennt werden).
diff --git a/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.pushsafer/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..fb61639
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="pushsafer"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="pushsafer-account">
+               <label>Pushsafer Account</label>
+               <description>Provides access to the Pushsafer Messages API.</description>
+
+               <representation-property>apikey</representation-property>
+
+               <config-description-ref uri="thing-type:pushsafer:pushsafer-account"/>
+       </thing-type>
+
+</thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.pushsafer/src/test/java/org/openhab/binding/pushsafer/internal/actions/PushsaferActionsTest.java b/bundles/org.openhab.binding.pushsafer/src/test/java/org/openhab/binding/pushsafer/internal/actions/PushsaferActionsTest.java
new file mode 100644 (file)
index 0000000..b425181
--- /dev/null
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.pushsafer.internal.actions;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.binding.pushsafer.internal.connection.PushsaferConfigurationException;
+import org.openhab.binding.pushsafer.internal.connection.PushsaferMessageBuilder;
+import org.openhab.binding.pushsafer.internal.handler.PushsaferAccountHandler;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingHandler;
+
+/**
+ * Unit tests for {@link PushsaferActions}.
+ *
+ * @author Kevin Siml - Initial contribution, forked from Christoph Weitkamp
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.WARN)
+public class PushsaferActionsTest {
+
+    private static final String MESSAGE = "My Message";
+    private static final String TITLE = "My Title";
+    private static final String URL = "https://www.test.com";
+    private static final String URL_TITLE = "Some Link";
+    private static final String RECEIPT = "12345";
+
+    @NonNullByDefault
+    private final ThingActions thingActionsStub = new ThingActions() {
+        @Override
+        public void setThingHandler(ThingHandler handler) {
+        }
+
+        @Override
+        public @Nullable ThingHandler getThingHandler() {
+            return null;
+        }
+    };
+
+    private @Mock PushsaferAccountHandler mockPushsaferAccountHandler;
+
+    private PushsaferActions pushsaferThingActions;
+
+    @BeforeEach
+    public void setUp() throws PushsaferConfigurationException {
+        pushsaferThingActions = new PushsaferActions();
+
+        when(mockPushsaferAccountHandler.getDefaultPushsaferMessageBuilder(any()))
+                .thenReturn(PushsaferMessageBuilder.getInstance("key", "user"));
+        when(mockPushsaferAccountHandler.sendPushsaferMessage(any())).thenReturn(Boolean.TRUE);
+        when(mockPushsaferAccountHandler.sendPushsaferPriorityMessage(any())).thenReturn(RECEIPT);
+    }
+
+    // sendPushsaferMessage
+    @Test
+    public void testSendMessageThingActionsIsNotPushsaferThingActions() {
+        assertThrows(ClassCastException.class,
+                () -> PushsaferActions.sendPushsaferMessage(thingActionsStub, MESSAGE, TITLE));
+    }
+
+    @Test
+    public void testSendMessageThingHandlerIsNull() {
+        assertThrows(RuntimeException.class,
+                () -> PushsaferActions.sendPushsaferMessage(pushsaferThingActions, MESSAGE, TITLE));
+    }
+
+    @Test
+    public void testSendMessageWithoutTitle() {
+        pushsaferThingActions.setThingHandler(mockPushsaferAccountHandler);
+        boolean sent = PushsaferActions.sendPushsaferMessage(pushsaferThingActions, MESSAGE, null);
+        assertThat(sent, is(true));
+    }
+
+    @Test
+    public void testSendMessage() {
+        pushsaferThingActions.setThingHandler(mockPushsaferAccountHandler);
+        boolean sent = PushsaferActions.sendPushsaferMessage(pushsaferThingActions, MESSAGE, TITLE);
+        assertThat(sent, is(true));
+    }
+
+    // sendPushsaferURLMessage
+    @Test
+    public void testSendURLMessageThingActionsIsNotPushsaferThingActions() {
+        assertThrows(ClassCastException.class,
+                () -> PushsaferActions.sendPushsaferURLMessage(thingActionsStub, MESSAGE, TITLE, URL, URL_TITLE));
+    }
+
+    @Test
+    public void testSendURLMessageThingHandlerIsNull() {
+        assertThrows(RuntimeException.class,
+                () -> PushsaferActions.sendPushsaferURLMessage(pushsaferThingActions, MESSAGE, TITLE, URL, URL_TITLE));
+    }
+
+    @Test
+    public void testSendURLMessageWithoutTitle() {
+        pushsaferThingActions.setThingHandler(mockPushsaferAccountHandler);
+        boolean sent = PushsaferActions.sendPushsaferURLMessage(pushsaferThingActions, MESSAGE, null, URL, URL_TITLE);
+        assertThat(sent, is(true));
+    }
+
+    @Test
+    public void testSendURLMessageWithoutURLTitle() {
+        pushsaferThingActions.setThingHandler(mockPushsaferAccountHandler);
+        boolean sent = PushsaferActions.sendPushsaferURLMessage(pushsaferThingActions, MESSAGE, TITLE, URL, null);
+        assertThat(sent, is(true));
+    }
+
+    @Test
+    public void testSendURLMessage() {
+        pushsaferThingActions.setThingHandler(mockPushsaferAccountHandler);
+        boolean sent = PushsaferActions.sendPushsaferURLMessage(pushsaferThingActions, MESSAGE, TITLE, URL, URL_TITLE);
+        assertThat(sent, is(true));
+    }
+
+    // sendPushsaferPriorityMessage
+    @Test
+    public void testSendPriorityMessageThingActionsIsNotPushsaferThingActions() {
+        assertThrows(ClassCastException.class, () -> PushsaferActions.sendPushsaferPriorityMessage(thingActionsStub,
+                MESSAGE, TITLE, PushsaferMessageBuilder.EMERGENCY_PRIORITY));
+    }
+
+    @Test
+    public void testSendPriorityMessageThingHandlerIsNull() {
+        assertThrows(RuntimeException.class, () -> PushsaferActions.sendPushsaferPriorityMessage(pushsaferThingActions,
+                MESSAGE, TITLE, PushsaferMessageBuilder.EMERGENCY_PRIORITY));
+    }
+
+    @Test
+    public void testSendPriorityMessageWithoutTitle() {
+        pushsaferThingActions.setThingHandler(mockPushsaferAccountHandler);
+        String receipt = PushsaferActions.sendPushsaferPriorityMessage(pushsaferThingActions, MESSAGE, null,
+                PushsaferMessageBuilder.EMERGENCY_PRIORITY);
+        assertThat(receipt, is(RECEIPT));
+    }
+
+    @Test
+    public void testSendPriorityMessage() {
+        pushsaferThingActions.setThingHandler(mockPushsaferAccountHandler);
+        String receipt = PushsaferActions.sendPushsaferPriorityMessage(pushsaferThingActions, MESSAGE, TITLE,
+                PushsaferMessageBuilder.EMERGENCY_PRIORITY);
+        assertThat(receipt, is(RECEIPT));
+    }
+}
index d890661143031ebf4fa3273be8c548f1eb27039a..20db7d23116287203154d9e85cc2bf237ee18c03 100644 (file)
     <module>org.openhab.binding.pulseaudio</module>
     <module>org.openhab.binding.pushbullet</module>
     <module>org.openhab.binding.pushover</module>
-    <module>org.openhab.binding.qbus</module>
+    <module>org.openhab.binding.pushsafer</module>
     <module>org.openhab.binding.radiothermostat</module>
     <module>org.openhab.binding.regoheatpump</module>
     <module>org.openhab.binding.revogi</module>