2 * Copyright (c) 2010-2023 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.TapoErrorConstants.*;
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;
61 * @param config TapoControlConfiguration class
63 public TapoDeviceConnector(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
64 super(device, bridgeThingHandler);
67 /***********************************
71 ************************************/
75 * @return true if success
77 public boolean login() {
78 if (this.pingDevice()) {
79 logger.trace("({}) sending login to url '{}'", uid, deviceURL);
81 long now = System.currentTimeMillis();
82 if (now > this.lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
87 /* create ssl-handschake (cookie) */
88 String cookie = createHandshake();
89 if (!cookie.isBlank()) {
91 String token = queryToken();
95 logger.trace("({}) not done cause of min_gap '{}'", uid, TAPO_LOGIN_MIN_GAP_MS);
97 return this.loggedIn();
99 logger.debug("({}) no ping while login '{}'", uid, this.ipAddress);
100 handleError(new TapoErrorHandler(ERR_DEVICE_OFFLINE, "no ping while login"));
105 /***********************************
109 ************************************/
112 * send custom command to device
114 * @param plBuilder Payloadbuilder with unencrypted payload
116 public void sendCustomQuery(String queryMethod) {
118 PayloadBuilder plBuilder = new PayloadBuilder();
119 plBuilder.method = queryMethod;
120 sendCustomPayload(plBuilder);
124 * send custom command to device
126 * @param plBuilder Payloadbuilder with unencrypted payload
128 public void sendCustomPayload(PayloadBuilder plBuilder) {
129 long now = System.currentTimeMillis();
130 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
131 String payload = plBuilder.getPayload();
132 sendSecurePasstrhroug(payload, DEVICE_CMD_CUSTOM);
134 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
139 * send "set_device_info" command to device
141 * @param name Name of command to send
142 * @param value Value to send to control
144 public void sendDeviceCommand(String name, Object value) {
145 long now = System.currentTimeMillis();
146 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
150 PayloadBuilder plBuilder = new PayloadBuilder();
151 plBuilder.method = DEVICE_CMD_SETINFO;
152 plBuilder.addParameter(name, value);
153 String payload = plBuilder.getPayload();
155 sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
157 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
162 * send "set_device_info" command to child's device
164 * @param index of the child
165 * @param childProperty to modify
166 * @param value for the property
168 public void sendChildCommand(Integer index, String childProperty, Object value) {
169 long now = System.currentTimeMillis();
170 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
172 getChild(index).ifPresent(child -> {
173 child.setDeviceOn(Boolean.valueOf((Boolean) value));
174 TapoSubRequest request = new TapoSubRequest(child.getDeviceId(), DEVICE_CMD_SETINFO, child);
175 sendSecurePasstrhroug(GSON.toJson(request), request.method());
178 logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastSent);
183 * send multiple "set_device_info" commands to device
185 * @param map HashMap<String, Object> (name, value of parameter)
187 public void sendDeviceCommands(HashMap<String, Object> map) {
188 long now = System.currentTimeMillis();
189 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
193 PayloadBuilder plBuilder = new PayloadBuilder();
194 plBuilder.method = DEVICE_CMD_SETINFO;
195 for (HashMap.Entry<String, Object> entry : map.entrySet()) {
196 plBuilder.addParameter(entry.getKey(), entry.getValue());
198 String payload = plBuilder.getPayload();
200 sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
202 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
207 * Query Info from Device and refresh deviceInfo
209 public void queryInfo() {
215 * Query Info from Device and refresh deviceInfo
217 * @param ignoreGap ignore gap to last query. query anyway
219 public void queryInfo(boolean ignoreGap) {
220 logger.trace("({}) DeviceConnetor_queryInfo from '{}'", uid, deviceURL);
221 long now = System.currentTimeMillis();
222 if (ignoreGap || now > this.lastQuery + TAPO_SEND_MIN_GAP_MS) {
223 this.lastQuery = now;
226 PayloadBuilder plBuilder = new PayloadBuilder();
227 plBuilder.method = DEVICE_CMD_GETINFO;
228 String payload = plBuilder.getPayload();
230 sendSecurePasstrhroug(payload, DEVICE_CMD_GETINFO);
232 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastQuery);
237 * Query Info from Child Devices and refresh deviceInfo
240 public void queryChildDevices() {
241 logger.trace("({}) DeviceConnetor_queryChildDevices from '{}'", uid, deviceURL);
244 PayloadBuilder plBuilder = new PayloadBuilder();
245 plBuilder.method = DEVICE_CMD_CHILD_DEVICE_LIST;
246 String payload = plBuilder.getPayload();
248 sendSecurePasstrhroug(payload, DEVICE_CMD_CHILD_DEVICE_LIST);
252 * Get energy usage from device
254 public void getEnergyUsage() {
255 logger.trace("({}) DeviceConnetor_getEnergyUsage from '{}'", uid, deviceURL);
258 PayloadBuilder plBuilder = new PayloadBuilder();
259 plBuilder.method = DEVICE_CMD_GETENERGY;
260 String payload = plBuilder.getPayload();
262 sendSecurePasstrhroug(payload, DEVICE_CMD_GETENERGY);
266 * SEND SECUREPASSTHROUGH
267 * encprypt payload and send to device
269 * @param payload payload sent to device
270 * @param command command executed - this will handle result
272 protected void sendSecurePasstrhroug(String payload, String command) {
273 /* encrypt payload */
274 logger.trace("({}) encrypting payload '{}'", uid, payload);
275 String encryptedPayload = encryptPayload(payload);
277 /* create secured payload */
278 PayloadBuilder plBuilder = new PayloadBuilder();
279 plBuilder.method = "securePassthrough";
280 plBuilder.addParameter("request", encryptedPayload);
281 String securePassthroughPayload = plBuilder.getPayload();
283 sendAsyncRequest(deviceURL, securePassthroughPayload, command);
286 /***********************************
290 ************************************/
293 * Handle SuccessResponse (setDeviceInfo)
295 * @param responseBody String with responseBody from device
298 protected void handleSuccessResponse(String responseBody) {
299 JsonObject jsnResult = getJsonFromResponse(responseBody);
300 Integer errorCode = jsonObjectToInt(jsnResult, "error_code", ERR_JSON_DECODE_FAIL);
301 if (errorCode != 0) {
302 logger.debug("({}) set deviceInfo not successful: {}", uid, jsnResult);
303 this.device.handleConnectionState();
305 this.device.responsePasstrough(responseBody);
310 * handle JsonResponse (getDeviceInfo)
312 * @param responseBody String with responseBody from device
315 protected void handleDeviceResult(String responseBody) {
316 JsonObject jsnResult = getJsonFromResponse(responseBody);
317 if (jsnResult.has(DEVICE_PROPERTY_ID)) {
318 this.deviceInfo = new TapoDeviceInfo(jsnResult);
319 this.device.setDeviceInfo(deviceInfo);
321 this.deviceInfo = new TapoDeviceInfo();
322 this.device.handleConnectionState();
324 this.device.responsePasstrough(responseBody);
328 * handle JsonResponse (getEnergyData)
330 * @param responseBody String with responseBody from device
333 protected void handleEnergyResult(String responseBody) {
334 JsonObject jsnResult = getJsonFromResponse(responseBody);
335 if (jsnResult.has(ENERGY_PROPERTY_POWER)) {
336 this.energyData = new TapoEnergyData(jsnResult);
337 this.device.setEnergyData(energyData);
339 this.energyData = new TapoEnergyData();
341 this.device.responsePasstrough(responseBody);
345 * handle JsonResponse (getChildDeviceList)
347 * @param responseBody String with responseBody from device
350 protected void handleChildDevices(String responseBody) {
351 JsonObject jsnResult = getJsonFromResponse(responseBody);
352 if (jsnResult.has(CHILD_PROPERTY_START_INDEX)) {
353 this.childData = Objects.requireNonNull(GSON.fromJson(jsnResult, TapoChildData.class));
354 this.device.setChildData(childData);
356 this.childData = new TapoChildData();
358 this.device.responsePasstrough(responseBody);
362 * handle custom response
364 * @param responseBody String with responseBody from device
367 protected void handleCustomResponse(String responseBody) {
368 this.device.responsePasstrough(responseBody);
374 * @param te TapoErrorHandler
377 protected void handleError(TapoErrorHandler tapoError) {
378 this.device.setError(tapoError);
382 * get Json from response
384 * @param responseBody
385 * @return JsonObject with result
387 private JsonObject getJsonFromResponse(String responseBody) {
388 JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
389 /* get errocode (0=success) */
390 if (jsonObject != null) {
391 Integer errorCode = jsonObjectToInt(jsonObject, "error_code");
392 if (errorCode == 0) {
393 /* decrypt response */
394 jsonObject = GSON.fromJson(responseBody, JsonObject.class);
395 logger.trace("({}) received result: {}", uid, responseBody);
396 if (jsonObject != null) {
397 /* return result if set / else request was successful */
398 if (jsonObject.has("result")) {
399 return jsonObject.getAsJsonObject("result");
405 /* return errorcode from device */
406 TapoErrorHandler te = new TapoErrorHandler(errorCode, "device answers with errorcode");
407 logger.debug("({}) device answers with errorcode {} - {}", uid, errorCode, te.getMessage());
412 logger.debug("({}) sendPayload exception {}", uid, responseBody);
413 handleError(new TapoErrorHandler(ERR_HTTP_RESPONSE));
414 return new JsonObject();
417 /***********************************
421 ************************************/
424 * Check if device is online
426 * @return true if device is online
428 public Boolean isOnline() {
429 return isOnline(false);
433 * Check if device is online
435 * @param raiseError if true
436 * @return true if device is online
438 public Boolean isOnline(Boolean raiseError) {
442 logger.trace("({}) device is offline (no ping)", uid);
444 handleError(new TapoErrorHandler(ERR_DEVICE_OFFLINE));
454 * @return String ipAdress
456 public String getIP() {
457 return this.ipAddress;
463 * @return true if ping successfull
465 public Boolean pingDevice() {
467 InetAddress address = InetAddress.getByName(this.ipAddress);
468 return address.isReachable(TAPO_PING_TIMEOUT_MS);
469 } catch (Exception e) {
470 logger.debug("({}) InetAdress throws: {}", uid, e.getMessage());
475 private Optional<TapoChild> getChild(int position) {
476 return childData.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();