diff --git a/CODEOWNERS b/CODEOWNERS
index 9379686ef47..3f6e50d0fce 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -201,6 +201,7 @@
/bundles/org.openhab.binding.powermax/ @lolodomo
/bundles/org.openhab.binding.pulseaudio/ @peuter
/bundles/org.openhab.binding.pushbullet/ @hakan42
+/bundles/org.openhab.binding.pushover/ @cweitkamp
/bundles/org.openhab.binding.radiothermostat/ @mlobstein
/bundles/org.openhab.binding.regoheatpump/ @crnjan
/bundles/org.openhab.binding.revogi/ @andibraeu
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index ea69d3c0c10..8c53914220e 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -996,6 +996,11 @@
org.openhab.binding.pushbullet
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.pushover
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.radiothermostat
diff --git a/bundles/org.openhab.binding.pushover/NOTICE b/bundles/org.openhab.binding.pushover/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/NOTICE
@@ -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.pushover/README.md b/bundles/org.openhab.binding.pushover/README.md
new file mode 100644
index 00000000000..9d18d8b4da6
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/README.md
@@ -0,0 +1,78 @@
+# Pushover Binding
+
+The Pushover binding allows you to notify mobile devices of a message using the [Pushover REST API](https://pushover.net/api).
+To get started you first need to register (a free process) to get an API token.
+Initially you have to create an application, set its name and optionally upload an icon, and get the API token in return.
+Once you have the token, you need a user key (or group key) and optionally a device name for each user to which you want to push notifications.
+
+## Supported Things
+
+There is only one Thing available - the `pushover-account`.
+You are able to create multiple instances of this Thing to broadcast to different users, groups or devices.
+
+## Thing Configuration
+
+| Configuration Parameter | Type | Description |
+|-------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `apikey` | text | Your API token / key (APP_TOKEN) to access the Pushover Message API. **mandatory** |
+| `user` | text | Your user key or group key (USER_KEY) to which 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: `default`) (see [supported notification sounds](https://pushover.net/api#sounds)). |
+| `retry` | integer | The retry parameter specifies how often (in seconds) the Pushover 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** |
+
+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 - parameter `message` **mandatory** - was sent successfully or not.
+The `title` parameter defaults to whatever value you defined in the `title` related configuration parameter.
+
+`sendMessage(String message, @Nullable String title)` - This method is used to send a plain text message.
+`sendHtmlMessage(String message, @Nullable String title)` - This method is used to send a HTML message.
+`sendMonospaceMessage(String message, @Nullable String title)` - This method is used to send a monospace message.
+`sendAttachmentMessage(String message, @Nullable String title, String attachment, @Nullable String contentType)` - This method is used to send a message with an attachment. It takes a (local) path (`attachment` **mandatory**) to the attachment and an optional parameter `contentType` to define the content-type of the attachment (default: `image/jpeg`).
+`sendURLMessage(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.
+`sendMessageToDevice(String device, String message, @Nullable String title)` - This method is used to send a message to a specific device. Parameter `device` **mandatory** is the name of a specific device (multiple devices may be separated by a comma).
+
+The `sendPriorityMessage` action returns a `String` value (the `receipt`) if the message was sent successfully, otherwise `null`.
+
+`sendPriorityMessage(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`).
+
+`cancelPriorityMessage` returns a `Boolean` value to indicate if the message was cancelled successfully or not.
+
+`cancelPriorityMessage(String receipt)` - This method is used to cancel a priority message.
+
+## Full Example
+
+demo.things:
+
+```java
+Thing pushover:pushover-account:account [ apikey="APP_TOKEN", user="USER_KEY" ]
+```
+
+demo.rules:
+
+```java
+val actions = getActions("pushover", "pushover:pushover-account:account")
+// send HTML message
+actions.sendHtmlMessage("Hello World!", "openHAB")
+```
+
+```java
+val actions = getActions("pushover", "pushover:pushover-account:account")
+// send priority message
+var receipt = actions.sendPriorityMessage("Hello World!", "openHAB", 3)
+
+// wait for your cancel condition
+
+if( receipt !== null ) {
+ actions.cancelPriorityMessage(receipt)
+ receipt = null
+}
+```
diff --git a/bundles/org.openhab.binding.pushover/pom.xml b/bundles/org.openhab.binding.pushover/pom.xml
new file mode 100644
index 00000000000..fbaa79eb07e
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.0.0-SNAPSHOT
+
+
+ org.openhab.binding.pushover
+
+ openHAB Add-ons :: Bundles :: Pushover Binding
+
+
diff --git a/bundles/org.openhab.binding.pushover/src/main/feature/feature.xml b/bundles/org.openhab.binding.pushover/src/main/feature/feature.xml
new file mode 100644
index 00000000000..e3d87f0545b
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.pushover/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/PushoverBindingConstants.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/PushoverBindingConstants.java
new file mode 100644
index 00000000000..0ad29960ea2
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/PushoverBindingConstants.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link PushoverBindingConstants} class defines common constants, which are used across the whole binding.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@NonNullByDefault
+public class PushoverBindingConstants {
+
+ private static final String BINDING_ID = "pushover";
+
+ public static final ThingTypeUID PUSHOVER_ACCOUNT = new ThingTypeUID(BINDING_ID, "pushover-account");
+
+ public static final String CONFIG_SOUND = "sound";
+
+ public static final String DEFAULT_SOUND = "default";
+ public static final String DEFAULT_TITLE = "openHAB";
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/actions/PushoverActions.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/actions/PushoverActions.java
new file mode 100644
index 00000000000..85113164932
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/actions/PushoverActions.java
@@ -0,0 +1,220 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal.actions;
+
+import static org.openhab.binding.pushover.internal.PushoverBindingConstants.DEFAULT_TITLE;
+import static org.openhab.binding.pushover.internal.connection.PushoverMessageBuilder.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pushover.internal.connection.PushoverMessageBuilder;
+import org.openhab.binding.pushover.internal.handler.PushoverAccountHandler;
+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 PushoverAccountHandler}.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@ThingActionsScope(name = "pushover")
+@NonNullByDefault
+public class PushoverActions implements ThingActions {
+
+ private static final String DEFAULT_EMERGENCY_PRIORITY = "2";
+
+ private final Logger logger = LoggerFactory.getLogger(PushoverActions.class);
+
+ private @NonNullByDefault({}) PushoverAccountHandler accountHandler;
+
+ @RuleAction(label = "@text/sendMessageActionLabel", description = "@text/sendMessageActionDescription")
+ public @ActionOutput(name = "sent", label = "@text/sendMessageActionOutputLabel", description = "@text/sendMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendMessage(
+ @ActionInput(name = "message", label = "@text/sendMessageActionInputMessageLabel", description = "@text/sendMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+ @ActionInput(name = "title", label = "@text/sendMessageActionInputTitleLabel", description = "@text/sendMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title) {
+ logger.trace("ThingAction 'sendMessage' called with value(s): message='{}', title='{}'", message, title);
+ return send(getDefaultPushoverMessageBuilder(message), title);
+ }
+
+ public static Boolean sendMessage(ThingActions actions, String message, @Nullable String title) {
+ return ((PushoverActions) actions).sendMessage(message, title);
+ }
+
+ @RuleAction(label = "@text/sendURLMessageActionLabel", description = "@text/sendURLMessageActionDescription")
+ public @ActionOutput(name = "sent", label = "@text/sendMessageActionOutputLabel", description = "@text/sendMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendURLMessage(
+ @ActionInput(name = "message", label = "@text/sendMessageActionInputMessageLabel", description = "@text/sendMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+ @ActionInput(name = "title", label = "@text/sendMessageActionInputTitleLabel", description = "@text/sendMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title,
+ @ActionInput(name = "url", label = "@text/sendMessageActionInputURLLabel", description = "@text/sendMessageActionInputURLDescription", type = "java.lang.String", required = true) String url,
+ @ActionInput(name = "urlTitle", label = "@text/sendMessageActionInputURLTitleLabel", description = "@text/sendMessageActionInputURLTitleDescription", type = "java.lang.String") @Nullable String urlTitle) {
+ logger.trace(
+ "ThingAction 'sendURLMessage' 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.");
+ }
+
+ PushoverMessageBuilder builder = getDefaultPushoverMessageBuilder(message).withUrl(url);
+ if (urlTitle != null) {
+ builder.withUrl(urlTitle);
+ }
+ return send(builder, title);
+ }
+
+ public static Boolean sendURLMessage(ThingActions actions, String message, @Nullable String title, String url,
+ @Nullable String urlTitle) {
+ return ((PushoverActions) actions).sendURLMessage(message, title, url, urlTitle);
+ }
+
+ @RuleAction(label = "@text/sendHTMLMessageActionLabel", description = "@text/sendHTMLMessageActionDescription")
+ public @ActionOutput(name = "sent", label = "@text/sendMessageActionOutputLabel", description = "@text/sendMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendHtmlMessage(
+ @ActionInput(name = "message", label = "@text/sendMessageActionInputMessageLabel", description = "@text/sendMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+ @ActionInput(name = "title", label = "@text/sendMessageActionInputTitleLabel", description = "@text/sendMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title) {
+ logger.trace("ThingAction 'sendHtmlMessage' called with value(s): message='{}', title='{}'", message, title);
+ return send(getDefaultPushoverMessageBuilder(message).withHtmlFormatting(), title);
+ }
+
+ public static Boolean sendHtmlMessage(ThingActions actions, String message, @Nullable String title) {
+ return ((PushoverActions) actions).sendHtmlMessage(message, title);
+ }
+
+ @RuleAction(label = "@text/sendMonospaceMessageActionLabel", description = "@text/sendMonospaceMessageActionDescription")
+ public @ActionOutput(name = "sent", label = "@text/sendMessageActionOutputLabel", description = "@text/sendMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendMonospaceMessage(
+ @ActionInput(name = "message", label = "@text/sendMessageActionInputMessageLabel", description = "@text/sendMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+ @ActionInput(name = "title", label = "@text/sendMessageActionInputTitleLabel", description = "@text/sendMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title) {
+ logger.trace("ThingAction 'sendMonospaceMessage' called with value(s): message='{}', title='{}'", message,
+ title);
+ return send(getDefaultPushoverMessageBuilder(message).withMonospaceFormatting(), title);
+ }
+
+ public static Boolean sendMonospaceMessage(ThingActions actions, String message, @Nullable String title) {
+ return ((PushoverActions) actions).sendMonospaceMessage(message, title);
+ }
+
+ @RuleAction(label = "@text/sendAttachmentMessageActionLabel", description = "@text/sendAttachmentMessageActionDescription")
+ public @ActionOutput(name = "sent", label = "@text/sendMessageActionOutputLabel", description = "@text/sendMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendAttachmentMessage(
+ @ActionInput(name = "message", label = "@text/sendMessageActionInputMessageLabel", description = "@text/sendMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+ @ActionInput(name = "title", label = "@text/sendMessageActionInputTitleLabel", description = "@text/sendMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title,
+ @ActionInput(name = "attachment", label = "@text/sendMessageActionInputAttachmentLabel", description = "@text/sendMessageActionInputAttachmentDescription", type = "java.lang.String", required = true) String attachment,
+ @ActionInput(name = "contentType", label = "@text/sendMessageActionInputContentTypeLabel", description = "@text/sendMessageActionInputContentTypeDescription", type = "java.lang.String", defaultValue = DEFAULT_CONTENT_TYPE) @Nullable String contentType) {
+ logger.trace(
+ "ThingAction 'sendAttachmentMessage' called with value(s): message='{}', title='{}', attachment='{}', contentType='{}'",
+ message, title, attachment, contentType);
+ if (attachment == null) {
+ throw new IllegalArgumentException("Skip sending message as 'attachment' is null.");
+ }
+
+ PushoverMessageBuilder builder = getDefaultPushoverMessageBuilder(message).withAttachment(attachment);
+ if (contentType != null) {
+ builder.withContentType(contentType);
+ }
+ return send(builder, title);
+ }
+
+ public static Boolean sendAttachmentMessage(ThingActions actions, String message, @Nullable String title,
+ String attachment, @Nullable String contentType) {
+ return ((PushoverActions) actions).sendAttachmentMessage(message, title, attachment, contentType);
+ }
+
+ @RuleAction(label = "@text/sendPriorityMessageActionLabel", description = "@text/sendPriorityMessageActionDescription")
+ public @ActionOutput(name = "receipt", label = "@text/sendPriorityMessageActionOutputLabel", description = "@text/sendPriorityMessageActionOutputDescription", type = "java.lang.String") String sendPriorityMessage(
+ @ActionInput(name = "message", label = "@text/sendMessageActionInputMessageLabel", description = "@text/sendMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+ @ActionInput(name = "title", label = "@text/sendMessageActionInputTitleLabel", description = "@text/sendMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title,
+ @ActionInput(name = "priority", label = "@text/sendMessageActionInputPriorityLabel", description = "@text/sendMessageActionInputPriorityDescription", type = "java.lang.Integer", defaultValue = DEFAULT_EMERGENCY_PRIORITY) @Nullable Integer priority) {
+ logger.trace("ThingAction 'sendPriorityMessage' called with value(s): message='{}', title='{}', priority='{}'",
+ message, title, priority);
+ PushoverMessageBuilder builder = getDefaultPushoverMessageBuilder(message)
+ .withPriority(priority == null ? EMERGENCY_PRIORITY : priority.intValue());
+
+ if (title != null) {
+ builder.withTitle(title);
+ }
+ return accountHandler.sendPriorityMessage(builder);
+ }
+
+ public static String sendPriorityMessage(ThingActions actions, String message, @Nullable String title,
+ @Nullable Integer priority) {
+ return ((PushoverActions) actions).sendPriorityMessage(message, title, priority);
+ }
+
+ @RuleAction(label = "@text/cancelPriorityMessageActionLabel", description = "@text/cancelPriorityMessageActionDescription")
+ public @ActionOutput(name = "canceled", label = "@text/cancelPriorityMessageActionOutputLabel", description = "@text/cancelPriorityMessageActionOutputDescription", type = "java.lang.Boolean") Boolean cancelPriorityMessage(
+ @ActionInput(name = "receipt", label = "@text/cancelPriorityMessageActionInputReceiptLabel", description = "@text/cancelPriorityMessageActionInputReceiptDescription", type = "java.lang.String", required = true) String receipt) {
+ logger.trace("ThingAction 'cancelPriorityMessage' called with value(s): '{}'", receipt);
+ if (accountHandler == null) {
+ throw new RuntimeException("PushoverAccountHandler is null!");
+ }
+
+ if (receipt == null) {
+ throw new IllegalArgumentException("Skip canceling message as 'receipt' is null.");
+ }
+
+ return accountHandler.cancelPriorityMessage(receipt);
+ }
+
+ public static Boolean cancelPriorityMessage(ThingActions actions, String receipt) {
+ return ((PushoverActions) actions).cancelPriorityMessage(receipt);
+ }
+
+ @RuleAction(label = "@text/sendMessageToDeviceActionLabel", description = "@text/sendMessageToDeviceActionDescription")
+ public @ActionOutput(name = "sent", label = "@text/sendMessageActionOutputLabel", description = "@text/sendMessageActionOutputDescription", type = "java.lang.Boolean") Boolean sendMessageToDevice(
+ @ActionInput(name = "device", label = "@text/sendMessageActionInputDeviceLabel", description = "@text/sendMessageActionInputDeviceDescription", type = "java.lang.String", required = true) String device,
+ @ActionInput(name = "message", label = "@text/sendMessageActionInputMessageLabel", description = "@text/sendMessageActionInputMessageDescription", type = "java.lang.String", required = true) String message,
+ @ActionInput(name = "title", label = "@text/sendMessageActionInputTitleLabel", description = "@text/sendMessageActionInputTitleDescription", type = "java.lang.String", defaultValue = DEFAULT_TITLE) @Nullable String title) {
+ logger.trace("ThingAction 'sendMessageToDevice' 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(getDefaultPushoverMessageBuilder(message).withDevice(device), title);
+ }
+
+ public static Boolean sendMessageToDevice(ThingActions actions, String device, String message,
+ @Nullable String title) {
+ return ((PushoverActions) actions).sendMessageToDevice(device, message, title);
+ }
+
+ private PushoverMessageBuilder getDefaultPushoverMessageBuilder(String message) {
+ if (accountHandler == null) {
+ throw new RuntimeException("PushoverAccountHandler is null!");
+ }
+
+ if (message == null) {
+ throw new IllegalArgumentException("Skip sending message as 'message' is null.");
+ }
+
+ return accountHandler.getDefaultPushoverMessageBuilder(message);
+ }
+
+ private Boolean send(PushoverMessageBuilder builder, @Nullable String title) {
+ if (title != null) {
+ builder.withTitle(title);
+ }
+ return accountHandler.sendMessage(builder);
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ this.accountHandler = (PushoverAccountHandler) handler;
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return accountHandler;
+ }
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/config/PushoverAccountConfiguration.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/config/PushoverAccountConfiguration.java
new file mode 100644
index 00000000000..2767e5d5be2
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/config/PushoverAccountConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal.config;
+
+import static org.openhab.binding.pushover.internal.PushoverBindingConstants.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link PushoverAccountConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@NonNullByDefault
+public class PushoverAccountConfiguration {
+ public @Nullable String apikey;
+ public @Nullable String user;
+ public String title = DEFAULT_TITLE;
+ public String format = "none";
+ public String sound = DEFAULT_SOUND;
+ public int retry = 300;
+ public int expire = 3600;
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/config/PushoverConfigOptionProvider.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/config/PushoverConfigOptionProvider.java
new file mode 100644
index 00000000000..fc76eab2355
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/config/PushoverConfigOptionProvider.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal.config;
+
+import static org.openhab.binding.pushover.internal.PushoverBindingConstants.*;
+
+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.pushover.internal.dto.Sound;
+import org.openhab.binding.pushover.internal.handler.PushoverAccountHandler;
+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 PushoverConfigOptionProvider} class contains fields mapping thing configuration parameters.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = ConfigOptionProvider.class)
+public class PushoverConfigOptionProvider implements ConfigOptionProvider, ThingHandlerService {
+
+ private @Nullable PushoverAccountHandler accountHandler;
+
+ @Override
+ public @Nullable Collection getParameterOptions(URI uri, String param, @Nullable String context,
+ @Nullable Locale locale) {
+ if (accountHandler != null && PUSHOVER_ACCOUNT.getAsString().equals(uri.getSchemeSpecificPart())
+ && CONFIG_SOUND.equals(param)) {
+ List sounds = accountHandler.getSounds();
+ if (!sounds.isEmpty()) {
+ return sounds.stream().map(Sound::getAsParameterOption)
+ .sorted(Comparator.comparing(ParameterOption::getLabel))
+ .collect(Collectors.toUnmodifiableList());
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void setThingHandler(@Nullable ThingHandler handler) {
+ this.accountHandler = (PushoverAccountHandler) handler;
+ }
+
+ @Override
+ public @Nullable ThingHandler getThingHandler() {
+ return accountHandler;
+ }
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverAPIConnection.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverAPIConnection.java
new file mode 100644
index 00000000000..4d75a60f768
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverAPIConnection.java
@@ -0,0 +1,193 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal.connection;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+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.pushover.internal.config.PushoverAccountConfiguration;
+import org.openhab.binding.pushover.internal.dto.Sound;
+import org.openhab.core.cache.ExpiringCacheMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+/**
+ * The {@link PushoverAPIConnection} is responsible for handling the connections to Pushover Messages API.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@NonNullByDefault
+public class PushoverAPIConnection {
+
+ private final Logger logger = LoggerFactory.getLogger(PushoverAPIConnection.class);
+
+ private static final String VALIDATE_URL = "https://api.pushover.net/1/users/validate.json";
+ private static final String MESSAGE_URL = "https://api.pushover.net/1/messages.json";
+ private static final String CANCEL_MESSAGE_URL = "https://api.pushover.net/1/receipts/{receipt}/cancel.json";
+ private static final String SOUNDS_URL = "https://api.pushover.net/1/sounds.json";
+
+ private final HttpClient httpClient;
+ private final PushoverAccountConfiguration config;
+
+ private final ExpiringCacheMap cache = new ExpiringCacheMap<>(TimeUnit.DAYS.toMillis(1));
+
+ private final JsonParser parser = new JsonParser();
+
+ public PushoverAPIConnection(HttpClient httpClient, PushoverAccountConfiguration config) {
+ this.httpClient = httpClient;
+ this.config = config;
+ }
+
+ public boolean validateUser() throws PushoverCommunicationException, PushoverConfigurationException {
+ return getMessageStatus(
+ post(VALIDATE_URL, PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
+ }
+
+ public boolean sendMessage(PushoverMessageBuilder message)
+ throws PushoverCommunicationException, PushoverConfigurationException {
+ return getMessageStatus(post(MESSAGE_URL, message.build()));
+ }
+
+ public String sendPriorityMessage(PushoverMessageBuilder message)
+ throws PushoverCommunicationException, PushoverConfigurationException {
+ final JsonObject json = parser.parse(post(MESSAGE_URL, message.build())).getAsJsonObject();
+ return getMessageStatus(json) && json.has("receipt") ? json.get("receipt").getAsString() : "";
+ }
+
+ public boolean cancelPriorityMessage(String receipt)
+ throws PushoverCommunicationException, PushoverConfigurationException {
+ return getMessageStatus(post(CANCEL_MESSAGE_URL.replace("{receipt}", receipt),
+ PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
+ }
+
+ public List getSounds() throws PushoverCommunicationException, PushoverConfigurationException {
+ final String localApikey = config.apikey;
+ if (localApikey == null || localApikey.isEmpty()) {
+ throw new PushoverConfigurationException("@text/offline.conf-error-missing-apikey");
+ }
+
+ final Map params = new HashMap<>(1);
+ params.put(PushoverMessageBuilder.MESSAGE_KEY_TOKEN, localApikey);
+
+ // TODO do not cache the response, cache the parsed list of sounds
+ final JsonObject json = parser.parse(getFromCache(buildURL(SOUNDS_URL, params))).getAsJsonObject();
+ if (json.has("sounds")) {
+ final JsonObject sounds = json.get("sounds").getAsJsonObject();
+ if (sounds != null) {
+ return Collections.unmodifiableList(sounds.entrySet().stream()
+ .map(entry -> new Sound(entry.getKey(), entry.getValue().getAsString()))
+ .collect(Collectors.toList()));
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ private String buildURL(String url, Map 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) {
+ return cache.putIfAbsentAndGet(url, () -> get(url));
+ }
+
+ private String get(String url) throws PushoverCommunicationException, PushoverConfigurationException {
+ return executeRequest(HttpMethod.GET, url, null);
+ }
+
+ private String post(String url, ContentProvider body)
+ throws PushoverCommunicationException, PushoverConfigurationException {
+ return executeRequest(HttpMethod.POST, url, body);
+ }
+
+ private String executeRequest(HttpMethod httpMethod, String url, @Nullable ContentProvider body)
+ throws PushoverCommunicationException, PushoverConfigurationException {
+ logger.trace("Pushover request: {} - URL = '{}'", httpMethod, url);
+ try {
+ final Request request = httpClient.newRequest(url).method(httpMethod).timeout(10, TimeUnit.SECONDS);
+
+ if (body != null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Pushover request body: '{}'", body);
+ }
+ request.content(body);
+ }
+
+ final ContentResponse contentResponse = request.send();
+
+ final int httpStatus = contentResponse.getStatus();
+ final String content = contentResponse.getContentAsString();
+ logger.trace("Pushover response: status = {}, content = '{}'", httpStatus, content);
+ switch (httpStatus) {
+ case HttpStatus.OK_200:
+ return content;
+ case HttpStatus.BAD_REQUEST_400:
+ logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
+ throw new PushoverConfigurationException(getMessageError(content));
+ default:
+ logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
+ throw new PushoverCommunicationException(content);
+ }
+ } catch (ExecutionException e) {
+ logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
+ throw new PushoverCommunicationException(e.getLocalizedMessage(), e.getCause());
+ } catch (InterruptedException | TimeoutException e) {
+ logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
+ throw new PushoverCommunicationException(e.getLocalizedMessage());
+ }
+ }
+
+ private String getMessageError(String content) {
+ final JsonObject json = parser.parse(content).getAsJsonObject();
+ if (json.has("errors")) {
+ final JsonArray errors = json.get("errors").getAsJsonArray();
+ if (errors != null) {
+ return errors.toString();
+ }
+ }
+ return "Unknown error occured.";
+ }
+
+ private boolean getMessageStatus(String content) {
+ final JsonObject json = parser.parse(content).getAsJsonObject();
+ return json.has("status") ? json.get("status").getAsInt() == 1 : false;
+ }
+
+ private boolean getMessageStatus(JsonObject json) {
+ return json.has("status") ? json.get("status").getAsInt() == 1 : false;
+ }
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverCommunicationException.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverCommunicationException.java
new file mode 100644
index 00000000000..44d26aa5a14
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverCommunicationException.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link PushoverCommunicationException} is a configuration exception for the connections to Pushover Messages API.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@NonNullByDefault
+public class PushoverCommunicationException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with null as its detail message.
+ */
+ public PushoverCommunicationException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message Detail message
+ */
+ public PushoverCommunicationException(@Nullable String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause.
+ *
+ * @param cause The cause
+ */
+ public PushoverCommunicationException(@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 PushoverCommunicationException(@Nullable String message, @Nullable Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverConfigurationException.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverConfigurationException.java
new file mode 100644
index 00000000000..cc15122fd8d
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverConfigurationException.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link PushoverConfigurationException} is a configuration exception for the connections to Pushover Messages API.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@NonNullByDefault
+public class PushoverConfigurationException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with null as its detail message.
+ */
+ public PushoverConfigurationException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message Detail message
+ */
+ public PushoverConfigurationException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause.
+ *
+ * @param cause The cause
+ */
+ public PushoverConfigurationException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * @param message Detail message
+ * @param cause The cause
+ */
+ public PushoverConfigurationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverMessageBuilder.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverMessageBuilder.java
new file mode 100644
index 00000000000..878b12ab970
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/connection/PushoverMessageBuilder.java
@@ -0,0 +1,263 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal.connection;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+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.PathContentProvider;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link PushoverMessageBuilder} builds the body for Pushover Messages API requests.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@NonNullByDefault
+public class PushoverMessageBuilder {
+
+ private final Logger logger = LoggerFactory.getLogger(PushoverMessageBuilder.class);
+
+ public static final String MESSAGE_KEY_TOKEN = "token";
+ private static final String MESSAGE_KEY_USER = "user";
+ private static final String MESSAGE_KEY_MESSAGE = "message";
+ private static final String MESSAGE_KEY_TITLE = "title";
+ private static final String MESSAGE_KEY_DEVICE = "device";
+ private static final String MESSAGE_KEY_PRIORITY = "priority";
+ private static final String MESSAGE_KEY_RETRY = "retry";
+ private static final String MESSAGE_KEY_EXPIRE = "expire";
+ private static final String MESSAGE_KEY_URL = "url";
+ private static final String MESSAGE_KEY_URL_TITLE = "url_title";
+ private static final String MESSAGE_KEY_SOUND = "sound";
+ private static final String MESSAGE_KEY_ATTACHMENT = "attachment";
+ public static final String MESSAGE_KEY_HTML = "html";
+ public static final String MESSAGE_KEY_MONOSPACE = "monospace";
+
+ private static final int MAX_MESSAGE_LENGTH = 1024;
+ private static final int MAX_TITLE_LENGTH = 250;
+ private static final int MAX_DEVICE_LENGTH = 25;
+ private static final List 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 = 30;
+ 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 = "image/jpeg";
+
+ 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 attachment;
+ private String contentType = DEFAULT_CONTENT_TYPE;
+ private boolean html = false;
+ private boolean monospace = false;
+
+ private PushoverMessageBuilder(String apikey, String user) throws PushoverConfigurationException {
+ body.addFieldPart(MESSAGE_KEY_TOKEN, new StringContentProvider(apikey), null);
+ body.addFieldPart(MESSAGE_KEY_USER, new StringContentProvider(user), null);
+ }
+
+ public static PushoverMessageBuilder getInstance(@Nullable String apikey, @Nullable String user)
+ throws PushoverConfigurationException {
+ if (apikey == null || apikey.isEmpty()) {
+ throw new PushoverConfigurationException("@text/offline.conf-error-missing-apikey");
+ }
+
+ if (user == null || user.isEmpty()) {
+ throw new PushoverConfigurationException("@text/offline.conf-error-missing-user");
+ }
+
+ return new PushoverMessageBuilder(apikey, user);
+ }
+
+ public PushoverMessageBuilder withMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public PushoverMessageBuilder withTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public PushoverMessageBuilder withDevice(String device) {
+ this.device = device;
+ return this;
+ }
+
+ public PushoverMessageBuilder withPriority(int priority) {
+ this.priority = priority;
+ return this;
+ }
+
+ public PushoverMessageBuilder withRetry(int retry) {
+ this.retry = retry;
+ return this;
+ }
+
+ public PushoverMessageBuilder withExpire(int expire) {
+ this.expire = expire;
+ return this;
+ }
+
+ public PushoverMessageBuilder withUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ public PushoverMessageBuilder withUrlTitle(String urlTitle) {
+ this.urlTitle = urlTitle;
+ return this;
+ }
+
+ public PushoverMessageBuilder withSound(String sound) {
+ this.sound = sound;
+ return this;
+ }
+
+ public PushoverMessageBuilder withAttachment(String attachment) {
+ this.attachment = attachment;
+ return this;
+ }
+
+ public PushoverMessageBuilder withContentType(String contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
+ public PushoverMessageBuilder withHtmlFormatting() {
+ this.html = true;
+ return this;
+ }
+
+ public PushoverMessageBuilder withMonospaceFormatting() {
+ this.monospace = true;
+ return this;
+ }
+
+ public ContentProvider build() {
+ 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 (attachment != null) {
+ File file = new File(attachment);
+ if (!file.exists()) {
+ throw new IllegalArgumentException(
+ String.format("Skip sending the message as file '%s' does not exist.", attachment));
+ }
+ try {
+ body.addFilePart(MESSAGE_KEY_ATTACHMENT, file.getName(),
+ new PathContentProvider(contentType, file.toPath()), null);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(String.format("Skip sending the message: %s", e.getMessage()));
+ }
+ }
+
+ if (html) {
+ body.addFieldPart(MESSAGE_KEY_HTML, new StringContentProvider("1"), null);
+ } else if (monospace) {
+ body.addFieldPart(MESSAGE_KEY_MONOSPACE, new StringContentProvider("1"), null);
+ }
+
+ return body;
+ }
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/dto/Sound.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/dto/Sound.java
new file mode 100644
index 00000000000..fb326c690bb
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/dto/Sound.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.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 Pushover API request..
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@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.pushover/src/main/java/org/openhab/binding/pushover/internal/factory/PushoverHandlerFactory.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/factory/PushoverHandlerFactory.java
new file mode 100644
index 00000000000..dfdd4b95a5b
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/factory/PushoverHandlerFactory.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal.factory;
+
+import static org.openhab.binding.pushover.internal.PushoverBindingConstants.PUSHOVER_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.pushover.internal.handler.PushoverAccountHandler;
+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 PushoverHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@Component(configurationPid = "binding.pushover", service = ThingHandlerFactory.class)
+@NonNullByDefault
+public class PushoverHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(PUSHOVER_ACCOUNT);
+
+ private final HttpClient httpClient;
+
+ @Activate
+ public PushoverHandlerFactory(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 (PUSHOVER_ACCOUNT.equals(thingTypeUID)) {
+ return new PushoverAccountHandler(thing, httpClient);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/handler/PushoverAccountHandler.java b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/handler/PushoverAccountHandler.java
new file mode 100644
index 00000000000..1fa8e075a0b
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/java/org/openhab/binding/pushover/internal/handler/PushoverAccountHandler.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.internal.handler;
+
+import static org.openhab.binding.pushover.internal.PushoverBindingConstants.DEFAULT_SOUND;
+
+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.pushover.internal.actions.PushoverActions;
+import org.openhab.binding.pushover.internal.config.PushoverAccountConfiguration;
+import org.openhab.binding.pushover.internal.config.PushoverConfigOptionProvider;
+import org.openhab.binding.pushover.internal.connection.PushoverAPIConnection;
+import org.openhab.binding.pushover.internal.connection.PushoverCommunicationException;
+import org.openhab.binding.pushover.internal.connection.PushoverConfigurationException;
+import org.openhab.binding.pushover.internal.connection.PushoverMessageBuilder;
+import org.openhab.binding.pushover.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 PushoverAccountHandler} is responsible for handling commands, which are sent to one of the channels.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@NonNullByDefault
+public class PushoverAccountHandler extends BaseThingHandler {
+
+ private static final Collection> SUPPORTED_THING_ACTIONS = Set
+ .of(PushoverActions.class, PushoverConfigOptionProvider.class);
+
+ private final HttpClient httpClient;
+
+ private @NonNullByDefault({}) PushoverAccountConfiguration config;
+ private @Nullable PushoverAPIConnection connection;
+
+ public PushoverAccountHandler(Thing thing, HttpClient httpClient) {
+ super(thing);
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ // nothing
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(PushoverAccountConfiguration.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 PushoverAPIConnection(httpClient, config);
+ scheduler.submit(this::asyncValidateUser);
+ }
+ }
+
+ @Override
+ public Collection> getServices() {
+ return SUPPORTED_THING_ACTIONS;
+ }
+
+ /**
+ * Retrieves the list of current sounds and their descriptions from the Pushover API.
+ *
+ * @return a list of {@link Sound}s
+ */
+ public List getSounds() {
+ return connection != null ? connection.getSounds() : List.of();
+ }
+
+ /**
+ * Returns a preconfigured {@link PushoverMessageBuilder}.
+ *
+ * @param message the message
+ * @return a {@link PushoverMessageBuilder} instance
+ */
+ public PushoverMessageBuilder getDefaultPushoverMessageBuilder(String message) {
+ PushoverMessageBuilder builder = PushoverMessageBuilder.getInstance(config.apikey, config.user)
+ .withMessage(message) //
+ .withTitle(config.title) //
+ .withRetry(config.retry) //
+ .withExpire(config.expire);
+ // specify format if defined
+ switch (config.format) {
+ case PushoverMessageBuilder.MESSAGE_KEY_HTML:
+ builder.withHtmlFormatting();
+ break;
+ case PushoverMessageBuilder.MESSAGE_KEY_MONOSPACE:
+ builder.withMonospaceFormatting();
+ default:
+ break;
+ }
+ // add sound if defined
+ if (!DEFAULT_SOUND.equals(config.sound)) {
+ builder.withSound(config.sound);
+ }
+ return builder;
+ }
+
+ public boolean sendMessage(PushoverMessageBuilder messageBuilder) {
+ if (connection != null) {
+ return connection.sendMessage(messageBuilder);
+ } else {
+ throw new IllegalArgumentException("PushoverAPIConnection is null!");
+ }
+ }
+
+ public String sendPriorityMessage(PushoverMessageBuilder messageBuilder) {
+ if (connection != null) {
+ return connection.sendPriorityMessage(messageBuilder);
+ } else {
+ throw new IllegalArgumentException("PushoverAPIConnection is null!");
+ }
+ }
+
+ public boolean cancelPriorityMessage(String receipt) {
+ if (connection != null) {
+ return connection.cancelPriorityMessage(receipt);
+ } else {
+ throw new IllegalArgumentException("PushoverAPIConnection is null!");
+ }
+ }
+
+ private void asyncValidateUser() {
+ try {
+ connection.validateUser();
+ updateStatus(ThingStatus.ONLINE);
+ } catch (PushoverCommunicationException | PushoverConfigurationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 00000000000..6ed0642794f
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ Pushover Binding
+ Pushover - Simple Notifications.
+
+
diff --git a/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 00000000000..8e74c72613c
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+ password
+
+ Your API token / key (APP_TOKEN) to access the Pushover Message API.
+
+
+ password
+
+ Your user key or group key (USER_KEY) to which you want to push notifications.
+
+
+
+ The default title of a message.
+ openHAB
+
+
+
+ The default format of a message.
+ none
+
+
+
+
+
+
+
+
+ The default notification sound on target device.
+ default
+
+
+ true
+
+ The retry parameter specifies how often the Pushover servers will send the same notification to the
+ user.
+ 300
+
+
+ true
+
+ The expire parameter specifies how long your notification will continue to be retried.
+ 3600
+
+
+
+
diff --git a/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover.properties b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover.properties
new file mode 100644
index 00000000000..c6a3bf9a847
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover.properties
@@ -0,0 +1,52 @@
+# user defined messages
+offline.conf-error-missing-apikey = The 'apikey' parameter must be configured.
+offline.conf-error-missing-user = The 'user' parameter must be configured.
+
+# actions
+sendMessageActionLabel = send a plain text message
+sendMessageActionDescription = This method is used to send a plain text message.
+sendMessageActionOutputLabel = Sent
+sendMessageActionOutputDescription = true, if message has been sent successfully
+sendMessageActionInputMessageLabel = Message
+sendMessageActionInputMessageDescription = Message to be sent.
+sendMessageActionInputTitleLabel = Title
+sendMessageActionInputTitleDescription = The title of the message.
+
+sendURLMessageActionLabel = send a plain text message with an URL
+sendURLMessageActionDescription = This method is used to send a message with an URL.
+sendMessageActionInputURLLabel = URL
+sendMessageActionInputURLDescription = A supplementary URL to show with the message.
+sendMessageActionInputURLTitleLabel = URL Title
+sendMessageActionInputURLTitleDescription = 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.
+
+sendMonospaceMessageActionLabel = send a monospace message
+sendMonospaceMessageActionDescription = This method is used to send a monospace message.
+
+sendAttachmentMessageActionLabel = send a plain text message with an attachment
+sendAttachmentMessageActionDescription = This method is used to send a message with an attachment.
+sendMessageActionInputAttachmentLabel = Attachment
+sendMessageActionInputAttachmentDescription = A (local) path to the attachment.
+sendMessageActionInputContentTypeLabel = Content Type
+sendMessageActionInputContentTypeDescription = The content type of the attachment. Defaults to "image/jpeg".
+
+sendPriorityMessageActionLabel = send a priority message
+sendPriorityMessageActionDescription = This method is used to send a priority message.
+sendPriorityMessageActionOutputLabel = Receipt
+sendPriorityMessageActionOutputDescription = Receipt, if priority message sent successfully.
+sendMessageActionInputPriorityLabel = Priority
+sendMessageActionInputPriorityDescription = Priority to be used. Defaults to 2.
+
+cancelPriorityMessageActionLabel = cancel a priority message
+cancelPriorityMessageActionDescription = This method is used to cancel a priority message.
+cancelPriorityMessageActionOnputLabel = Cancelled
+cancelPriorityMessageActionOnputDescription = true, if message has been cancelled successfully.
+cancelPriorityMessageActionInputReceiptLabel = Receipt
+cancelPriorityMessageActionInputReceiptDescription = Receipt of the message to be canceled.
+
+sendMessageToDeviceActionLabel = send a plain text message to a specific device
+sendMessageToDeviceActionDescription = This method is used to send a message to a specific device.
+sendMessageActionInputDeviceLabel = Device
+sendMessageActionInputDeviceDescription = The name of a specific device (multiple devices may be separated by a comma).
diff --git a/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover_de.properties b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover_de.properties
new file mode 100644
index 00000000000..bcc86ce12c8
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/i18n/pushover_de.properties
@@ -0,0 +1,75 @@
+# binding
+binding.pushover.description = Pushover - Einfache Benachrichtigungen.
+
+# thing types
+thing-type.pushover.pushover-account.label = Pushover Konto
+thing-type.pushover.pushover-account.description = Ermöglicht den Zugriff auf die Pushover Message API.
+
+# thing type config description
+thing-type.config.pushover.pushover-account.apikey.label = API Token / Key
+thing-type.config.pushover.pushover-account.apikey.description = API Token / Schlüssel für den Zugriff auf die Pushover Message API.
+thing-type.config.pushover.pushover-account.user.label = User / Group Key
+thing-type.config.pushover.pushover-account.user.description = User / Group Key (USER_KEY) an den / die Nachrichten gesendet werden sollen.
+thing-type.config.pushover.pushover-account.title.label = Titel
+thing-type.config.pushover.pushover-account.title.description = Standardtitel der Nachricht.
+thing-type.config.pushover.pushover-account.format.label = Format
+thing-type.config.pushover.pushover-account.format.description = Standardformat der Nachricht.
+thing-type.config.pushover.pushover-account.sound.label = Benachrichtigungston
+thing-type.config.pushover.pushover-account.sound.description = Standardbenachrichtigungston auf dem Endgerät.
+thing-type.config.pushover.pushover-account.retry.label = Wiederholungen
+thing-type.config.pushover.pushover-account.retry.description = Dieser Parameter gibt an, in welchen Abständen eine Prioritätsnachricht wiederholt an den Benutzer gesendet werden soll.
+thing-type.config.pushover.pushover-account.expire.label = Verfall
+thing-type.config.pushover.pushover-account.expire.description = Dieser Parameter gibt an, wie lange eine Prioritätsnachricht wiederholt wird.
+
+# 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.
+
+# actions
+sendMessageActionLabel = eine Textnachricht senden
+sendMessageActionDescription = Action zum Versenden einer Textnachricht.
+sendMessageActionOutputLabel = Gesendet
+sendMessageActionOutputDescription = true, wenn die Nachricht erfolgreich versendet wurde.
+sendMessageActionInputMessageLabel = Nachricht
+sendMessageActionInputMessageDescription = Die Nachricht.
+sendMessageActionInputTitleLabel = Titel
+sendMessageActionInputTitleDescription = Titel der Nachricht.
+
+sendURLMessageActionLabel = eine Textnachricht mit URL senden
+sendURLMessageActionDescription = Action zum Versenden einer Textnachricht mit einer URL.
+sendMessageActionInputURLLabel = URL
+sendMessageActionInputURLDescription = Eine zusätzliche URL, die mit der Nachricht angezeigt werden soll.
+sendMessageActionInputURLTitleLabel = URL Title
+sendMessageActionInputURLTitleDescription = Ein Titel für die URL, andernfalls wird nur die URL angezeigt.
+
+sendHTMLMessageActionLabel = eine HTML-Nachricht senden
+sendHTMLMessageActionDescription = Action zum Versenden einer HTML-Nachricht.
+
+sendMonospaceMessageActionLabel = eine monospace-Nachricht senden
+sendMonospaceMessageActionDescription = Action zum Versenden einer monospace-Nachricht.
+
+sendAttachmentMessageActionLabel = eine Textnachricht mit Anhang senden
+sendAttachmentMessageActionDescription = Action zum Versenden einer Textnachricht mit Anhang.
+sendMessageActionInputAttachmentLabel = Anhang
+sendMessageActionInputAttachmentDescription = Lokaler Pfad zum Anhang.
+sendMessageActionInputContentTypeLabel = Content-Type
+sendMessageActionInputContentTypeDescription = Der Content-Type für den Anhang. Default: "image/jpeg".
+
+sendPriorityMessageActionLabel = eine Prioritätsnachricht senden
+sendPriorityMessageActionDescription = Action zum Versenden einer Prioritätsnachricht.
+sendPriorityMessageActionOutputLabel = Receipt
+sendPriorityMessageActionOutputDescription = ID der Prioritätsnachricht, wenn diese erfolgreich versendet wurde.
+sendMessageActionInputPriorityLabel = Priorität
+sendMessageActionInputPriorityDescription = Die Priorität. Default: 2.
+
+cancelPriorityMessageActionLabel = eine Prioritätsnachricht annullieren
+cancelPriorityMessageActionDescription = Action zum Annullieren einer Prioritätsnachricht.
+cancelPriorityMessageActionOnputLabel = Annulliert
+cancelPriorityMessageActionOnputDescription = true, wenn die Prioritätsnachricht erfolgreich annulliert wurde.
+cancelPriorityMessageActionInputReceiptLabel = Receipt
+cancelPriorityMessageActionInputReceiptDescription = Die ID der Prioritätsnachricht.
+
+sendMessageToDeviceActionLabel = eine Nachricht an ein Endgerät
+sendMessageToDeviceActionDescription = Action zum Versenden einer Nachricht an ein Endgerät.
+sendMessageActionInputDeviceLabel = Endgerät
+sendMessageActionInputDeviceDescription = Der Name des Endgeräts (mehrere Geräte können durch ein Komma getrennt werden).
diff --git a/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 00000000000..0bb228b5e4a
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Provides access to the Pushover Messages API.
+
+ apikey
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.pushover/src/test/java/org/openhab/binding/pushover/internal/actions/PushoverActionsTest.java b/bundles/org.openhab.binding.pushover/src/test/java/org/openhab/binding/pushover/internal/actions/PushoverActionsTest.java
new file mode 100644
index 00000000000..423087bc5d6
--- /dev/null
+++ b/bundles/org.openhab.binding.pushover/src/test/java/org/openhab/binding/pushover/internal/actions/PushoverActionsTest.java
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) 2010-2020 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.pushover.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.pushover.internal.connection.PushoverMessageBuilder;
+import org.openhab.binding.pushover.internal.handler.PushoverAccountHandler;
+import org.openhab.core.thing.binding.ThingActions;
+import org.openhab.core.thing.binding.ThingHandler;
+
+/**
+ * Unit tests for {@link PushoverActions}.
+ *
+ * @author Christoph Weitkamp - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.WARN)
+public class PushoverActionsTest {
+
+ 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 PushoverAccountHandler mockPushoverAccountHandler;
+
+ private PushoverActions pushoverThingActions;
+
+ @BeforeEach
+ public void setUp() {
+ pushoverThingActions = new PushoverActions();
+
+ when(mockPushoverAccountHandler.getDefaultPushoverMessageBuilder(any()))
+ .thenReturn(PushoverMessageBuilder.getInstance("key", "user"));
+ when(mockPushoverAccountHandler.sendMessage(any())).thenReturn(Boolean.TRUE);
+ when(mockPushoverAccountHandler.sendPriorityMessage(any())).thenReturn(RECEIPT);
+ when(mockPushoverAccountHandler.cancelPriorityMessage(RECEIPT)).thenReturn(Boolean.TRUE);
+ }
+
+ // sendMessage
+ @Test
+ public void testSendMessageThingActionsIsNotPushoverThingActions() {
+ assertThrows(ClassCastException.class, () -> PushoverActions.sendMessage(thingActionsStub, MESSAGE, TITLE));
+ }
+
+ @Test
+ public void testSendMessageThingHandlerIsNull() {
+ assertThrows(RuntimeException.class, () -> PushoverActions.sendMessage(pushoverThingActions, MESSAGE, TITLE));
+ }
+
+ @Test
+ public void testSendMessageWithoutTitle() {
+ pushoverThingActions.setThingHandler(mockPushoverAccountHandler);
+ boolean sent = PushoverActions.sendMessage(pushoverThingActions, MESSAGE, null);
+ assertThat(sent, is(true));
+ }
+
+ @Test
+ public void testSendMessage() {
+ pushoverThingActions.setThingHandler(mockPushoverAccountHandler);
+ boolean sent = PushoverActions.sendMessage(pushoverThingActions, MESSAGE, TITLE);
+ assertThat(sent, is(true));
+ }
+
+ // sendURLMessage
+ @Test
+ public void testSendURLMessageThingActionsIsNotPushoverThingActions() {
+ assertThrows(ClassCastException.class,
+ () -> PushoverActions.sendURLMessage(thingActionsStub, MESSAGE, TITLE, URL, URL_TITLE));
+ }
+
+ @Test
+ public void testSendURLMessageThingHandlerIsNull() {
+ assertThrows(RuntimeException.class,
+ () -> PushoverActions.sendURLMessage(pushoverThingActions, MESSAGE, TITLE, URL, URL_TITLE));
+ }
+
+ @Test
+ public void testSendURLMessageWithoutTitle() {
+ pushoverThingActions.setThingHandler(mockPushoverAccountHandler);
+ boolean sent = PushoverActions.sendURLMessage(pushoverThingActions, MESSAGE, null, URL, URL_TITLE);
+ assertThat(sent, is(true));
+ }
+
+ @Test
+ public void testSendURLMessageWithoutURLTitle() {
+ pushoverThingActions.setThingHandler(mockPushoverAccountHandler);
+ boolean sent = PushoverActions.sendURLMessage(pushoverThingActions, MESSAGE, TITLE, URL, null);
+ assertThat(sent, is(true));
+ }
+
+ @Test
+ public void testSendURLMessage() {
+ pushoverThingActions.setThingHandler(mockPushoverAccountHandler);
+ boolean sent = PushoverActions.sendURLMessage(pushoverThingActions, MESSAGE, TITLE, URL, URL_TITLE);
+ assertThat(sent, is(true));
+ }
+
+ // sendPriorityMessage
+ @Test
+ public void testSendPriorityMessageThingActionsIsNotPushoverThingActions() {
+ assertThrows(ClassCastException.class, () -> PushoverActions.sendPriorityMessage(thingActionsStub, MESSAGE,
+ TITLE, PushoverMessageBuilder.EMERGENCY_PRIORITY));
+ }
+
+ @Test
+ public void testSendPriorityMessageThingHandlerIsNull() {
+ assertThrows(RuntimeException.class, () -> PushoverActions.sendPriorityMessage(pushoverThingActions, MESSAGE,
+ TITLE, PushoverMessageBuilder.EMERGENCY_PRIORITY));
+ }
+
+ @Test
+ public void testSendPriorityMessageWithoutTitle() {
+ pushoverThingActions.setThingHandler(mockPushoverAccountHandler);
+ String receipt = PushoverActions.sendPriorityMessage(pushoverThingActions, MESSAGE, null,
+ PushoverMessageBuilder.EMERGENCY_PRIORITY);
+ assertThat(receipt, is(RECEIPT));
+ }
+
+ @Test
+ public void testSendPriorityMessage() {
+ pushoverThingActions.setThingHandler(mockPushoverAccountHandler);
+ String receipt = PushoverActions.sendPriorityMessage(pushoverThingActions, MESSAGE, TITLE,
+ PushoverMessageBuilder.EMERGENCY_PRIORITY);
+ assertThat(receipt, is(RECEIPT));
+ }
+
+ // cancelPriorityMessage
+ @Test
+ public void testCancelPriorityMessageThingActionsIsNotPushoverThingActions() {
+ assertThrows(ClassCastException.class, () -> PushoverActions.cancelPriorityMessage(thingActionsStub, RECEIPT));
+ }
+
+ @Test
+ public void testCancelPriorityMessageThingHandlerIsNull() {
+ assertThrows(RuntimeException.class,
+ () -> PushoverActions.cancelPriorityMessage(pushoverThingActions, RECEIPT));
+ }
+
+ @Test
+ public void testCancelPriorityMessageWithValidReceipt() {
+ pushoverThingActions.setThingHandler(mockPushoverAccountHandler);
+ boolean cancelled = PushoverActions.cancelPriorityMessage(pushoverThingActions, RECEIPT);
+ assertThat(cancelled, is(true));
+ }
+
+ @Test
+ public void testCancelPriorityMessageWithInvalidReceipt() {
+ pushoverThingActions.setThingHandler(mockPushoverAccountHandler);
+ boolean cancelled = PushoverActions.cancelPriorityMessage(pushoverThingActions, "invalid");
+ assertThat(cancelled, is(false));
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 11b1484ea50..cc8f55fae4a 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -233,6 +233,7 @@
org.openhab.binding.powermax
org.openhab.binding.pulseaudio
org.openhab.binding.pushbullet
+ org.openhab.binding.pushover
org.openhab.binding.radiothermostat
org.openhab.binding.regoheatpump
org.openhab.binding.revogi