[pushover] Migration of Pushover OH1 action to OH3 binding (#8586)

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
This commit is contained in:
Christoph Weitkamp 2020-11-29 12:31:21 +01:00 committed by GitHub
parent 5d60d6464b
commit 6e0cacab31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1725 additions and 0 deletions

View File

@ -201,6 +201,7 @@
/bundles/org.openhab.binding.powermax/ @lolodomo /bundles/org.openhab.binding.powermax/ @lolodomo
/bundles/org.openhab.binding.pulseaudio/ @peuter /bundles/org.openhab.binding.pulseaudio/ @peuter
/bundles/org.openhab.binding.pushbullet/ @hakan42 /bundles/org.openhab.binding.pushbullet/ @hakan42
/bundles/org.openhab.binding.pushover/ @cweitkamp
/bundles/org.openhab.binding.radiothermostat/ @mlobstein /bundles/org.openhab.binding.radiothermostat/ @mlobstein
/bundles/org.openhab.binding.regoheatpump/ @crnjan /bundles/org.openhab.binding.regoheatpump/ @crnjan
/bundles/org.openhab.binding.revogi/ @andibraeu /bundles/org.openhab.binding.revogi/ @andibraeu

View File

@ -996,6 +996,11 @@
<artifactId>org.openhab.binding.pushbullet</artifactId> <artifactId>org.openhab.binding.pushbullet</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.pushover</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.radiothermostat</artifactId> <artifactId>org.openhab.binding.radiothermostat</artifactId>

View File

@ -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

View File

@ -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 <font color='green'>World</font>!", "openHAB")
```
```java
val actions = getActions("pushover", "pushover:pushover-account:account")
// send priority message
var receipt = actions.sendPriorityMessage("Hello <font color='green'>World</font>!", "openHAB", 3)
// wait for your cancel condition
if( receipt !== null ) {
actions.cancelPriorityMessage(receipt)
receipt = null
}
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
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.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.pushover</artifactId>
<name>openHAB Add-ons :: Bundles :: Pushover Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.pushover-${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-pushover" description="Pushover Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.pushover/${project.version}</bundle>
</feature>
</features>

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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<ParameterOption> 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<Sound> 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;
}
}

View File

@ -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<String, String> 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<Sound> 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<String, String> 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<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) {
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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<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 = 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;
}
}

View File

@ -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);
}
}

View File

@ -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<ThingTypeUID> 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;
}
}

View File

@ -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<Class<? extends ThingHandlerService>> 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<Class<? extends ThingHandlerService>> 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<Sound> 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());
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="pushover" 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>Pushover Binding</name>
<description>Pushover - Simple Notifications.</description>
</binding:binding>

View File

@ -0,0 +1,53 @@
<?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:pushover:pushover-account">
<parameter name="apikey" type="text" required="true">
<context>password</context>
<label>API Token / Key</label>
<description>Your API token / key (APP_TOKEN) to access the Pushover Message API.</description>
</parameter>
<parameter name="user" type="text" required="true">
<context>password</context>
<label>User / Group Key</label>
<description>Your user key or group key (USER_KEY) to which you want to push notifications.</description>
</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>default</default>
</parameter>
<parameter name="retry" type="integer" min="30" unit="s">
<advanced>true</advanced>
<label>Retry</label>
<description>The retry parameter specifies how often the Pushover servers will send the same notification to the
user.</description>
<default>300</default>
</parameter>
<parameter name="expire" type="integer" min="0" max="10800" unit="s">
<advanced>true</advanced>
<label>Expire</label>
<description>The expire parameter specifies how long your notification will continue to be retried.</description>
<default>3600</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -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).

View File

@ -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).

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pushover"
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="pushover-account">
<label>Pushover Account</label>
<description>Provides access to the Pushover Messages API.</description>
<representation-property>apikey</representation-property>
<config-description-ref uri="thing-type:pushover:pushover-account"/>
</thing-type>
</thing:thing-descriptions>

View File

@ -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));
}
}

View File

@ -233,6 +233,7 @@
<module>org.openhab.binding.powermax</module> <module>org.openhab.binding.powermax</module>
<module>org.openhab.binding.pulseaudio</module> <module>org.openhab.binding.pulseaudio</module>
<module>org.openhab.binding.pushbullet</module> <module>org.openhab.binding.pushbullet</module>
<module>org.openhab.binding.pushover</module>
<module>org.openhab.binding.radiothermostat</module> <module>org.openhab.binding.radiothermostat</module>
<module>org.openhab.binding.regoheatpump</module> <module>org.openhab.binding.regoheatpump</module>
<module>org.openhab.binding.revogi</module> <module>org.openhab.binding.revogi</module>