]> git.basschouten.com Git - openhab-addons.git/blob
7bd5af66773dbc708796cbeba509b01380d310f4
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.TapoErrorCode.*;
17 import static org.openhab.binding.tapocontrol.internal.constants.TapoThingConstants.*;
18 import static org.openhab.binding.tapocontrol.internal.helpers.TapoUtils.jsonObjectToInt;
19
20 import java.net.InetAddress;
21 import java.util.HashMap;
22 import java.util.Objects;
23 import java.util.Optional;
24
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;
37
38 import com.google.gson.JsonObject;
39
40 /**
41  * Handler class for TAPO Smart Home device connections.
42  * This class uses asynchronous HttpClient-Requests
43  *
44  * @author Christian Wild - Initial contribution
45  */
46 @NonNullByDefault
47 public class TapoDeviceConnector extends TapoDeviceHttpApi {
48
49     private final Logger logger = LoggerFactory.getLogger(TapoDeviceConnector.class);
50
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;
57
58     /**
59      * INIT CLASS
60      *
61      * @param config TapoControlConfiguration class
62      */
63     public TapoDeviceConnector(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
64         super(device, bridgeThingHandler);
65     }
66
67     /***********************************
68      *
69      * LOGIN FUNCTIONS
70      *
71      ************************************/
72     /**
73      * login
74      *
75      * @return true if success
76      */
77     public boolean login() {
78         if (this.pingDevice()) {
79             logger.trace("({}) sending login to url '{}'", uid, deviceURL);
80
81             long now = System.currentTimeMillis();
82             if (now > this.lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
83                 this.lastLogin = now;
84                 unsetToken();
85                 unsetCookie();
86
87                 /* create ssl-handschake (cookie) */
88                 String cookie = createHandshake();
89                 if (!cookie.isBlank()) {
90                     setCookie(cookie);
91                     String token = queryToken();
92                     setToken(token);
93                 }
94             } else {
95                 logger.trace("({}) not done cause of min_gap '{}'", uid, TAPO_LOGIN_MIN_GAP_MS);
96             }
97             return this.loggedIn();
98         } else {
99             logger.debug("({}) no ping while login '{}'", uid, this.ipAddress);
100             handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE, "no ping while login"));
101             return false;
102         }
103     }
104
105     /***********************************
106      *
107      * DEVICE ACTIONS
108      *
109      ************************************/
110
111     /**
112      * send custom command to device
113      *
114      * @param plBuilder Payloadbuilder with unencrypted payload
115      */
116     public void sendCustomQuery(String queryMethod) {
117         /* create payload */
118         PayloadBuilder plBuilder = new PayloadBuilder();
119         plBuilder.method = queryMethod;
120         sendCustomPayload(plBuilder);
121     }
122
123     /**
124      * send custom command to device
125      *
126      * @param plBuilder Payloadbuilder with unencrypted payload
127      */
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);
133         } else {
134             logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
135         }
136     }
137
138     /**
139      * send "set_device_info" command to device
140      *
141      * @param name Name of command to send
142      * @param value Value to send to control
143      */
144     public void sendDeviceCommand(String name, Object value) {
145         sendDeviceCommand(DEVICE_CMD_SETINFO, name, value);
146     }
147
148     /**
149      * send "set_device_info" command to device
150      *
151      * @param method Method command belongs to
152      * @param name Name of command to send
153      * @param value Value to send to control
154      */
155     public void sendDeviceCommand(String method, String name, Object value) {
156         long now = System.currentTimeMillis();
157         if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
158             this.lastSent = now;
159
160             /* create payload */
161             PayloadBuilder plBuilder = new PayloadBuilder();
162             plBuilder.method = method;
163             plBuilder.addParameter(name, value);
164             String payload = plBuilder.getPayload();
165
166             sendSecurePasstrhroug(payload, method);
167         } else {
168             logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
169         }
170     }
171
172     /**
173      * send "set_device_info" command to child's device
174      *
175      * @param index of the child
176      * @param childProperty to modify
177      * @param value for the property
178      */
179     public void sendChildCommand(Integer index, String childProperty, Object value) {
180         long now = System.currentTimeMillis();
181         if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
182             this.lastSent = now;
183             getChild(index).ifPresent(child -> {
184                 child.setDeviceOn(Boolean.valueOf((Boolean) value));
185                 TapoSubRequest request = new TapoSubRequest(child.getDeviceId(), DEVICE_CMD_SETINFO, child);
186                 sendSecurePasstrhroug(GSON.toJson(request), request.method());
187             });
188         } else {
189             logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastSent);
190         }
191     }
192
193     /**
194      * send multiple "set_device_info" commands to device
195      *
196      * @param map HashMap<String, Object> (name, value of parameter)
197      */
198     public void sendDeviceCommands(HashMap<String, Object> map) {
199         sendDeviceCommands(DEVICE_CMD_SETINFO, map);
200     }
201
202     /**
203      * send multiple commands to device
204      *
205      * @param method Method command belongs to
206      * @param map HashMap<String, Object> (name, value of parameter)
207      */
208     public void sendDeviceCommands(String method, HashMap<String, Object> map) {
209         long now = System.currentTimeMillis();
210         if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
211             this.lastSent = now;
212
213             /* create payload */
214             PayloadBuilder plBuilder = new PayloadBuilder();
215             plBuilder.method = method;
216             for (HashMap.Entry<String, Object> entry : map.entrySet()) {
217                 plBuilder.addParameter(entry.getKey(), entry.getValue());
218             }
219             String payload = plBuilder.getPayload();
220
221             sendSecurePasstrhroug(payload, method);
222         } else {
223             logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
224         }
225     }
226
227     /**
228      * Query Info from Device and refresh deviceInfo
229      */
230     public void queryInfo() {
231         queryInfo(false);
232     }
233
234     /**
235      * Query Info from Device and refresh deviceInfo
236      *
237      *
238      * @param ignoreGap ignore gap to last query. query anyway
239      */
240     public void queryInfo(boolean ignoreGap) {
241         logger.trace("({}) DeviceConnector_queryInfo from '{}'", uid, deviceURL);
242         queryCommand(DEVICE_CMD_GETINFO, ignoreGap);
243     }
244
245     /**
246      * Query Info from Child Devices and refresh deviceInfo
247      */
248     @Override
249     public void queryChildDevices() {
250         logger.trace("({}) DeviceConnector_queryChildDevices from '{}'", uid, deviceURL);
251         queryCommand(DEVICE_CMD_CHILD_DEVICE_LIST, true);
252     }
253
254     /**
255      * Get energy usage from device
256      */
257     public void getEnergyUsage() {
258         queryCommand(DEVICE_CMD_GETENERGY, true);
259     }
260
261     /**
262      * Send Custom DeviceQuery
263      *
264      * @param queryCommand Command to be queried
265      * @param ignoreGap ignore gap to last query. query anyway
266      */
267     public void queryCommand(String queryCommand, boolean ignoreGap) {
268         logger.trace("({}) DeviceConnector_queryCommand '{}' from '{}'", uid, queryCommand, deviceURL);
269         long now = System.currentTimeMillis();
270         if (ignoreGap || now > this.lastQuery + TAPO_SEND_MIN_GAP_MS) {
271             this.lastQuery = now;
272
273             /* create payload */
274             PayloadBuilder plBuilder = new PayloadBuilder();
275             plBuilder.method = queryCommand;
276             String payload = plBuilder.getPayload();
277
278             sendSecurePasstrhroug(payload, queryCommand);
279         } else {
280             logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastQuery);
281         }
282     }
283
284     /**
285      * SEND SECUREPASSTHROUGH
286      * encprypt payload and send to device
287      *
288      * @param payload payload sent to device
289      * @param command command executed - this will handle result
290      */
291     protected void sendSecurePasstrhroug(String payload, String command) {
292         /* encrypt payload */
293         logger.trace("({}) encrypting payload '{}'", uid, payload);
294         String encryptedPayload = encryptPayload(payload);
295
296         /* create secured payload */
297         PayloadBuilder plBuilder = new PayloadBuilder();
298         plBuilder.method = "securePassthrough";
299         plBuilder.addParameter("request", encryptedPayload);
300         String securePassthroughPayload = plBuilder.getPayload();
301
302         sendAsyncRequest(deviceURL, securePassthroughPayload, command);
303     }
304
305     /***********************************
306      *
307      * HANDLE RESPONSES
308      *
309      ************************************/
310
311     /**
312      * Handle SuccessResponse (setDeviceInfo)
313      *
314      * @param responseBody String with responseBody from device
315      */
316     @Override
317     protected void handleSuccessResponse(String responseBody) {
318         JsonObject jsnResult = getJsonFromResponse(responseBody);
319         Integer errorCode = jsonObjectToInt(jsnResult, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
320         if (errorCode != 0) {
321             logger.debug("({}) set deviceInfo not successful: {}", uid, jsnResult);
322             this.device.handleConnectionState();
323         }
324         this.device.responsePasstrough(responseBody);
325     }
326
327     /**
328      *
329      * handle JsonResponse (getDeviceInfo)
330      *
331      * @param responseBody String with responseBody from device
332      */
333     @Override
334     protected void handleDeviceResult(String responseBody) {
335         JsonObject jsnResult = getJsonFromResponse(responseBody);
336         if (jsnResult.has(JSON_KEY_ID)) {
337             this.deviceInfo = new TapoDeviceInfo(jsnResult);
338             this.device.setDeviceInfo(deviceInfo);
339         } else {
340             this.deviceInfo = new TapoDeviceInfo();
341             this.device.handleConnectionState();
342         }
343         this.device.responsePasstrough(responseBody);
344     }
345
346     /**
347      * handle JsonResponse (getEnergyData)
348      *
349      * @param responseBody String with responseBody from device
350      */
351     @Override
352     protected void handleEnergyResult(String responseBody) {
353         JsonObject jsnResult = getJsonFromResponse(responseBody);
354         if (jsnResult.has(JSON_KEY_ENERGY_POWER)) {
355             this.energyData = new TapoEnergyData(jsnResult);
356             this.device.setEnergyData(energyData);
357         } else {
358             this.energyData = new TapoEnergyData();
359         }
360         this.device.responsePasstrough(responseBody);
361     }
362
363     /**
364      * handle JsonResponse (getChildDeviceList)
365      *
366      * @param responseBody String with responseBody from device
367      */
368     @Override
369     protected void handleChildDevices(String responseBody) {
370         JsonObject jsnResult = getJsonFromResponse(responseBody);
371         if (jsnResult.has(JSON_KEY_CHILD_START_INDEX)) {
372             this.childData = Objects.requireNonNull(GSON.fromJson(jsnResult, TapoChildData.class));
373             this.device.setChildData(childData);
374         } else {
375             this.childData = new TapoChildData();
376         }
377         this.device.responsePasstrough(responseBody);
378     }
379
380     /**
381      * handle custom response
382      *
383      * @param responseBody String with responseBody from device
384      */
385     @Override
386     protected void handleCustomResponse(String responseBody) {
387         this.device.responsePasstrough(responseBody);
388     }
389
390     /**
391      * handle error
392      *
393      * @param te TapoErrorHandler
394      */
395     @Override
396     protected void handleError(TapoErrorHandler tapoError) {
397         this.device.setError(tapoError);
398     }
399
400     /**
401      * get Json from response
402      *
403      * @param responseBody
404      * @return JsonObject with result
405      */
406     private JsonObject getJsonFromResponse(String responseBody) {
407         JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
408         /* get errocode (0=success) */
409         if (jsonObject != null) {
410             Integer errorCode = jsonObjectToInt(jsonObject, "error_code");
411             if (errorCode == 0) {
412                 /* decrypt response */
413                 jsonObject = GSON.fromJson(responseBody, JsonObject.class);
414                 logger.trace("({}) received result: {}", uid, responseBody);
415                 if (jsonObject != null) {
416                     /* return result if set / else request was successful */
417                     if (jsonObject.has("result")) {
418                         return jsonObject.getAsJsonObject("result");
419                     } else {
420                         return jsonObject;
421                     }
422                 }
423             } else {
424                 /* return errorcode from device */
425                 TapoErrorHandler te = new TapoErrorHandler(errorCode, "device answers with errorcode");
426                 logger.debug("({}) device answers with errorcode {} - {}", uid, errorCode, te.getMessage());
427                 handleError(te);
428                 return jsonObject;
429             }
430         }
431         logger.debug("({}) sendPayload exception {}", uid, responseBody);
432         handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE));
433         return new JsonObject();
434     }
435
436     /***********************************
437      *
438      * GET RESULTS
439      *
440      ************************************/
441
442     /**
443      * Check if device is online
444      *
445      * @return true if device is online
446      */
447     public Boolean isOnline() {
448         return isOnline(false);
449     }
450
451     /**
452      * Check if device is online
453      *
454      * @param raiseError if true
455      * @return true if device is online
456      */
457     public Boolean isOnline(Boolean raiseError) {
458         if (pingDevice()) {
459             return true;
460         } else {
461             logger.trace("({})  device is offline (no ping)", uid);
462             if (raiseError) {
463                 handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE));
464             }
465             logout();
466             return false;
467         }
468     }
469
470     /**
471      * IP-Adress
472      *
473      * @return String ipAdress
474      */
475     public String getIP() {
476         return this.ipAddress;
477     }
478
479     /**
480      * PING IP Adress
481      *
482      * @return true if ping successfull
483      */
484     public Boolean pingDevice() {
485         try {
486             InetAddress address = InetAddress.getByName(this.ipAddress);
487             return address.isReachable(TAPO_PING_TIMEOUT_MS);
488         } catch (Exception e) {
489             logger.debug("({}) InetAdress throws: {}", uid, e.getMessage());
490             return false;
491         }
492     }
493
494     private Optional<TapoChild> getChild(int position) {
495         return childData.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();
496     }
497 }