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