]> git.basschouten.com Git - openhab-addons.git/blob
4e3c59b7204d42e78472b0a0324f1ca0fa085604
[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.tapocontrol.internal.api.protocol.klap;
14
15 import static org.openhab.binding.tapocontrol.internal.TapoControlHandlerFactory.GSON;
16 import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
17 import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
18 import static org.openhab.binding.tapocontrol.internal.helpers.utils.ByteUtils.*;
19 import static org.openhab.binding.tapocontrol.internal.helpers.utils.JsonUtils.*;
20 import static org.openhab.binding.tapocontrol.internal.helpers.utils.TapoUtils.*;
21
22 import java.util.Objects;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jetty.client.HttpResponse;
28 import org.eclipse.jetty.client.api.ContentResponse;
29 import org.eclipse.jetty.client.api.Request;
30 import org.eclipse.jetty.client.api.Result;
31 import org.eclipse.jetty.client.util.BufferingResponseListener;
32 import org.eclipse.jetty.client.util.BytesContentProvider;
33 import org.eclipse.jetty.client.util.StringContentProvider;
34 import org.eclipse.jetty.http.HttpMethod;
35 import org.openhab.binding.tapocontrol.internal.api.TapoConnectorInterface;
36 import org.openhab.binding.tapocontrol.internal.dto.TapoBaseRequestInterface;
37 import org.openhab.binding.tapocontrol.internal.dto.TapoRequest;
38 import org.openhab.binding.tapocontrol.internal.dto.TapoResponse;
39 import org.openhab.binding.tapocontrol.internal.helpers.TapoCredentials;
40 import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * Handler class for TAPO-KLAP-Protocol
46  *
47  * @author Christian Wild - Initial contribution
48  */
49 @NonNullByDefault
50 public class KlapProtocol implements org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface {
51
52     private final Logger logger = LoggerFactory.getLogger(KlapProtocol.class);
53     protected final TapoConnectorInterface httpDelegator;
54     private KlapSession session;
55     private String uid;
56
57     /***********************
58      * Init Class
59      **********************/
60     public KlapProtocol(TapoConnectorInterface httpDelegator) {
61         this.httpDelegator = httpDelegator;
62         session = new KlapSession(this);
63         uid = httpDelegator.getThingUID() + " / HTTP-KLAP";
64     }
65
66     @Override
67     public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
68         logger.trace("({}) login to device", uid);
69         session.reset();
70         session.login(tapoCredentials);
71         return isLoggedIn();
72     }
73
74     @Override
75     public void logout() {
76         session.reset();
77     }
78
79     @Override
80     public boolean isLoggedIn() {
81         return session.isHandshakeComplete() && session.seedIsOkay() && !session.isExpired();
82     }
83
84     /***********************
85      * Request Sender
86      **********************/
87
88     /*
89      * send synchron request - response will be handled in [responseReceived()] function
90      */
91     @Override
92     public void sendRequest(TapoRequest tapoRequest) throws TapoErrorHandler {
93         String url = getUrl();
94         String command = tapoRequest.method();
95         logger.trace("({}) sending unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
96
97         Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
98
99         /* set header */
100         httpRequest = setHeaders(httpRequest);
101         httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
102
103         /* add request body */
104         httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
105
106         try {
107             responseReceived(httpRequest.send(), command);
108         } catch (Exception e) {
109             throw new TapoErrorHandler(e, "error sending content");
110         }
111     }
112
113     /**
114      * handle asynchron request-response
115      * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
116      */
117     @Override
118     public void sendAsyncRequest(TapoBaseRequestInterface tapoRequest) throws TapoErrorHandler {
119         String url = getUrl();
120         String command = tapoRequest.method();
121         logger.trace("({}) sendAsync unencrypted request: '{}' to '{}' ", uid, tapoRequest, url);
122
123         /* encrypt request */
124         byte[] encodedBytes = session.encryptRequest(tapoRequest);
125         String encrypteString = byteArrayToHex(encodedBytes);
126         Integer ivSequence = session.getIvSequence();
127         logger.trace("({}) encrypted request is '{}' with sequence '{}'", uid, encrypteString, ivSequence);
128
129         Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
130
131         /* set header and params */
132         httpRequest = setHeaders(httpRequest);
133         httpRequest.param("seq", ivSequence.toString());
134
135         /* add request body */
136         httpRequest.content(new BytesContentProvider(encodedBytes));
137
138         httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
139             @NonNullByDefault({})
140             @Override
141             public void onComplete(Result result) {
142                 final HttpResponse response = (HttpResponse) result.getResponse();
143
144                 if (result.getFailure() != null) {
145                     /* handle result errors */
146                     Throwable e = result.getFailure();
147                     String errorMessage = getValueOrDefault(e.getMessage(), "");
148                     /* throw errors to delegator */
149                     if (e instanceof TimeoutException) {
150                         logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage);
151                         httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_CONNECT_TIMEOUT, errorMessage));
152                     } else {
153                         logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
154                         httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
155                     }
156                 } else if (response.getStatus() != 200) {
157                     logger.debug("({}) sendAsyncRequest response error'{}'", uid, response.getStatus());
158                     httpDelegator.handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, getContentAsString()));
159                 } else {
160                     /* request successful */
161                     byte[] responseBytes = getContent();
162                     try {
163                         encryptedResponseReceived(responseBytes, ivSequence, command);
164                     } catch (TapoErrorHandler tapoError) {
165                         httpDelegator.handleError(tapoError);
166                     }
167                 }
168             }
169         });
170     }
171
172     /************************
173      * RESPONSE HANDLERS
174      ************************/
175
176     /**
177      * handle synchron request-response
178      * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
179      */
180     @Override
181     public void responseReceived(ContentResponse response, String command) throws TapoErrorHandler {
182         logger.trace("({}) received response content: '{}'", uid, response.getContentAsString());
183         TapoResponse tapoResponse = getTapoResponse(response);
184         httpDelegator.handleResponse(tapoResponse, command);
185         httpDelegator.responsePasstrough(response.getContentAsString(), command);
186     }
187
188     /**
189      * handle asynchron request-response
190      * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
191      */
192     @Override
193     public void asyncResponseReceived(String content, String command) throws TapoErrorHandler {
194         try {
195             TapoResponse tapoResponse = getTapoResponse(content);
196             httpDelegator.handleResponse(tapoResponse, command);
197         } catch (TapoErrorHandler tapoError) {
198             httpDelegator.handleError(tapoError);
199         }
200     }
201
202     /**
203      * handle encrypted response. decrypt it and pass to asyncRequestReceived
204      * 
205      * @param content bytearray with encrypted payload
206      * @param ivSeq ivSequence-Number which is incremented each request
207      * @param command command was sent to device
208      * @throws TapoErrorHandler
209      */
210     public void encryptedResponseReceived(byte[] content, Integer ivSeq, String command) throws TapoErrorHandler {
211         String stringContent = byteArrayToHex(content);
212         logger.trace("({}) receivedRespose '{}'", uid, stringContent);
213         String decryptedResponse = session.decryptResponse(content, ivSeq);
214         logger.trace("({}) decrypted response: '{}'", uid, decryptedResponse);
215         asyncResponseReceived(decryptedResponse, command);
216     }
217
218     /**
219      * Get Tapo-Response from Contentresponse
220      * decrypt if is encrypted
221      */
222     protected TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
223         if (response.getStatus() == 200) {
224             return getTapoResponse(response.getContentAsString());
225         } else {
226             logger.debug("({}) invalid response received", uid);
227             throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
228         }
229     }
230
231     /**
232      * Get Tapo-Response from responsestring
233      * decrypt if is encrypted
234      */
235     protected TapoResponse getTapoResponse(String responseString) throws TapoErrorHandler {
236         if (isValidJson(responseString)) {
237             TapoResponse tapoResponse = Objects.requireNonNull(GSON.fromJson(responseString, TapoResponse.class));
238             if (tapoResponse.hasError()) {
239                 throw new TapoErrorHandler(tapoResponse.errorCode(), tapoResponse.message());
240             }
241             return tapoResponse;
242         } else {
243             logger.debug("({}) invalid response received", uid);
244             throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
245         }
246     }
247
248     /************************
249      * PRIVATE HELPERS
250      ************************/
251
252     protected String getUrl() {
253         String baseUrl = String.format(TAPO_DEVICE_URL, httpDelegator.getBaseUrl());
254         if (session.isHandshakeComplete()) {
255             return baseUrl + "/request";
256         } else {
257             return baseUrl;
258         }
259     }
260
261     /*
262      * Set HTTP-Headers
263      */
264     protected Request setHeaders(Request httpRequest) {
265         if (!session.isHandshakeComplete()) {
266             httpRequest.header("content-type", CONTENT_TYPE_JSON);
267             httpRequest.header("Accept", CONTENT_TYPE_JSON);
268         }
269         if (!session.getCookie().isBlank()) {
270             httpRequest.header(HTTP_AUTH_TYPE_COOKIE, session.getCookie());
271         }
272         return httpRequest;
273     }
274 }