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.doorbird.internal.api;
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;
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;
41 import com.google.gson.Gson;
42 import com.google.gson.JsonSyntaxException;
45 * The {@link DoorbirdAPI} class exposes the functionality provided by the Doorbird API.
47 * @author Mark Hilbush - Initial contribution
50 public final class DoorbirdAPI {
51 private static final long API_REQUEST_TIMEOUT_SECONDS = 16L;
53 // Single Gson instance shared by multiple classes
54 private static final Gson GSON = new Gson();
56 private final Logger logger = LoggerFactory.getLogger(DoorbirdAPI.class);
57 private static final int CHUNK_SIZE = 256;
59 private @Nullable Authorization authorization;
60 private @Nullable HttpClient httpClient;
62 // define a completed listener when sending audio asynchronously :
63 private Response.CompleteListener complete = new Response.CompleteListener() {
65 public void onComplete(@Nullable Result result) {
67 logger.debug("Doorbird audio sent. Response status {} {} ", result.getResponse().getStatus(),
68 result.getResponse().getReason());
73 public static Gson getGson() {
77 public static <T> T fromJson(String json, Class<T> dataClass) {
78 return GSON.fromJson(json, dataClass);
81 public void setAuthorization(String doorbirdHost, String userId, String userPassword) {
82 this.authorization = new Authorization(doorbirdHost, userId, userPassword);
85 public void setHttpClient(HttpClient httpClient) {
86 this.httpClient = httpClient;
89 public @Nullable DoorbirdInfo getDoorbirdInfo() {
90 DoorbirdInfo doorbirdInfo = null;
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");
105 public @Nullable SipStatus getSipStatus() {
106 SipStatus sipStatus = null;
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");
121 public void lightOn() {
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");
132 public void restart() {
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");
143 public void sipHangup() {
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");
154 public @Nullable DoorbirdImage downloadCurrentImage() {
155 return downloadImage("/bha-api/image.cgi");
158 public @Nullable DoorbirdImage downloadDoorbellHistoryImage(String imageNumber) {
159 return downloadImage("/bha-api/history.cgi?event=doorbell&index=" + imageNumber);
162 public @Nullable DoorbirdImage downloadMotionHistoryImage(String imageNumber) {
163 return downloadImage("/bha-api/history.cgi?event=motionsensor&index=" + imageNumber);
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");
174 logAuthorizationError("audio-transmit");
177 String url = buildUrl(auth, "/bha-api/audio-transmit.cgi");
178 logger.debug("Executing doorbird API post audio: {}", url);
179 DeferredContentProvider content = new DeferredContentProvider();
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")
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.
196 long nextChunkSendTimeStamp = 0;
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));
209 nextChunkSendTimeStamp = System.currentTimeMillis() + 30;
210 } while (nbByteRead != -1);
211 } catch (InterruptedException | IOException e) {
212 logger.info("Unable to communicate with Doorbird", e);
218 public void openDoorController(String controllerId, String doorNumber) {
219 openDoor("/bha-api/open-door.cgi?r=" + controllerId + "@" + doorNumber);
222 public void openDoorDoorbell(String doorNumber) {
223 openDoor("/bha-api/open-door.cgi?r=" + doorNumber);
226 private void openDoor(String urlFragment) {
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");
237 private @Nullable synchronized DoorbirdImage downloadImage(String urlFragment) {
238 Authorization auth = authorization;
240 logAuthorizationError("downloadImage");
243 HttpClient client = httpClient;
244 if (client == null) {
245 logger.info("Unable to download image because httpClient is not set");
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);
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;
267 errorMsg = String.format("HTTP GET failed: %d, %s", contentResponse.getStatus(),
268 contentResponse.getReason());
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();
279 logger.debug("{}", errorMsg);
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
287 if (timestamp != null) {
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();
295 value = ZonedDateTime.now().toEpochSecond();
300 private String buildUrl(Authorization auth, String path) {
301 return "http://" + auth.getHost() + path;
304 private synchronized String executeGetRequest(String urlFragment)
305 throws IOException, DoorbirdUnauthorizedException {
306 Authorization auth = authorization;
308 throw new DoorbirdUnauthorizedException();
310 String url = buildUrl(auth, urlFragment);
311 logger.debug("Executing doorbird API request: {}", url);
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();
322 private void logAuthorizationError(String operation) {
323 logger.info("Authorization info is not set or is incorrect on call to '{}' API", operation);