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.tapocontrol.internal.api.protocol.klap;
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.*;
22 import java.util.Objects;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
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;
45 * Handler class for TAPO-KLAP-Protocol
47 * @author Christian Wild - Initial contribution
50 public class KlapProtocol implements org.openhab.binding.tapocontrol.internal.api.protocol.TapoProtocolInterface {
52 private final Logger logger = LoggerFactory.getLogger(KlapProtocol.class);
53 protected final TapoConnectorInterface httpDelegator;
54 private KlapSession session;
57 /***********************
59 **********************/
60 public KlapProtocol(TapoConnectorInterface httpDelegator) {
61 this.httpDelegator = httpDelegator;
62 session = new KlapSession(this);
63 uid = httpDelegator.getThingUID() + " / HTTP-KLAP";
67 public boolean login(TapoCredentials tapoCredentials) throws TapoErrorHandler {
68 logger.trace("({}) login to device", uid);
70 session.login(tapoCredentials);
75 public void logout() {
80 public boolean isLoggedIn() {
81 return session.isHandshakeComplete() && session.seedIsOkay() && !session.isExpired();
84 /***********************
86 **********************/
89 * send synchron request - response will be handled in [responseReceived()] function
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);
97 Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
100 httpRequest = setHeaders(httpRequest);
101 httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS);
103 /* add request body */
104 httpRequest.content(new StringContentProvider(tapoRequest.toString(), CONTENT_CHARSET), CONTENT_TYPE_JSON);
107 responseReceived(httpRequest.send(), command);
108 } catch (Exception e) {
109 throw new TapoErrorHandler(e, "error sending content");
114 * handle asynchron request-response
115 * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
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);
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);
129 Request httpRequest = httpDelegator.getHttpClient().newRequest(url).method(HttpMethod.POST.toString());
131 /* set header and params */
132 httpRequest = setHeaders(httpRequest);
133 httpRequest.param("seq", ivSequence.toString());
135 /* add request body */
136 httpRequest.content(new BytesContentProvider(encodedBytes));
138 httpRequest.timeout(TAPO_HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() {
139 @NonNullByDefault({})
141 public void onComplete(Result result) {
142 final HttpResponse response = (HttpResponse) result.getResponse();
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));
153 logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage);
154 httpDelegator.handleError(new TapoErrorHandler(new Exception(e), errorMessage));
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()));
160 /* request successful */
161 byte[] responseBytes = getContent();
163 encryptedResponseReceived(responseBytes, ivSequence, command);
164 } catch (TapoErrorHandler tapoError) {
165 httpDelegator.handleError(tapoError);
172 /************************
174 ************************/
177 * handle synchron request-response
178 * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
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);
189 * handle asynchron request-response
190 * pushes (decrypted) TapoResponse to [httpDelegator.handleResponse()]-function
193 public void asyncResponseReceived(String content, String command) throws TapoErrorHandler {
195 TapoResponse tapoResponse = getTapoResponse(content);
196 httpDelegator.handleResponse(tapoResponse, command);
197 } catch (TapoErrorHandler tapoError) {
198 httpDelegator.handleError(tapoError);
203 * handle encrypted response. decrypt it and pass to asyncRequestReceived
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
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);
219 * Get Tapo-Response from Contentresponse
220 * decrypt if is encrypted
222 protected TapoResponse getTapoResponse(ContentResponse response) throws TapoErrorHandler {
223 if (response.getStatus() == 200) {
224 return getTapoResponse(response.getContentAsString());
226 logger.debug("({}) invalid response received", uid);
227 throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
232 * Get Tapo-Response from responsestring
233 * decrypt if is encrypted
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());
243 logger.debug("({}) invalid response received", uid);
244 throw new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE, "invalid response receicved");
248 /************************
250 ************************/
252 protected String getUrl() {
253 String baseUrl = String.format(TAPO_DEVICE_URL, httpDelegator.getBaseUrl());
254 if (session.isHandshakeComplete()) {
255 return baseUrl + "/request";
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);
269 if (!session.getCookie().isBlank()) {
270 httpRequest.header(HTTP_AUTH_TYPE_COOKIE, session.getCookie());