]> git.basschouten.com Git - openhab-addons.git/blob
90b15db2b80cd6fe56b6fce42eb941b72a359811
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.tapocontrol.internal.api;
14
15 import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
16 import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorConstants.*;
17 import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.*;
18
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.TimeoutException;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpResponse;
25 import org.eclipse.jetty.client.api.ContentResponse;
26 import org.eclipse.jetty.client.api.Request;
27 import org.eclipse.jetty.client.api.Result;
28 import org.eclipse.jetty.client.util.BufferingResponseListener;
29 import org.eclipse.jetty.client.util.StringContentProvider;
30 import org.eclipse.jetty.http.HttpMethod;
31 import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
32 import org.openhab.binding.tapocontrol.internal.device.TapoDevice;
33 import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
34 import org.openhab.binding.tapocontrol.internal.helpers.TapoCipher;
35 import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 import com.google.gson.Gson;
40 import com.google.gson.JsonObject;
41
42 /**
43  * Handler class for TAPO Smart Home device connections.
44  * This class uses synchronous HttpClient-Requests for login to device
45  *
46  * @author Christian Wild - Initial contribution
47  */
48 @NonNullByDefault
49 public class TapoDeviceHttpApi {
50     private final Logger logger = LoggerFactory.getLogger(TapoDeviceHttpApi.class);
51     private final String uid;
52     private final TapoCipher tapoCipher;
53     private final TapoBridgeHandler bridge;
54     private Gson gson;
55     private String token = "";
56     private String cookie = "";
57     protected String deviceURL = "";
58     protected String ipAddress = "";
59
60     /**
61      * INIT CLASS
62      *
63      * @param config TapoControlConfiguration class
64      */
65     public TapoDeviceHttpApi(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
66         this.bridge = bridgeThingHandler;
67         this.tapoCipher = new TapoCipher();
68         this.gson = new Gson();
69         this.uid = device.getThingUID().getAsString();
70         String ipAddress = device.getIpAddress();
71         setDeviceURL(ipAddress);
72     }
73
74     /***********************************
75      *
76      * DELEGATING FUNCTIONS
77      * will normaly be delegated to extension-classes(TapoDeviceConnector)
78      *
79      ************************************/
80     /**
81      * handle SuccessResponse (setDeviceInfo)
82      * 
83      * @param responseBody String with responseBody from device
84      */
85     protected void handleSuccessResponse(String responseBody) {
86     }
87
88     /**
89      * handle JsonResponse (getDeviceInfo)
90      * 
91      * @param responseBody String with responseBody from device
92      */
93     protected void handleDeviceResult(String responseBody) {
94     }
95
96     /**
97      * handle custom response
98      * 
99      * @param responseBody String with responseBody from device
100      */
101     protected void handleCustomResponse(String responseBody) {
102     }
103
104     /**
105      * handle error
106      * 
107      * @param te TapoErrorHandler
108      */
109     protected void handleError(TapoErrorHandler tapoError) {
110     }
111
112     /***********************************
113      *
114      * LOGIN FUNCTIONS
115      *
116      ************************************/
117
118     /**
119      * Create Handshake and set cookie
120      *
121      * @return true if handshake (cookie) was created
122      */
123     protected String createHandshake() {
124         String cookie = "";
125         try {
126             /* create payload for handshake */
127             PayloadBuilder plBuilder = new PayloadBuilder();
128             plBuilder.method = "handshake";
129             plBuilder.addParameter("key", bridge.getCredentials().getPublicKey()); // ?.decode("UTF-8")
130             String payload = plBuilder.getPayload();
131
132             /* send request (create ) */
133             logger.trace("({}) create handhsake with payload: {}", uid, payload.toString());
134             ContentResponse response = sendRequest(this.deviceURL, payload);
135             if (response != null && getErrorCode(response) == 0) {
136                 String encryptedKey = getKeyFromResponse(response);
137                 this.tapoCipher.setKey(encryptedKey, bridge.getCredentials());
138                 cookie = getCookieFromResponse(response);
139             }
140         } catch (Exception e) {
141             logger.debug("({}) could not createHandshake: {}", uid, e.toString());
142             handleError(new TapoErrorHandler(ERR_HAND_SHAKE_FAILED, "could not createHandshake"));
143         }
144         return cookie;
145     }
146
147     /**
148      * return encrypted key from 'handshake' request
149      * 
150      * @param response ContentResponse from "handshake" method
151      * @return
152      */
153     private String getKeyFromResponse(ContentResponse response) {
154         String rBody = response.getContentAsString();
155         JsonObject jsonObj = gson.fromJson(rBody, JsonObject.class);
156         if (jsonObj != null) {
157             logger.trace("({}) received awnser: {}", uid, rBody);
158             return jsonObjectToString(jsonObj.getAsJsonObject("result"), "key");
159         } else {
160             logger.warn("({}) could not getKeyFromResponse '{}'", uid, rBody);
161             handleError(new TapoErrorHandler(ERR_HAND_SHAKE_FAILED, "could not getKeyFromResponse"));
162         }
163         return "";
164     }
165
166     /**
167      * return cookie from 'handshake' request
168      * 
169      * @param response ContentResponse from "handshake" metho
170      * @return
171      */
172     private String getCookieFromResponse(ContentResponse response) {
173         String cookie = "";
174         try {
175             cookie = response.getHeaders().get("Set-Cookie").split(";")[0];
176             logger.trace("({}) got cookie: '{}'", uid, cookie);
177         } catch (Exception e) {
178             logger.warn("({}) could not getCookieFromResponse", uid);
179             handleError(new TapoErrorHandler(ERR_HAND_SHAKE_FAILED, "could not getCookieFromResponse"));
180         }
181         return cookie;
182     }
183
184     /**
185      * Query Token from device
186      * 
187      * @return String with token returned from device
188      */
189     protected String queryToken() {
190         String token = "";
191         try {
192             /* encrypt login credentials */
193             PayloadBuilder plBuilder = new PayloadBuilder();
194             plBuilder.method = "login_device";
195             plBuilder.addParameter("username", bridge.getCredentials().getEncodedEmail());
196             plBuilder.addParameter("password", bridge.getCredentials().getEncodedPassword());
197             String payload = plBuilder.getPayload();
198             String encryptedPayload = this.encryptPayload(payload);
199
200             /* create secured login informations */
201             plBuilder = new PayloadBuilder();
202             plBuilder.method = "securePassthrough";
203             plBuilder.addParameter("request", encryptedPayload);
204             String securePassthroughPayload = plBuilder.getPayload();
205
206             /* sendRequest and get Token */
207             ContentResponse response = sendRequest(deviceURL, securePassthroughPayload);
208             token = getTokenFromResponse(response);
209         } catch (Exception e) {
210             logger.debug("({}) error building login payload: {}", uid, e.toString());
211             handleError(new TapoErrorHandler(e, "error building login payload"));
212         }
213         return token;
214     }
215
216     /**
217      * get Token from "login"-request
218      * 
219      * @param response
220      * @return
221      */
222     private String getTokenFromResponse(@Nullable ContentResponse response) {
223         String result = "";
224         TapoErrorHandler tapoError = new TapoErrorHandler();
225         if (response != null && response.getStatus() == 200) {
226             String rBody = response.getContentAsString();
227             String decryptedResponse = this.decryptResponse(rBody);
228             logger.trace("({}) received result: {}", uid, decryptedResponse);
229
230             /* get errocode (0=success) */
231             JsonObject jsonObject = gson.fromJson(decryptedResponse, JsonObject.class);
232             if (jsonObject != null) {
233                 Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_JSON_DECODE_FAIL);
234                 if (errorCode == 0) {
235                     /* return result if set / else request was successfull */
236                     result = jsonObjectToString(jsonObject.getAsJsonObject("result"), "token");
237                 } else {
238                     /* return errorcode from device */
239                     tapoError.raiseError(errorCode, "could not get token");
240                     logger.debug("({}) login recieved errorCode {} - {}", uid, errorCode, tapoError.getMessage());
241                 }
242             } else {
243                 logger.debug("({}) unexpected json-response '{}'", uid, decryptedResponse);
244                 tapoError.raiseError(ERR_JSON_ENCODE_FAIL, "could not get token");
245             }
246         } else {
247             logger.debug("({}) invalid response while login", uid);
248             tapoError.raiseError(ERR_HTTP_RESPONSE, "invalid response while login");
249         }
250         /* handle error */
251         if (tapoError.hasError()) {
252             handleError(tapoError);
253         }
254         return result;
255     }
256
257     /***********************************
258      *
259      * HTTP-ACTIONS
260      *
261      ************************************/
262     /**
263      * SEND SYNCHRON HTTP-REQUEST
264      * 
265      * @param url url request is sent to
266      * @param payload payload (String) to send
267      * @return ContentResponse of request
268      */
269     @Nullable
270     protected ContentResponse sendRequest(String url, String payload) {
271         logger.trace("({}) sendRequest to '{}' with cookie '{}'", uid, url, this.cookie);
272
273         Request httpRequest = bridge.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
274
275         /* set header */
276         httpRequest = setHeaders(httpRequest);
277         httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
278
279         /* add request body */
280         httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
281
282         try {
283             ContentResponse httpResponse = httpRequest.send();
284             return httpResponse;
285         } catch (InterruptedException e) {
286             logger.debug("({}) sending request interrupted: {}", uid, e.toString());
287             handleError(new TapoErrorHandler(e));
288         } catch (TimeoutException e) {
289             logger.debug("({}) sending request timeout: {}", uid, e.toString());
290             handleError(new TapoErrorHandler(ERR_CONNECT_TIMEOUT, e.toString()));
291         } catch (Exception e) {
292             logger.debug("({}) sending request failed: {}", uid, e.toString());
293             handleError(new TapoErrorHandler(e));
294         }
295         return null;
296     }
297
298     /**
299      * SEND ASYNCHRONOUS HTTP-REQUEST
300      * (don't wait for awnser with programm code)
301      * 
302      * @param url string url request is sent to
303      * @param payload data-payload
304      * @param command command executed - this will handle RepsonseType
305      */
306     protected void sendAsyncRequest(String url, String payload, String command) {
307         logger.trace("({}) sendAsncRequest to '{}' with cookie '{}'", uid, url, this.cookie);
308         try {
309             Request httpRequest = bridge.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
310
311             /* set header */
312             httpRequest = setHeaders(httpRequest);
313
314             /* add request body */
315             httpRequest.content(new StringContentProvider(payload, CONTENT_CHARSET), CONTENT_TYPE_JSON);
316
317             httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
318                 @NonNullByDefault({})
319                 @Override
320                 public void onComplete(Result result) {
321                     final HttpResponse response = (HttpResponse) result.getResponse();
322                     if (result.getFailure() != null) {
323                         /* handle result errors */
324                         Throwable e = result.getFailure();
325                         String errorMessage = getValueOrDefault(e.getMessage(), "");
326                         if (e instanceof TimeoutException) {
327                             logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
328                             handleError(new TapoErrorHandler(ERR_CONNECT_TIMEOUT, errorMessage));
329                         } else {
330                             logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
331                             handleError(new TapoErrorHandler(new Exception(e), errorMessage));
332                         }
333                     } else if (response.getStatus() != 200) {
334                         logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
335                         handleError(new TapoErrorHandler(ERR_HTTP_RESPONSE, getContentAsString()));
336                     } else {
337                         /* request succesfull */
338                         String rBody = getContentAsString();
339                         rBody = decryptResponse(rBody);
340                         logger.trace("({}) requestCompleted '{}'", uid, rBody);
341                         /* handle result */
342                         switch (command) {
343                             case DEVICE_CMD_SETINFO:
344                                 handleSuccessResponse(rBody);
345                                 break;
346                             case DEVICE_CMD_GETINFO:
347                                 handleDeviceResult(rBody);
348                                 break;
349                             case DEVICE_CMD_CUSTOM:
350                                 handleCustomResponse(rBody);
351                                 break;
352                         }
353                     }
354                 }
355             });
356         } catch (Exception e) {
357             handleError(new TapoErrorHandler(e));
358         }
359     }
360
361     /**
362      * return error code from response
363      * 
364      * @param response
365      * @return 0 if request was successfull
366      */
367     protected Integer getErrorCode(@Nullable ContentResponse response) {
368         try {
369             if (response != null) {
370                 String responseBody = response.getContentAsString();
371                 return getErrorCode(responseBody);
372             } else {
373                 return ERR_HTTP_RESPONSE;
374             }
375         } catch (Exception e) {
376             return ERR_HTTP_RESPONSE;
377         }
378     }
379
380     /**
381      * return error code from responseBody
382      * 
383      * @param responseBody
384      * @return 0 if request was successfull
385      */
386     protected Integer getErrorCode(String responseBody) {
387         try {
388             JsonObject jsonObject = gson.fromJson(responseBody, JsonObject.class);
389             /* get errocode (0=success) */
390             Integer errorCode = jsonObjectToInt(jsonObject, "error_code", ERR_JSON_DECODE_FAIL);
391             if (errorCode == 0) {
392                 return 0;
393             } else {
394                 logger.debug("({}) device returns errorcode '{}'", uid, errorCode);
395                 handleError(new TapoErrorHandler(errorCode));
396                 return errorCode;
397             }
398         } catch (Exception e) {
399             return ERR_HTTP_RESPONSE;
400         }
401     }
402
403     /**
404      * SET HTTP-HEADERS
405      */
406     private Request setHeaders(Request httpRequest) {
407         /* set header */
408         httpRequest.header("content-type", CONTENT_TYPE_JSON);
409         httpRequest.header("Accept", CONTENT_TYPE_JSON);
410         if (!this.cookie.isEmpty()) {
411             httpRequest.header(HTTP_AUTH_TYPE_COOKIE, this.cookie);
412         }
413         return httpRequest;
414     }
415
416     /***********************************
417      *
418      * ENCRYPTION / CODING
419      *
420      ************************************/
421
422     /**
423      * Decrypt Response
424      * 
425      * @param responseBody encrypted string from response-body
426      * @return String decrypted responseBody
427      */
428     protected String decryptResponse(String responseBody) {
429         try {
430             JsonObject jsonObject = gson.fromJson(responseBody, JsonObject.class);
431             if (jsonObject != null) {
432                 String encryptedResponse = jsonObjectToString(jsonObject.getAsJsonObject("result"), "response");
433                 return tapoCipher.decode(encryptedResponse);
434             } else {
435                 handleError(new TapoErrorHandler(ERR_JSON_DECODE_FAIL));
436             }
437         } catch (Exception ex) {
438             logger.debug("({}) exception '{}' decryptingResponse: '{}'", uid, ex.toString(), responseBody);
439         }
440         return responseBody;
441     }
442
443     /**
444      * encrypt payload
445      * 
446      * @param payload
447      * @return encrypted payload
448      */
449     protected String encryptPayload(String payload) {
450         try {
451             return tapoCipher.encode(payload);
452         } catch (Exception ex) {
453             logger.debug("({}) exception encoding Payload '{}'", uid, ex.toString());
454             return "";
455         }
456     }
457
458     /**
459      * perform logout (dispose cookie)
460      */
461     public void logout() {
462         logger.trace("DeviceHttpApi_logout");
463         unsetToken();
464         unsetCookie();
465     }
466
467     /***********************************
468      *
469      * GET RESULTS
470      *
471      ************************************/
472     /**
473      * Logged In
474      * 
475      * @return true if logged in
476      */
477     public Boolean loggedIn() {
478         return loggedIn(false);
479     }
480
481     /**
482      * Logged In
483      * 
484      * @param raiseError if true
485      * @return true if logged in
486      */
487     public Boolean loggedIn(Boolean raiseError) {
488         if (!this.token.isBlank() && !this.cookie.isBlank()) {
489             return true;
490         } else {
491             logger.trace("({}) not logged in", uid);
492             if (raiseError) {
493                 handleError(new TapoErrorHandler(ERR_LOGIN));
494             }
495             return false;
496         }
497     }
498
499     /***********************************
500      *
501      * SET VALUES
502      *
503      ************************************/
504
505     /**
506      * Set new ipAddress
507      * 
508      * @param new ipAdress
509      */
510     public void setDeviceURL(String ipAddress) {
511         this.ipAddress = ipAddress;
512         this.deviceURL = String.format(TAPO_DEVICE_URL, ipAddress);
513     }
514
515     /**
516      * Set new ipAdresss with token
517      * 
518      * @param ipAddress ipAddres of device
519      * @param token token from login-ressult
520      */
521     public void setDeviceURL(String ipAddress, String token) {
522         this.ipAddress = ipAddress;
523         this.deviceURL = String.format(TAPO_DEVICE_URL, ipAddress);
524     }
525
526     /**
527      * Set new token
528      * 
529      * @param deviceURL
530      * @param token
531      */
532     protected void setToken(String token) {
533         if (!token.isBlank()) {
534             String url = this.deviceURL.replaceAll("\\?token=\\w*", "");
535             this.deviceURL = url + "?token=" + token;
536         }
537         this.token = token;
538     }
539
540     /**
541      * Unset Token (device logout)
542      */
543     protected void unsetToken() {
544         this.deviceURL = this.deviceURL.replaceAll("\\?token=\\w*", "");
545         this.token = "";
546     }
547
548     /**
549      * Set new cookie
550      * 
551      * @param cookie
552      */
553     protected void setCookie(String cookie) {
554         this.cookie = cookie;
555     }
556
557     /**
558      * Unset Cookie (device logout)
559      */
560     protected void unsetCookie() {
561         bridge.getHttpClient().getCookieStore().removeAll();
562         this.cookie = "";
563     }
564 }