2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.pushbullet.internal.handler;
15 import static org.openhab.binding.pushbullet.internal.PushbulletBindingConstants.*;
17 import java.io.IOException;
18 import java.net.MalformedURLException;
20 import java.nio.file.Files;
21 import java.nio.file.Path;
22 import java.util.Collection;
23 import java.util.Objects;
25 import java.util.regex.Pattern;
27 import javax.mail.internet.AddressException;
28 import javax.mail.internet.InternetAddress;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.openhab.binding.pushbullet.internal.PushbulletConfiguration;
34 import org.openhab.binding.pushbullet.internal.PushbulletHttpClient;
35 import org.openhab.binding.pushbullet.internal.action.PushbulletActions;
36 import org.openhab.binding.pushbullet.internal.exception.PushbulletApiException;
37 import org.openhab.binding.pushbullet.internal.exception.PushbulletAuthenticationException;
38 import org.openhab.binding.pushbullet.internal.model.PushRequest;
39 import org.openhab.binding.pushbullet.internal.model.PushResponse;
40 import org.openhab.binding.pushbullet.internal.model.PushType;
41 import org.openhab.binding.pushbullet.internal.model.UploadRequest;
42 import org.openhab.binding.pushbullet.internal.model.UploadResponse;
43 import org.openhab.binding.pushbullet.internal.model.User;
44 import org.openhab.core.io.net.http.HttpUtil;
45 import org.openhab.core.library.types.RawType;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseThingHandler;
51 import org.openhab.core.thing.binding.ThingHandlerService;
52 import org.openhab.core.types.Command;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * The {@link PushbulletHandler} is responsible for handling commands, which are
58 * sent to one of the channels.
60 * @author Hakan Tandogan - Initial contribution
61 * @author Jeremy Setton - Add link and file push type support
64 public class PushbulletHandler extends BaseThingHandler {
66 private static final Pattern CHANNEL_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
68 private final Logger logger = LoggerFactory.getLogger(PushbulletHandler.class);
70 private final PushbulletHttpClient httpClient;
72 private int maxUploadSize;
74 public PushbulletHandler(Thing thing, HttpClient httpClient) {
76 this.httpClient = new PushbulletHttpClient(httpClient);
80 public void handleCommand(ChannelUID channelUID, Command command) {
81 logger.debug("About to handle {} on {}", command, channelUID);
83 // Future improvement: If recipient is already set, send a push on a command channel change
84 // check reconnect channel of the unifi binding for that
86 logger.debug("The Pushbullet binding is a read-only binding and cannot handle command '{}'.", command);
90 public void initialize() {
91 logger.debug("Starting {}", thing.getUID());
93 PushbulletConfiguration config = getConfigAs(PushbulletConfiguration.class);
95 if (config.getAccessToken().isEmpty()) {
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Undefined access token.");
100 httpClient.setConfiguration(config);
102 scheduler.execute(() -> retrieveAccountInfo());
104 updateStatus(ThingStatus.UNKNOWN);
107 private void retrieveAccountInfo() {
109 User user = httpClient.executeRequest(API_ENDPOINT_USERS_ME, User.class);
111 maxUploadSize = Objects.requireNonNullElse(user.getMaxUploadSize(), MAX_UPLOAD_SIZE);
113 logger.debug("Set maximum upload size for {} to {} bytes", thing.getUID(), maxUploadSize);
115 updateProperty(PROPERTY_NAME, user.getName());
116 updateProperty(PROPERTY_EMAIL, user.getEmail());
118 logger.debug("Updated properties for {} to {}", thing.getUID(), thing.getProperties());
120 updateStatus(ThingStatus.ONLINE);
121 } catch (PushbulletAuthenticationException e) {
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid access token.");
123 } catch (PushbulletApiException e) {
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
125 "Unable to retrieve account info.");
130 public Collection<Class<? extends ThingHandlerService>> getServices() {
131 return Set.of(PushbulletActions.class);
137 * @param recipient the recipient
138 * @param title the title
139 * @param message the message
140 * @return true if successful
142 public boolean sendPushNote(@Nullable String recipient, @Nullable String title, String message) {
143 PushRequest request = newPushRequest(recipient, title, message, PushType.NOTE);
145 return sendPush(request);
151 * @param recipient the recipient
152 * @param title the title
153 * @param message the message
154 * @param url the message url
155 * @return true if successful
157 public boolean sendPushLink(@Nullable String recipient, @Nullable String title, @Nullable String message,
159 PushRequest request = newPushRequest(recipient, title, message, PushType.LINK);
162 return sendPush(request);
168 * @param recipient the recipient
169 * @param title the title
170 * @param message the message
171 * @param content the file content
172 * @param fileName the file name
173 * @return true if successful
175 public boolean sendPushFile(@Nullable String recipient, @Nullable String title, @Nullable String message,
176 String content, @Nullable String fileName) {
177 UploadResponse upload = uploadFile(content, fileName);
178 if (upload == null) {
182 PushRequest request = newPushRequest(recipient, title, message, PushType.FILE);
183 request.setFileName(upload.getFileName());
184 request.setFileType(upload.getFileType());
185 request.setFileUrl(upload.getFileUrl());
187 return sendPush(request);
191 * Helper method to send a push request
193 * @param request the push request
194 * @return true if successful
196 private boolean sendPush(PushRequest request) {
197 logger.debug("Sending push notification for {}", thing.getUID());
198 logger.debug("Push Request: {}", request);
201 httpClient.executeRequest(API_ENDPOINT_PUSHES, request, PushResponse.class);
203 } catch (PushbulletApiException e) {
209 * Helper method to upload a file to use in push message
211 * @param content the file content
212 * @param fileName the file name
213 * @return the upload response if successful, otherwise null
215 private @Nullable UploadResponse uploadFile(String content, @Nullable String fileName) {
216 RawType data = getContentData(content);
218 logger.warn("Failed to get content data from '{}'", content);
222 logger.debug("Content Data: {}", data);
224 int size = data.getBytes().length;
225 if (size > maxUploadSize) {
226 logger.warn("Content data size {} is greater than maximum upload size {}", size, maxUploadSize);
231 UploadRequest request = new UploadRequest();
232 request.setFileName(fileName != null ? fileName : getContentFileName(content));
233 request.setFileType(data.getMimeType());
235 logger.debug("Upload Request: {}", request);
237 UploadResponse response = httpClient.executeRequest(API_ENDPOINT_UPLOAD_REQUEST, request,
238 UploadResponse.class);
240 String uploadUrl = response.getUploadUrl();
241 if (uploadUrl == null) {
242 throw new PushbulletApiException("Undefined upload url");
245 httpClient.uploadFile(uploadUrl, data);
248 } catch (PushbulletApiException e) {
254 * Helper method to get the data for a given content
256 * @param content the file content
257 * @return the data raw type if available, otherwise null
259 private @Nullable RawType getContentData(String content) {
261 if (content.startsWith("data:")) {
262 return RawType.valueOf(content);
263 } else if (content.startsWith("http")) {
264 return HttpUtil.downloadImage(content);
266 Path path = Path.of(content);
267 byte[] bytes = Files.readAllBytes(path);
268 String mimeType = Files.probeContentType(path);
269 return new RawType(bytes, mimeType);
271 } catch (IllegalArgumentException | IOException e) {
272 logger.debug("Failed to get content data: {}", e.getMessage());
278 * Helper method to get the file name for a given content
280 * @param content the file content
281 * @return the file name if available, otherwise null
283 private @Nullable String getContentFileName(String content) {
284 if (content.startsWith("data:")) {
285 return IMAGE_FILE_NAME;
288 Path fileName = Path.of(content.startsWith("http") ? new URL(content).getPath() : content).getFileName();
289 if (fileName != null) {
290 return fileName.toString();
292 } catch (MalformedURLException e) {
293 logger.debug("Malformed url content: {}", e.getMessage());
299 * Helper method to create a push request
301 * @param recipient the recipient
302 * @param title the title
303 * @param message the message
304 * @param type the push type
306 * @return the push request object
308 private PushRequest newPushRequest(@Nullable String recipient, @Nullable String title, @Nullable String message,
310 logger.debug("Recipient is '{}'", recipient);
311 logger.debug("Title is '{}'", title);
312 logger.debug("Message is '{}'", message);
313 logger.debug("Type is '{}'", type);
315 PushRequest request = new PushRequest();
316 request.setTitle(title);
317 request.setBody(message);
318 request.setType(type);
320 if (recipient != null) {
321 if (isValidEmail(recipient)) {
322 logger.debug("Recipient is an email address");
323 request.setEmail(recipient);
324 } else if (isValidChannel(recipient)) {
325 logger.debug("Recipient is a channel tag");
326 request.setChannel(recipient);
328 logger.warn("Invalid recipient: {}", recipient);
329 logger.warn("Message will be broadcast to all user's devices.");
337 * Helper method checking if channel tag is valid
339 * @param channel the channel tag
340 * @return true if matches pattern
342 private static boolean isValidChannel(String channel) {
343 return CHANNEL_PATTERN.matcher(channel).matches();
347 * Helper method checking if email address is valid
349 * @param email the email address
350 * @return true if parsed successfully
352 private static boolean isValidEmail(String email) {
354 new InternetAddress(email, true);
356 } catch (AddressException e) {