Skip to content

Commit

Permalink
[pushbullet] Add link and file push type support
Browse files Browse the repository at this point in the history
Signed-off-by: jsetton <[email protected]>
  • Loading branch information
jsetton committed Sep 24, 2024
1 parent 163f517 commit 3e906c2
Show file tree
Hide file tree
Showing 16 changed files with 1,041 additions and 241 deletions.
21 changes: 20 additions & 1 deletion bundles/org.openhab.binding.pushbullet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ Two different actions available:

- `sendPushbulletNote(String recipient, String messsage)`
- `sendPushbulletNote(String recipient, String title, String messsage)`
- `sendPushbulletLink(String recipient, String url)`
- `sendPushbulletLink(String recipient, String title, String messsage, String url)`
- `sendPushbulletFile(String recipient, String content)`
- `sendPushbulletFile(String recipient, String title, String messsage, String content)`
- `sendPushbulletFile(String recipient, String title, String messsage, String content, String fileName)`

Since there is a separate rule action instance for each `bot` thing, this needs to be retrieved through `getActions(scope, thingUID)`.
The first parameter always has to be `pushbullet` and the second is the full Thing UID of the bot that should be used.
Expand All @@ -56,11 +61,25 @@ Once this action instance is retrieved, you can invoke the action method on it.
The recipient can either be an email address, a channel tag or `null`.
If it is not specified or properly formatted, the note will be broadcast to all of the user account's devices.

The file content can be an image URL, a local file path or an Image item state.

The file name is used in the upload link and how it appears in the push message for non-image content.
If it is not specified, it is automatically determined from the image URL or file path.
For Image item state content, it is always `image.jpg`.

For the `sendPushbulletNote` action, parameter `message` is always required.
Likewise, for `sendPushbulletLink`, `url` and for `sendPushbulletFile`, `content` parameters are required.
Any other parameters for these actions are optional and can set to `null`.

Examples:

```java
val actions = getActions("pushbullet", "pushbullet:bot:r2d2")
val result = actions.sendPushbulletNote("[email protected]", "R2D2 talks here...", "This is the pushed note.")
actions.sendPushbulletNote("[email protected]", "Note Example", "This is the pushed note.")
actions.sendPushbulletLink("[email protected]", "Link Example", "This is the pushed link", "https://example.com")
actions.sendPushbulletFile("[email protected]", "File Example", "This is the pushed file", "https://example.com/image.png")
actions.sendPushbulletFile("[email protected]", null, null, "/path/to/somefile.pdf", "document.pdf")
actions.sendPushbulletFile("[email protected]", ImageItem.state.toFullString)
```

## Full Example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
* used across the whole binding.
*
* @author Hakan Tandogan - Initial contribution
* @author Jeremy Setton - Add link and file push type support
*/
@NonNullByDefault
public class PushbulletBindingConstants {

private static final String BINDING_ID = "pushbullet";
public static final String BINDING_ID = "pushbullet";

// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_BOT = new ThingTypeUID(BINDING_ID, "bot");
Expand All @@ -39,7 +40,11 @@ public class PushbulletBindingConstants {
public static final String MESSAGE = "message";

// Binding logic constants
public static final String API_METHOD_PUSHES = "pushes";
public static final String API_ENDPOINT_PUSHES = "/pushes";
public static final String API_ENDPOINT_UPLOAD_REQUEST = "/upload-request";
public static final String API_ENDPOINT_USERS_ME = "/users/me";

public static final int TIMEOUT = 30 * 1000; // 30 seconds
public static final String IMAGE_FILE_NAME = "image.jpg";

public static final int MAX_UPLOAD_SIZE = 26214400;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,26 @@
* The {@link PushbulletConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Hakan Tandogan - Initial contribution
* @author Jeremy Setton - Add link and file push type support
*/
@NonNullByDefault
public class PushbulletConfiguration {

private @Nullable String name;

private String token = "invalid";
private String token = "";

private String apiUrlBase = "invalid";
private String apiUrlBase = "https://api.pushbullet.com/v2";

public @Nullable String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}

public String getApiUrlBase() {
return apiUrlBase;
}

public void setApiUrlBase(String apiUrlBase) {
this.apiUrlBase = apiUrlBase;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,36 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.pushbullet.internal.handler.PushbulletHandler;
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 PushbulletHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Hakan Tandogan - Initial contribution
* @author Jeremy Setton - Add link and file push type support
*/
@NonNullByDefault
@Component(configurationPid = "binding.pushbullet", service = ThingHandlerFactory.class)
public class PushbulletHandlerFactory extends BaseThingHandlerFactory {

private final HttpClient httpClient;

@Activate
public PushbulletHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}

@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
Expand All @@ -44,7 +56,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (THING_TYPE_BOT.equals(thingTypeUID)) {
return new PushbulletHandler(thing);
return new PushbulletHandler(thing, httpClient);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* Copyright (c) 2010-2024 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.pushbullet.internal;

import java.time.Instant;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.client.util.MultiPartContentProvider;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.openhab.binding.pushbullet.internal.model.InstantDeserializer;
import org.openhab.core.OpenHAB;
import org.openhab.core.library.types.RawType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;

/**
* The {@link PushbulletHttpClient} handles requests to Pushbullet API
*
* @author Jeremy Setton - Initial contribution
*/
@NonNullByDefault
public class PushbulletHttpClient {
private static final String AGENT = "openHAB/" + OpenHAB.getVersion();

private static final int TIMEOUT = 30; // in seconds

private final Logger logger = LoggerFactory.getLogger(PushbulletHttpClient.class);

private final Gson gson = new GsonBuilder().registerTypeHierarchyAdapter(Instant.class, new InstantDeserializer())
.create();

private PushbulletConfiguration config = new PushbulletConfiguration();

private final HttpClient httpClient;

public PushbulletHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}

public void setConfiguration(PushbulletConfiguration config) {
this.config = config;
}

/**
* Executes an api request
*
* @param apiEndpoint the request api endpoint
* @param responseType the response type
* @return the unpacked response if available, otherwise null
*/
public <T> @Nullable T executeRequest(String apiEndpoint, Class<T> responseType) {
return executeRequest(apiEndpoint, null, responseType);
}

/**
* Executes an api request
*
* @param apiEndpoint the request api endpoint
* @param body the request body object
* @param responseType the response type
* @return the unpacked response if available, otherwise null
*/
public <T> @Nullable T executeRequest(String apiEndpoint, @Nullable Object body, Class<T> responseType) {
String url = config.getApiUrlBase() + apiEndpoint;
String accessToken = config.getToken();

Request request = newRequest(url).header("Access-Token", accessToken);

if (body != null) {
StringContentProvider content = new StringContentProvider(gson.toJson(body));
String contentType = MimeTypes.Type.APPLICATION_JSON.asString();

request.method(HttpMethod.POST).content(content, contentType);
}

ContentResponse contentResponse = sendRequest(request);
if (contentResponse != null && (contentResponse.getStatus() == HttpStatus.OK_200
|| HttpStatus.isClientError(contentResponse.getStatus()))) {
try {
@Nullable
T response = gson.fromJson(contentResponse.getContentAsString(), responseType);
logger.debug("Unpacked Response: {}", response);
return response;
} catch (JsonSyntaxException e) {
logger.debug("Failed to unpack response as '{}': {}", responseType.getSimpleName(), e.getMessage());
}
}

return null;
}

/**
* Uploads a file
*
* @param url the upload url
* @param data the file data
* @return true if response status code 204
*/
public boolean uploadFile(String url, RawType data) {
MultiPartContentProvider content = new MultiPartContentProvider();
content.addFieldPart("file", new BytesContentProvider(data.getMimeType(), data.getBytes()), null);

Request request = newRequest(url).method(HttpMethod.POST).content(content);

ContentResponse contentResponse = sendRequest(request);

return contentResponse != null && contentResponse.getStatus() == HttpStatus.NO_CONTENT_204;
}

/**
* Creates a new http request
*
* @param url the request url
* @return the new Request object with default parameters
*/
private Request newRequest(String url) {
return httpClient.newRequest(url).agent(AGENT).timeout(TIMEOUT, TimeUnit.SECONDS);
}

/**
* Sends http request
*
* @param request the request to send
* @return the ContentResponse object if no exception, otherwise null
*/
private @Nullable ContentResponse sendRequest(Request request) {
try {
logger.debug("Request {} {}", request.getMethod(), request.getURI());
logger.debug("Request Headers: {}", request.getHeaders());

ContentResponse response = request.send();

logger.debug("Got HTTP {} Response: '{}'", response.getStatus(), response.getContentAsString());

return response;
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.debug("Failed to send request: {}", e.getMessage());
return null;
}
}
}
Loading

0 comments on commit 3e906c2

Please sign in to comment.