]> git.basschouten.com Git - openhab-addons.git/blob
c8f88d02784c184bf8e3c46b76f87c2b2691f4b8
[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;
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 device
62      * @param bridgeThingHandler
63      */
64     public TapoDeviceConnector(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
65         super(device, bridgeThingHandler);
66     }
67
68     /***********************************
69      *
70      * LOGIN FUNCTIONS
71      *
72      ************************************/
73     /**
74      * login
75      *
76      * @return true if success
77      */
78     public boolean login() {
79         if (this.pingDevice()) {
80             logger.trace("({}) sending login to url '{}'", uid, deviceURL);
81
82             long now = System.currentTimeMillis();
83             if (now > this.lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
84                 this.lastLogin = now;
85                 unsetToken();
86                 unsetCookie();
87
88                 /* create ssl-handschake (cookie) */
89                 String cookie = createHandshake();
90                 if (!cookie.isBlank()) {
91                     setCookie(cookie);
92                     String token = queryToken();
93                     setToken(token);
94                 }
95             } else {
96                 logger.trace("({}) not done cause of min_gap '{}'", uid, TAPO_LOGIN_MIN_GAP_MS);
97             }
98             return this.loggedIn();
99         } else {
100             logger.debug("({}) no ping while login '{}'", uid, this.ipAddress);
101             handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE, "no ping while login"));
102             return false;
103         }
104     }
105
106     /***********************************
107      *
108      * DEVICE ACTIONS
109      *
110      ************************************/
111
112     /**
113      * send custom command to device
114      *
115      * @param queryMethod query method
116      */
117     public void sendCustomQuery(String queryMethod) {
118         /* create payload */
119         PayloadBuilder plBuilder = new PayloadBuilder();
120         plBuilder.method = queryMethod;
121         sendCustomPayload(plBuilder);
122     }
123
124     /**
125      * send custom command to device
126      *
127      * @param plBuilder Payloadbuilder with unencrypted payload
128      */
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);
134         } else {
135             logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
136         }
137     }
138
139     /**
140      * send "set_device_info" command to device
141      *
142      * @param name Name of command to send
143      * @param value Value to send to control
144      */
145     public void sendDeviceCommand(String name, Object value) {
146         sendDeviceCommand(DEVICE_CMD_SETINFO, name, value);
147     }
148
149     /**
150      * send "set_device_info" command to device
151      *
152      * @param method Method command belongs to
153      * @param name Name of command to send
154      * @param value Value to send to control
155      */
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) {
159             this.lastSent = now;
160
161             /* create payload */
162             PayloadBuilder plBuilder = new PayloadBuilder();
163             plBuilder.method = method;
164             plBuilder.addParameter(name, value);
165             String payload = plBuilder.getPayload();
166
167             sendSecurePasstrhroug(payload, method);
168         } else {
169             logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
170         }
171     }
172
173     /**
174      * send "set_device_info" command to child's device
175      *
176      * @param index of the child
177      * @param childProperty to modify
178      * @param value for the property
179      */
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) {
183             this.lastSent = now;
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());
188             });
189         } else {
190             logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastSent);
191         }
192     }
193
194     /**
195      * send multiple "set_device_info" commands to device
196      *
197      * @param map {@code HashMap<String, Object> (name, value of parameter)}
198      */
199     public void sendDeviceCommands(HashMap<String, Object> map) {
200         sendDeviceCommands(DEVICE_CMD_SETINFO, map);
201     }
202
203     /**
204      * send multiple commands to device
205      *
206      * @param method Method command belongs to
207      * @param map {@code HashMap<String, Object> (name, value of parameter)}
208      */
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) {
212             this.lastSent = now;
213
214             /* create payload */
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());
219             }
220             String payload = plBuilder.getPayload();
221
222             sendSecurePasstrhroug(payload, method);
223         } else {
224             logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
225         }
226     }
227
228     /**
229      * Query Info from Device and refresh deviceInfo
230      */
231     public void queryInfo() {
232         queryInfo(false);
233     }
234
235     /**
236      * Query Info from Device and refresh deviceInfo
237      *
238      *
239      * @param ignoreGap ignore gap to last query. query anyway
240      */
241     public void queryInfo(boolean ignoreGap) {
242         logger.trace("({}) DeviceConnector_queryInfo from '{}'", uid, deviceURL);
243         queryCommand(DEVICE_CMD_GETINFO, ignoreGap);
244     }
245
246     /**
247      * Query Info from Child Devices and refresh deviceInfo
248      */
249     @Override
250     public void queryChildDevices() {
251         logger.trace("({}) DeviceConnector_queryChildDevices from '{}'", uid, deviceURL);
252         queryCommand(DEVICE_CMD_CHILD_DEVICE_LIST, true);
253     }
254
255     /**
256      * Get energy usage from device
257      */
258     public void getEnergyUsage() {
259         queryCommand(DEVICE_CMD_GETENERGY, true);
260     }
261
262     /**
263      * Send Custom DeviceQuery
264      *
265      * @param queryCommand Command to be queried
266      * @param ignoreGap ignore gap to last query. query anyway
267      */
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;
273
274             /* create payload */
275             PayloadBuilder plBuilder = new PayloadBuilder();
276             plBuilder.method = queryCommand;
277             String payload = plBuilder.getPayload();
278
279             sendSecurePasstrhroug(payload, queryCommand);
280         } else {
281             logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastQuery);
282         }
283     }
284
285     /**
286      * SEND SECUREPASSTHROUGH
287      * encprypt payload and send to device
288      *
289      * @param payload payload sent to device
290      * @param command command executed - this will handle result
291      */
292     protected void sendSecurePasstrhroug(String payload, String command) {
293         /* encrypt payload */
294         logger.trace("({}) encrypting payload '{}'", uid, payload);
295         String encryptedPayload = encryptPayload(payload);
296
297         /* create secured payload */
298         PayloadBuilder plBuilder = new PayloadBuilder();
299         plBuilder.method = "securePassthrough";
300         plBuilder.addParameter("request", encryptedPayload);
301         String securePassthroughPayload = plBuilder.getPayload();
302
303         sendAsyncRequest(deviceURL, securePassthroughPayload, command);
304     }
305
306     /***********************************
307      *
308      * HANDLE RESPONSES
309      *
310      ************************************/
311
312     /**
313      * Handle SuccessResponse (setDeviceInfo)
314      *
315      * @param responseBody String with responseBody from device
316      */
317     @Override
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();
324         }
325         this.device.responsePasstrough(responseBody);
326     }
327
328     /**
329      *
330      * handle JsonResponse (getDeviceInfo)
331      *
332      * @param responseBody String with responseBody from device
333      */
334     @Override
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);
340         } else {
341             this.deviceInfo = new TapoDeviceInfo();
342             this.device.handleConnectionState();
343         }
344         this.device.responsePasstrough(responseBody);
345     }
346
347     /**
348      * handle JsonResponse (getEnergyData)
349      *
350      * @param responseBody String with responseBody from device
351      */
352     @Override
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);
358         } else {
359             this.energyData = new TapoEnergyData();
360         }
361         this.device.responsePasstrough(responseBody);
362     }
363
364     /**
365      * handle JsonResponse (getChildDeviceList)
366      *
367      * @param responseBody String with responseBody from device
368      */
369     @Override
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);
375         } else {
376             this.childData = new TapoChildData();
377         }
378         this.device.responsePasstrough(responseBody);
379     }
380
381     /**
382      * handle custom response
383      *
384      * @param responseBody String with responseBody from device
385      */
386     @Override
387     protected void handleCustomResponse(String responseBody) {
388         this.device.responsePasstrough(responseBody);
389     }
390
391     /**
392      * handle error
393      *
394      * @param tapoError TapoErrorHandler
395      */
396     @Override
397     protected void handleError(TapoErrorHandler tapoError) {
398         this.device.setError(tapoError);
399     }
400
401     /**
402      * get Json from response
403      *
404      * @param responseBody
405      * @return JsonObject with result
406      */
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");
420                     } else {
421                         return jsonObject;
422                     }
423                 }
424             } else {
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());
428                 handleError(te);
429                 return jsonObject;
430             }
431         }
432         logger.debug("({}) sendPayload exception {}", uid, responseBody);
433         handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE));
434         return new JsonObject();
435     }
436
437     /***********************************
438      *
439      * GET RESULTS
440      *
441      ************************************/
442
443     /**
444      * Check if device is online
445      *
446      * @return true if device is online
447      */
448     public Boolean isOnline() {
449         return isOnline(false);
450     }
451
452     /**
453      * Check if device is online
454      *
455      * @param raiseError if true
456      * @return true if device is online
457      */
458     public Boolean isOnline(Boolean raiseError) {
459         if (pingDevice()) {
460             return true;
461         } else {
462             logger.trace("({})  device is offline (no ping)", uid);
463             if (raiseError) {
464                 handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE));
465             }
466             logout();
467             return false;
468         }
469     }
470
471     /**
472      * IP-Adress
473      *
474      * @return String ipAdress
475      */
476     public String getIP() {
477         return this.ipAddress;
478     }
479
480     /**
481      * PING IP Adress
482      *
483      * @return true if ping successfull
484      */
485     public Boolean pingDevice() {
486         try {
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());
491             return false;
492         }
493     }
494
495     private Optional<TapoChild> getChild(int position) {
496         return childData.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();
497     }
498 }