]> git.basschouten.com Git - openhab-addons.git/blob
3055ea3f80ca541702f3aee7daa08f2077dc12a9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.pushbullet.internal.handler;
14
15 import static org.openhab.binding.pushbullet.internal.PushbulletBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.MalformedURLException;
19 import java.net.URL;
20 import java.nio.file.Files;
21 import java.nio.file.Path;
22 import java.util.Collection;
23 import java.util.Objects;
24 import java.util.Set;
25 import java.util.regex.Pattern;
26
27 import javax.mail.internet.AddressException;
28 import javax.mail.internet.InternetAddress;
29
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;
55
56 /**
57  * The {@link PushbulletHandler} is responsible for handling commands, which are
58  * sent to one of the channels.
59  *
60  * @author Hakan Tandogan - Initial contribution
61  * @author Jeremy Setton - Add link and file push type support
62  */
63 @NonNullByDefault
64 public class PushbulletHandler extends BaseThingHandler {
65
66     private static final Pattern CHANNEL_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
67
68     private final Logger logger = LoggerFactory.getLogger(PushbulletHandler.class);
69
70     private final PushbulletHttpClient httpClient;
71
72     private int maxUploadSize;
73
74     public PushbulletHandler(Thing thing, HttpClient httpClient) {
75         super(thing);
76         this.httpClient = new PushbulletHttpClient(httpClient);
77     }
78
79     @Override
80     public void handleCommand(ChannelUID channelUID, Command command) {
81         logger.debug("About to handle {} on {}", command, channelUID);
82
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
85
86         logger.debug("The Pushbullet binding is a read-only binding and cannot handle command '{}'.", command);
87     }
88
89     @Override
90     public void initialize() {
91         logger.debug("Starting {}", thing.getUID());
92
93         PushbulletConfiguration config = getConfigAs(PushbulletConfiguration.class);
94
95         if (config.getAccessToken().isEmpty()) {
96             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Undefined access token.");
97             return;
98         }
99
100         httpClient.setConfiguration(config);
101
102         scheduler.execute(() -> retrieveAccountInfo());
103
104         updateStatus(ThingStatus.UNKNOWN);
105     }
106
107     private void retrieveAccountInfo() {
108         try {
109             User user = httpClient.executeRequest(API_ENDPOINT_USERS_ME, User.class);
110
111             maxUploadSize = Objects.requireNonNullElse(user.getMaxUploadSize(), MAX_UPLOAD_SIZE);
112
113             logger.debug("Set maximum upload size for {} to {} bytes", thing.getUID(), maxUploadSize);
114
115             updateProperty(PROPERTY_NAME, user.getName());
116             updateProperty(PROPERTY_EMAIL, user.getEmail());
117
118             logger.debug("Updated properties for {} to {}", thing.getUID(), thing.getProperties());
119
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.");
126         }
127     }
128
129     @Override
130     public Collection<Class<? extends ThingHandlerService>> getServices() {
131         return Set.of(PushbulletActions.class);
132     }
133
134     /**
135      * Sends a push note
136      *
137      * @param recipient the recipient
138      * @param title the title
139      * @param message the message
140      * @return true if successful
141      */
142     public boolean sendPushNote(@Nullable String recipient, @Nullable String title, String message) {
143         PushRequest request = newPushRequest(recipient, title, message, PushType.NOTE);
144
145         return sendPush(request);
146     }
147
148     /**
149      * Sends a push link
150      *
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
156      */
157     public boolean sendPushLink(@Nullable String recipient, @Nullable String title, @Nullable String message,
158             String url) {
159         PushRequest request = newPushRequest(recipient, title, message, PushType.LINK);
160         request.setUrl(url);
161
162         return sendPush(request);
163     }
164
165     /**
166      * Sends a push file
167      *
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
174      */
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) {
179             return false;
180         }
181
182         PushRequest request = newPushRequest(recipient, title, message, PushType.FILE);
183         request.setFileName(upload.getFileName());
184         request.setFileType(upload.getFileType());
185         request.setFileUrl(upload.getFileUrl());
186
187         return sendPush(request);
188     }
189
190     /**
191      * Helper method to send a push request
192      *
193      * @param request the push request
194      * @return true if successful
195      */
196     private boolean sendPush(PushRequest request) {
197         logger.debug("Sending push notification for {}", thing.getUID());
198         logger.debug("Push Request: {}", request);
199
200         try {
201             httpClient.executeRequest(API_ENDPOINT_PUSHES, request, PushResponse.class);
202             return true;
203         } catch (PushbulletApiException e) {
204             return false;
205         }
206     }
207
208     /**
209      * Helper method to upload a file to use in push message
210      *
211      * @param content the file content
212      * @param fileName the file name
213      * @return the upload response if successful, otherwise null
214      */
215     private @Nullable UploadResponse uploadFile(String content, @Nullable String fileName) {
216         RawType data = getContentData(content);
217         if (data == null) {
218             logger.warn("Failed to get content data from '{}'", content);
219             return null;
220         }
221
222         logger.debug("Content Data: {}", data);
223
224         int size = data.getBytes().length;
225         if (size > maxUploadSize) {
226             logger.warn("Content data size {} is greater than maximum upload size {}", size, maxUploadSize);
227             return null;
228         }
229
230         try {
231             UploadRequest request = new UploadRequest();
232             request.setFileName(fileName != null ? fileName : getContentFileName(content));
233             request.setFileType(data.getMimeType());
234
235             logger.debug("Upload Request: {}", request);
236
237             UploadResponse response = httpClient.executeRequest(API_ENDPOINT_UPLOAD_REQUEST, request,
238                     UploadResponse.class);
239
240             String uploadUrl = response.getUploadUrl();
241             if (uploadUrl == null) {
242                 throw new PushbulletApiException("Undefined upload url");
243             }
244
245             httpClient.uploadFile(uploadUrl, data);
246
247             return response;
248         } catch (PushbulletApiException e) {
249             return null;
250         }
251     }
252
253     /**
254      * Helper method to get the data for a given content
255      *
256      * @param content the file content
257      * @return the data raw type if available, otherwise null
258      */
259     private @Nullable RawType getContentData(String content) {
260         try {
261             if (content.startsWith("data:")) {
262                 return RawType.valueOf(content);
263             } else if (content.startsWith("http")) {
264                 return HttpUtil.downloadImage(content);
265             } else {
266                 Path path = Path.of(content);
267                 byte[] bytes = Files.readAllBytes(path);
268                 String mimeType = Files.probeContentType(path);
269                 return new RawType(bytes, mimeType);
270             }
271         } catch (IllegalArgumentException | IOException e) {
272             logger.debug("Failed to get content data: {}", e.getMessage());
273             return null;
274         }
275     }
276
277     /**
278      * Helper method to get the file name for a given content
279      *
280      * @param content the file content
281      * @return the file name if available, otherwise null
282      */
283     private @Nullable String getContentFileName(String content) {
284         if (content.startsWith("data:")) {
285             return IMAGE_FILE_NAME;
286         }
287         try {
288             Path fileName = Path.of(content.startsWith("http") ? new URL(content).getPath() : content).getFileName();
289             if (fileName != null) {
290                 return fileName.toString();
291             }
292         } catch (MalformedURLException e) {
293             logger.debug("Malformed url content: {}", e.getMessage());
294         }
295         return null;
296     }
297
298     /**
299      * Helper method to create a push request
300      *
301      * @param recipient the recipient
302      * @param title the title
303      * @param message the message
304      * @param type the push type
305      *
306      * @return the push request object
307      */
308     private PushRequest newPushRequest(@Nullable String recipient, @Nullable String title, @Nullable String message,
309             PushType type) {
310         logger.debug("Recipient is '{}'", recipient);
311         logger.debug("Title is     '{}'", title);
312         logger.debug("Message is   '{}'", message);
313         logger.debug("Type is      '{}'", type);
314
315         PushRequest request = new PushRequest();
316         request.setTitle(title);
317         request.setBody(message);
318         request.setType(type);
319
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);
327             } else {
328                 logger.warn("Invalid recipient: {}", recipient);
329                 logger.warn("Message will be broadcast to all user's devices.");
330             }
331         }
332
333         return request;
334     }
335
336     /**
337      * Helper method checking if channel tag is valid
338      *
339      * @param channel the channel tag
340      * @return true if matches pattern
341      */
342     private static boolean isValidChannel(String channel) {
343         return CHANNEL_PATTERN.matcher(channel).matches();
344     }
345
346     /**
347      * Helper method checking if email address is valid
348      *
349      * @param email the email address
350      * @return true if parsed successfully
351      */
352     private static boolean isValidEmail(String email) {
353         try {
354             new InternetAddress(email, true);
355             return true;
356         } catch (AddressException e) {
357             return false;
358         }
359     }
360 }