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;
15 import static org.openhab.binding.tapocontrol.internal.constants.TapoBindingSettings.*;
16 import static org.openhab.binding.tapocontrol.internal.constants.TapoErrorCode.*;
17 import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
18 import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.jsonObjectToInt;
20 import java.net.InetAddress;
21 import java.util.HashMap;
22 import java.util.Objects;
23 import java.util.Optional;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
27 import org.openhab.binding.tapocontrol.internal.device.TapoDevice;
28 import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
29 import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
30 import org.openhab.binding.tapocontrol.internal.structures.TapoChild;
31 import org.openhab.binding.tapocontrol.internal.structures.TapoChildData;
32 import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
33 import org.openhab.binding.tapocontrol.internal.structures.TapoEnergyData;
34 import org.openhab.binding.tapocontrol.internal.structures.TapoSubRequest;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
38 import com.google.gson.JsonObject;
41 * Handler class for TAPO Smart Home device connections.
42 * This class uses asynchronous HttpClient-Requests
44 * @author Christian Wild - Initial contribution
47 public class TapoDeviceConnector extends TapoDeviceHttpApi {
49 private final Logger logger = LoggerFactory.getLogger(TapoDeviceConnector.class);
51 private TapoDeviceInfo deviceInfo = new TapoDeviceInfo();
52 private TapoEnergyData energyData = new TapoEnergyData();
53 private TapoChildData childData = new TapoChildData();
54 private long lastQuery = 0L;
55 private long lastSent = 0L;
56 private long lastLogin = 0L;
62 * @param bridgeThingHandler
64 public TapoDeviceConnector(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
65 super(device, bridgeThingHandler);
68 /***********************************
72 ************************************/
76 * @return true if success
78 public boolean login() {
79 if (this.pingDevice()) {
80 logger.trace("({}) sending login to url '{}'", uid, deviceURL);
82 long now = System.currentTimeMillis();
83 if (now > this.lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
88 /* create ssl-handschake (cookie) */
89 String cookie = createHandshake();
90 if (!cookie.isBlank()) {
92 String token = queryToken();
96 logger.trace("({}) not done cause of min_gap '{}'", uid, TAPO_LOGIN_MIN_GAP_MS);
98 return this.loggedIn();
100 logger.debug("({}) no ping while login '{}'", uid, this.ipAddress);
101 handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE, "no ping while login"));
106 /***********************************
110 ************************************/
113 * send custom command to device
115 * @param queryMethod query method
117 public void sendCustomQuery(String queryMethod) {
119 PayloadBuilder plBuilder = new PayloadBuilder();
120 plBuilder.method = queryMethod;
121 sendCustomPayload(plBuilder);
125 * send custom command to device
127 * @param plBuilder Payloadbuilder with unencrypted payload
129 public void sendCustomPayload(PayloadBuilder plBuilder) {
130 long now = System.currentTimeMillis();
131 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
132 String payload = plBuilder.getPayload();
133 sendSecurePasstrhroug(payload, DEVICE_CMD_CUSTOM);
135 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
140 * send "set_device_info" command to device
142 * @param name Name of command to send
143 * @param value Value to send to control
145 public void sendDeviceCommand(String name, Object value) {
146 sendDeviceCommand(DEVICE_CMD_SETINFO, name, value);
150 * send "set_device_info" command to device
152 * @param method Method command belongs to
153 * @param name Name of command to send
154 * @param value Value to send to control
156 public void sendDeviceCommand(String method, String name, Object value) {
157 long now = System.currentTimeMillis();
158 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
162 PayloadBuilder plBuilder = new PayloadBuilder();
163 plBuilder.method = method;
164 plBuilder.addParameter(name, value);
165 String payload = plBuilder.getPayload();
167 sendSecurePasstrhroug(payload, method);
169 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
174 * send "set_device_info" command to child's device
176 * @param index of the child
177 * @param childProperty to modify
178 * @param value for the property
180 public void sendChildCommand(Integer index, String childProperty, Object value) {
181 long now = System.currentTimeMillis();
182 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
184 getChild(index).ifPresent(child -> {
185 child.setDeviceOn(Boolean.valueOf((Boolean) value));
186 TapoSubRequest request = new TapoSubRequest(child.getDeviceId(), DEVICE_CMD_SETINFO, child);
187 sendSecurePasstrhroug(GSON.toJson(request), request.method());
190 logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastSent);
195 * send multiple "set_device_info" commands to device
197 * @param map {@code HashMap<String, Object> (name, value of parameter)}
199 public void sendDeviceCommands(HashMap<String, Object> map) {
200 sendDeviceCommands(DEVICE_CMD_SETINFO, map);
204 * send multiple commands to device
206 * @param method Method command belongs to
207 * @param map {@code HashMap<String, Object> (name, value of parameter)}
209 public void sendDeviceCommands(String method, HashMap<String, Object> map) {
210 long now = System.currentTimeMillis();
211 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
215 PayloadBuilder plBuilder = new PayloadBuilder();
216 plBuilder.method = method;
217 for (HashMap.Entry<String, Object> entry : map.entrySet()) {
218 plBuilder.addParameter(entry.getKey(), entry.getValue());
220 String payload = plBuilder.getPayload();
222 sendSecurePasstrhroug(payload, method);
224 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
229 * Query Info from Device and refresh deviceInfo
231 public void queryInfo() {
236 * Query Info from Device and refresh deviceInfo
239 * @param ignoreGap ignore gap to last query. query anyway
241 public void queryInfo(boolean ignoreGap) {
242 logger.trace("({}) DeviceConnector_queryInfo from '{}'", uid, deviceURL);
243 queryCommand(DEVICE_CMD_GETINFO, ignoreGap);
247 * Query Info from Child Devices and refresh deviceInfo
250 public void queryChildDevices() {
251 logger.trace("({}) DeviceConnector_queryChildDevices from '{}'", uid, deviceURL);
252 queryCommand(DEVICE_CMD_CHILD_DEVICE_LIST, true);
256 * Get energy usage from device
258 public void getEnergyUsage() {
259 queryCommand(DEVICE_CMD_GETENERGY, true);
263 * Send Custom DeviceQuery
265 * @param queryCommand Command to be queried
266 * @param ignoreGap ignore gap to last query. query anyway
268 public void queryCommand(String queryCommand, boolean ignoreGap) {
269 logger.trace("({}) DeviceConnector_queryCommand '{}' from '{}'", uid, queryCommand, deviceURL);
270 long now = System.currentTimeMillis();
271 if (ignoreGap || now > this.lastQuery + TAPO_SEND_MIN_GAP_MS) {
272 this.lastQuery = now;
275 PayloadBuilder plBuilder = new PayloadBuilder();
276 plBuilder.method = queryCommand;
277 String payload = plBuilder.getPayload();
279 sendSecurePasstrhroug(payload, queryCommand);
281 logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastQuery);
286 * SEND SECUREPASSTHROUGH
287 * encprypt payload and send to device
289 * @param payload payload sent to device
290 * @param command command executed - this will handle result
292 protected void sendSecurePasstrhroug(String payload, String command) {
293 /* encrypt payload */
294 logger.trace("({}) encrypting payload '{}'", uid, payload);
295 String encryptedPayload = encryptPayload(payload);
297 /* create secured payload */
298 PayloadBuilder plBuilder = new PayloadBuilder();
299 plBuilder.method = "securePassthrough";
300 plBuilder.addParameter("request", encryptedPayload);
301 String securePassthroughPayload = plBuilder.getPayload();
303 sendAsyncRequest(deviceURL, securePassthroughPayload, command);
306 /***********************************
310 ************************************/
313 * Handle SuccessResponse (setDeviceInfo)
315 * @param responseBody String with responseBody from device
318 protected void handleSuccessResponse(String responseBody) {
319 JsonObject jsnResult = getJsonFromResponse(responseBody);
320 Integer errorCode = jsonObjectToInt(jsnResult, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
321 if (errorCode != 0) {
322 logger.debug("({}) set deviceInfo not successful: {}", uid, jsnResult);
323 this.device.handleConnectionState();
325 this.device.responsePasstrough(responseBody);
330 * handle JsonResponse (getDeviceInfo)
332 * @param responseBody String with responseBody from device
335 protected void handleDeviceResult(String responseBody) {
336 JsonObject jsnResult = getJsonFromResponse(responseBody);
337 if (jsnResult.has(JSON_KEY_ID)) {
338 this.deviceInfo = new TapoDeviceInfo(jsnResult);
339 this.device.setDeviceInfo(deviceInfo);
341 this.deviceInfo = new TapoDeviceInfo();
342 this.device.handleConnectionState();
344 this.device.responsePasstrough(responseBody);
348 * handle JsonResponse (getEnergyData)
350 * @param responseBody String with responseBody from device
353 protected void handleEnergyResult(String responseBody) {
354 JsonObject jsnResult = getJsonFromResponse(responseBody);
355 if (jsnResult.has(JSON_KEY_ENERGY_POWER)) {
356 this.energyData = new TapoEnergyData(jsnResult);
357 this.device.setEnergyData(energyData);
359 this.energyData = new TapoEnergyData();
361 this.device.responsePasstrough(responseBody);
365 * handle JsonResponse (getChildDeviceList)
367 * @param responseBody String with responseBody from device
370 protected void handleChildDevices(String responseBody) {
371 JsonObject jsnResult = getJsonFromResponse(responseBody);
372 if (jsnResult.has(JSON_KEY_CHILD_START_INDEX)) {
373 this.childData = Objects.requireNonNull(GSON.fromJson(jsnResult, TapoChildData.class));
374 this.device.setChildData(childData);
376 this.childData = new TapoChildData();
378 this.device.responsePasstrough(responseBody);
382 * handle custom response
384 * @param responseBody String with responseBody from device
387 protected void handleCustomResponse(String responseBody) {
388 this.device.responsePasstrough(responseBody);
394 * @param tapoError TapoErrorHandler
397 protected void handleError(TapoErrorHandler tapoError) {
398 this.device.setError(tapoError);
402 * get Json from response
404 * @param responseBody
405 * @return JsonObject with result
407 private JsonObject getJsonFromResponse(String responseBody) {
408 JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
409 /* get errocode (0=success) */
410 if (jsonObject != null) {
411 Integer errorCode = jsonObjectToInt(jsonObject, "error_code");
412 if (errorCode == 0) {
413 /* decrypt response */
414 jsonObject = GSON.fromJson(responseBody, JsonObject.class);
415 logger.trace("({}) received result: {}", uid, responseBody);
416 if (jsonObject != null) {
417 /* return result if set / else request was successful */
418 if (jsonObject.has("result")) {
419 return jsonObject.getAsJsonObject("result");
425 /* return errorcode from device */
426 TapoErrorHandler te = new TapoErrorHandler(errorCode, "device answers with errorcode");
427 logger.debug("({}) device answers with errorcode {} - {}", uid, errorCode, te.getMessage());
432 logger.debug("({}) sendPayload exception {}", uid, responseBody);
433 handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE));
434 return new JsonObject();
437 /***********************************
441 ************************************/
444 * Check if device is online
446 * @return true if device is online
448 public Boolean isOnline() {
449 return isOnline(false);
453 * Check if device is online
455 * @param raiseError if true
456 * @return true if device is online
458 public Boolean isOnline(Boolean raiseError) {
462 logger.trace("({}) device is offline (no ping)", uid);
464 handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE));
474 * @return String ipAdress
476 public String getIP() {
477 return this.ipAddress;
483 * @return true if ping successfull
485 public Boolean pingDevice() {
487 InetAddress address = InetAddress.getByName(this.ipAddress);
488 return address.isReachable(TAPO_PING_TIMEOUT_MS);
489 } catch (Exception e) {
490 logger.debug("({}) InetAdress throws: {}", uid, e.getMessage());
495 private Optional<TapoChild> getChild(int position) {
496 return childData.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();