Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pushbullet] Add link and file push type support #17472

Merged
merged 6 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 defaults to `image.jpg`.

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

Examples:

```java
val actions = getActions("pushbullet", "pushbullet:bot:r2d2")
val result = actions.sendPushbulletNote("someone@example.com", "R2D2 talks here...", "This is the pushed note.")
actions.sendPushbulletNote("someone@example.com", "Note Example", "This is the pushed note.")
actions.sendPushbulletLink("someone@example.com", "Link Example", "This is the pushed link", "https://example.com")
actions.sendPushbulletFile("someone@example.com", "File Example", "This is the pushed file", "https://example.com/image.png")
actions.sendPushbulletFile("someone@example.com", null, null, "/path/to/somefile.pdf", "document.pdf")
actions.sendPushbulletFile("someone@example.com", 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 @@ -38,8 +39,16 @@ public class PushbulletBindingConstants {
public static final String TITLE = "title";
public static final String MESSAGE = "message";

// Thing properties
public static final String PROPERTY_EMAIL = "email";
public static final String PROPERTY_NAME = "name";

// 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 String IMAGE_FILE_NAME = "image.jpg";

public static final int TIMEOUT = 30 * 1000; // 30 seconds
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() {
public String getAccessToken() {
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,203 @@
/**
* 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.Objects;
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.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.openhab.binding.pushbullet.internal.exception.PushbulletApiException;
import org.openhab.binding.pushbullet.internal.exception.PushbulletAuthenticationException;
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 static final String HEADER_RATELIMIT_RESET = "X-Ratelimit-Reset";

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
* @throws PushbulletApiException
*/
public <T> T executeRequest(String apiEndpoint, Class<T> responseType) throws PushbulletApiException {
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
* @throws PushbulletApiException
*/
public <T> T executeRequest(String apiEndpoint, @Nullable Object body, Class<T> responseType)
throws PushbulletApiException {
String url = config.getApiUrlBase() + apiEndpoint;
String accessToken = config.getAccessToken();

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

String responseBody = sendRequest(request);

try {
T response = Objects.requireNonNull(gson.fromJson(responseBody, responseType));
logger.debug("Unpacked Response: {}", response);
return response;
} catch (JsonSyntaxException e) {
logger.debug("Failed to unpack response as '{}': {}", responseType.getSimpleName(), e.getMessage());
throw new PushbulletApiException(e);
}
}

/**
* Uploads a file
*
* @param url the upload url
* @param data the file data
* @throws PushbulletApiException
*/
public void uploadFile(String url, RawType data) throws PushbulletApiException {
MultiPartContentProvider content = new MultiPartContentProvider();
content.addFieldPart("file", new BytesContentProvider(data.getMimeType(), data.getBytes()), null);

Request request = newRequest(url).method(HttpMethod.POST).content(content);
jsetton marked this conversation as resolved.
Show resolved Hide resolved

sendRequest(request);
}

/**
* 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 a http request
*
* @param request the request to send
* @return the response body
* @throws PushbulletApiException
*/
private String sendRequest(Request request) throws PushbulletApiException {
try {
logger.debug("Request {} {}", request.getMethod(), request.getURI());
logger.debug("Request Headers: {}", request.getHeaders());

ContentResponse response = request.send();

int statusCode = response.getStatus();
String statusReason = response.getReason();
String responseBody = response.getContentAsString();

logger.debug("Got HTTP {} Response: '{}'", statusCode, responseBody);

switch (statusCode) {
case HttpStatus.OK_200:
case HttpStatus.NO_CONTENT_204:
return responseBody;
case HttpStatus.UNAUTHORIZED_401:
case HttpStatus.FORBIDDEN_403:
throw new PushbulletAuthenticationException(statusReason);
case HttpStatus.TOO_MANY_REQUESTS_429:
logger.warn("Rate limited for making too many requests until {}",
getRateLimitResetTime(response.getHeaders()));
default:
throw new PushbulletApiException(statusReason);
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.debug("Failed to send request: {}", e.getMessage());
throw new PushbulletApiException(e);
}
}

/**
* Returns the rate limit reset time included in response headers
*
* @param headers the response headers
* @return the rate limit reset time if found in headers, otherwise null
*/
private @Nullable Instant getRateLimitResetTime(HttpFields headers) {
try {
long resetTime = headers.getLongField(HEADER_RATELIMIT_RESET);
if (resetTime != -1) {
return Instant.ofEpochSecond(resetTime);
}
} catch (NumberFormatException ignored) {
}
return null;
}
}
Loading