]> git.basschouten.com Git - openhab-addons.git/blob
94af5c436862538acd357ccf9bf2fa246485bdd9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.doorbird.internal.api;
14
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.nio.ByteBuffer;
18 import java.time.Duration;
19 import java.time.ZonedDateTime;
20 import java.util.Arrays;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.eclipse.jetty.client.api.ContentResponse;
29 import org.eclipse.jetty.client.api.Request;
30 import org.eclipse.jetty.client.api.Response;
31 import org.eclipse.jetty.client.api.Result;
32 import org.eclipse.jetty.client.util.DeferredContentProvider;
33 import org.eclipse.jetty.http.HttpHeader;
34 import org.eclipse.jetty.http.HttpMethod;
35 import org.eclipse.jetty.http.HttpStatus;
36 import org.openhab.core.io.net.http.HttpRequestBuilder;
37 import org.openhab.core.library.types.RawType;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import com.google.gson.Gson;
42 import com.google.gson.JsonSyntaxException;
43
44 /**
45  * The {@link DoorbirdAPI} class exposes the functionality provided by the Doorbird API.
46  *
47  * @author Mark Hilbush - Initial contribution
48  */
49 @NonNullByDefault
50 public final class DoorbirdAPI {
51     private static final long API_REQUEST_TIMEOUT_SECONDS = 16L;
52
53     // Single Gson instance shared by multiple classes
54     private static final Gson GSON = new Gson();
55
56     private final Logger logger = LoggerFactory.getLogger(DoorbirdAPI.class);
57     private static final int CHUNK_SIZE = 256;
58
59     private @Nullable Authorization authorization;
60     private @Nullable HttpClient httpClient;
61
62     // define a completed listener when sending audio asynchronously :
63     private Response.CompleteListener complete = new Response.CompleteListener() {
64         @Override
65         public void onComplete(@Nullable Result result) {
66             if (result != null) {
67                 logger.debug("Doorbird audio sent. Response status {} {} ", result.getResponse().getStatus(),
68                         result.getResponse().getReason());
69             }
70         }
71     };
72
73     public static Gson getGson() {
74         return (GSON);
75     }
76
77     public static <T> T fromJson(String json, Class<T> dataClass) {
78         return GSON.fromJson(json, dataClass);
79     }
80
81     public void setAuthorization(String doorbirdHost, String userId, String userPassword) {
82         this.authorization = new Authorization(doorbirdHost, userId, userPassword);
83     }
84
85     public void setHttpClient(HttpClient httpClient) {
86         this.httpClient = httpClient;
87     }
88
89     public @Nullable DoorbirdInfo getDoorbirdInfo() {
90         DoorbirdInfo doorbirdInfo = null;
91         try {
92             String infoResponse = executeGetRequest("/bha-api/info.cgi");
93             logger.debug("Doorbird returned json response: {}", infoResponse);
94             doorbirdInfo = new DoorbirdInfo(infoResponse);
95         } catch (IOException e) {
96             logger.info("Unable to communicate with Doorbird: {}", e.getMessage());
97         } catch (JsonSyntaxException e) {
98             logger.info("Unable to parse Doorbird response: {}", e.getMessage());
99         } catch (DoorbirdUnauthorizedException e) {
100             logAuthorizationError("getDoorbirdName");
101         }
102         return doorbirdInfo;
103     }
104
105     public @Nullable SipStatus getSipStatus() {
106         SipStatus sipStatus = null;
107         try {
108             String statusResponse = executeGetRequest("/bha-api/sip.cgi&action=status");
109             logger.debug("Doorbird returned json response: {}", statusResponse);
110             sipStatus = new SipStatus(statusResponse);
111         } catch (IOException e) {
112             logger.info("Unable to communicate with Doorbird: {}", e.getMessage());
113         } catch (JsonSyntaxException e) {
114             logger.info("Unable to parse Doorbird response: {}", e.getMessage());
115         } catch (DoorbirdUnauthorizedException e) {
116             logAuthorizationError("getSipStatus");
117         }
118         return sipStatus;
119     }
120
121     public void lightOn() {
122         try {
123             String response = executeGetRequest("/bha-api/light-on.cgi");
124             logger.debug("Response={}", response);
125         } catch (IOException e) {
126             logger.debug("IOException turning on light: {}", e.getMessage());
127         } catch (DoorbirdUnauthorizedException e) {
128             logAuthorizationError("lightOn");
129         }
130     }
131
132     public void restart() {
133         try {
134             String response = executeGetRequest("/bha-api/restart.cgi");
135             logger.debug("Response={}", response);
136         } catch (IOException e) {
137             logger.debug("IOException restarting device: {}", e.getMessage());
138         } catch (DoorbirdUnauthorizedException e) {
139             logAuthorizationError("restart");
140         }
141     }
142
143     public void sipHangup() {
144         try {
145             String response = executeGetRequest("/bha-api/sip.cgi?action=hangup");
146             logger.debug("Response={}", response);
147         } catch (IOException e) {
148             logger.debug("IOException hanging up SIP call: {}", e.getMessage());
149         } catch (DoorbirdUnauthorizedException e) {
150             logAuthorizationError("sipHangup");
151         }
152     }
153
154     public @Nullable DoorbirdImage downloadCurrentImage() {
155         return downloadImage("/bha-api/image.cgi");
156     }
157
158     public @Nullable DoorbirdImage downloadDoorbellHistoryImage(String imageNumber) {
159         return downloadImage("/bha-api/history.cgi?event=doorbell&index=" + imageNumber);
160     }
161
162     public @Nullable DoorbirdImage downloadMotionHistoryImage(String imageNumber) {
163         return downloadImage("/bha-api/history.cgi?event=motionsensor&index=" + imageNumber);
164     }
165
166     public void sendAudio(InputStream audioInputStream) {
167         Authorization auth = authorization;
168         HttpClient client = httpClient;
169         if (client == null) {
170             logger.warn("Unable to send audio because httpClient is not set");
171             return;
172         }
173         if (auth == null) {
174             logAuthorizationError("audio-transmit");
175             return;
176         }
177         String url = buildUrl(auth, "/bha-api/audio-transmit.cgi");
178         logger.debug("Executing doorbird API post audio: {}", url);
179         DeferredContentProvider content = new DeferredContentProvider();
180         try {
181             // @formatter:off
182             client.POST(url)
183                     .header("Authorization", "Basic " + auth.getAuthorization())
184                     .header("Content-Type", "audio/basic")
185                     .header("Content-Length", "9999999")
186                     .header("Connection", "Keep-Alive")
187                     .header("Cache-Control", "no-cache")
188                     .content(content)
189                     .send(complete);
190             // @formatter:on
191
192             // It is crucial to send data in small chunks to not overload the doorbird
193             // It means that we have to wait the appropriate amount of time between chunk to send
194             // real time data, as if it were live spoken.
195             int nbByteRead = -1;
196             long nextChunkSendTimeStamp = 0;
197             do {
198                 byte[] data = new byte[CHUNK_SIZE];
199                 nbByteRead = audioInputStream.read(data);
200                 if (nbByteRead > 0) {
201                     if (nbByteRead != CHUNK_SIZE) {
202                         data = Arrays.copyOf(data, nbByteRead);
203                     } // compute exact waiting time needed, by checking previous estimation against current time
204                     long timeToWait = Math.max(0, nextChunkSendTimeStamp - System.currentTimeMillis());
205                     Thread.sleep(timeToWait);
206                     logger.debug("Sending chunk...");
207                     content.offer(ByteBuffer.wrap(data));
208                 }
209                 nextChunkSendTimeStamp = System.currentTimeMillis() + 30;
210             } while (nbByteRead != -1);
211         } catch (InterruptedException | IOException e) {
212             logger.info("Unable to communicate with Doorbird", e);
213         } finally {
214             content.close();
215         }
216     }
217
218     public void openDoorController(String controllerId, String doorNumber) {
219         openDoor("/bha-api/open-door.cgi?r=" + controllerId + "@" + doorNumber);
220     }
221
222     public void openDoorDoorbell(String doorNumber) {
223         openDoor("/bha-api/open-door.cgi?r=" + doorNumber);
224     }
225
226     private void openDoor(String urlFragment) {
227         try {
228             String response = executeGetRequest(urlFragment);
229             logger.debug("Response={}", response);
230         } catch (IOException e) {
231             logger.debug("IOException opening door: {}", e.getMessage());
232         } catch (DoorbirdUnauthorizedException e) {
233             logAuthorizationError("openDoor");
234         }
235     }
236
237     private @Nullable synchronized DoorbirdImage downloadImage(String urlFragment) {
238         Authorization auth = authorization;
239         if (auth == null) {
240             logAuthorizationError("downloadImage");
241             return null;
242         }
243         HttpClient client = httpClient;
244         if (client == null) {
245             logger.info("Unable to download image because httpClient is not set");
246             return null;
247         }
248         String errorMsg;
249         try {
250             String url = buildUrl(auth, urlFragment);
251             logger.debug("Downloading image from doorbird: {}", url);
252             Request request = client.newRequest(url);
253             request.method(HttpMethod.GET);
254             request.header("Authorization", "Basic " + auth.getAuthorization());
255             request.timeout(API_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
256
257             ContentResponse contentResponse = request.send();
258             switch (contentResponse.getStatus()) {
259                 case HttpStatus.OK_200:
260                     DoorbirdImage doorbirdImage = new DoorbirdImage();
261                     doorbirdImage.setImage(new RawType(contentResponse.getContent(),
262                             contentResponse.getHeaders().get(HttpHeader.CONTENT_TYPE)));
263                     doorbirdImage.setTimestamp(convertXTimestamp(contentResponse.getHeaders().get("X-Timestamp")));
264                     return doorbirdImage;
265
266                 default:
267                     errorMsg = String.format("HTTP GET failed: %d, %s", contentResponse.getStatus(),
268                             contentResponse.getReason());
269                     break;
270             }
271         } catch (TimeoutException e) {
272             errorMsg = "TimeoutException: Call to Doorbird API timed out";
273         } catch (ExecutionException e) {
274             errorMsg = String.format("ExecutionException: %s", e.getMessage());
275         } catch (InterruptedException e) {
276             errorMsg = String.format("InterruptedException: %s", e.getMessage());
277             Thread.currentThread().interrupt();
278         }
279         logger.debug("{}", errorMsg);
280         return null;
281     }
282
283     private long convertXTimestamp(@Nullable String timestamp) {
284         // Convert Unix Epoch string timestamp to long value
285         // Use current time if passed null string or if conversion fails
286         long value;
287         if (timestamp != null) {
288             try {
289                 value = Integer.parseInt(timestamp);
290             } catch (NumberFormatException e) {
291                 logger.debug("X-Timestamp header is not a number: {}", timestamp);
292                 value = ZonedDateTime.now().toEpochSecond();
293             }
294         } else {
295             value = ZonedDateTime.now().toEpochSecond();
296         }
297         return value;
298     }
299
300     private String buildUrl(Authorization auth, String path) {
301         return "http://" + auth.getHost() + path;
302     }
303
304     private synchronized String executeGetRequest(String urlFragment)
305             throws IOException, DoorbirdUnauthorizedException {
306         Authorization auth = authorization;
307         if (auth == null) {
308             throw new DoorbirdUnauthorizedException();
309         }
310         String url = buildUrl(auth, urlFragment);
311         logger.debug("Executing doorbird API request: {}", url);
312         // @formatter:off
313         return HttpRequestBuilder.getFrom(url)
314             .withTimeout(Duration.ofSeconds(API_REQUEST_TIMEOUT_SECONDS))
315             .withHeader("Authorization", "Basic " + auth.getAuthorization())
316             .withHeader("charset", "utf-8")
317             .withHeader("Accept-language", "en-us")
318             .getContentAsString();
319         // @formatter:on
320     }
321
322     private void logAuthorizationError(String operation) {
323         logger.info("Authorization info is not set or is incorrect on call to '{}' API", operation);
324     }
325 }