]> git.basschouten.com Git - openhab-addons.git/blob
0913654bc7dd559fc274ed1c835f4dfdaa8c3d55
[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         long now = System.currentTimeMillis();
146         if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
147             this.lastSent = now;
148
149             /* create payload */
150             PayloadBuilder plBuilder = new PayloadBuilder();
151             plBuilder.method = DEVICE_CMD_SETINFO;
152             plBuilder.addParameter(name, value);
153             String payload = plBuilder.getPayload();
154
155             sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
156         } else {
157             logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
158         }
159     }
160
161     /**
162      * send "set_device_info" command to child's device
163      *
164      * @param index of the child
165      * @param childProperty to modify
166      * @param value for the property
167      */
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) {
171             this.lastSent = now;
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());
176             });
177         } else {
178             logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastSent);
179         }
180     }
181
182     /**
183      * send multiple "set_device_info" commands to device
184      *
185      * @param map HashMap<String, Object> (name, value of parameter)
186      */
187     public void sendDeviceCommands(HashMap<String, Object> map) {
188         long now = System.currentTimeMillis();
189         if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
190             this.lastSent = now;
191
192             /* create payload */
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());
197             }
198             String payload = plBuilder.getPayload();
199
200             sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
201         } else {
202             logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
203         }
204     }
205
206     /**
207      * Query Info from Device and refresh deviceInfo
208      */
209     public void queryInfo() {
210         queryInfo(false);
211         queryChildDevices();
212     }
213
214     /**
215      * Query Info from Device and refresh deviceInfo
216      *
217      *
218      * @param ignoreGap ignore gap to last query. query anyway
219      */
220     public void queryInfo(boolean ignoreGap) {
221         logger.trace("({}) DeviceConnector_queryInfo from '{}'", uid, deviceURL);
222         queryCommand(DEVICE_CMD_GETINFO, ignoreGap);
223     }
224
225     /**
226      * Query Info from Child Devices and refresh deviceInfo
227      */
228     @Override
229     public void queryChildDevices() {
230         logger.trace("({}) DeviceConnector_queryChildDevices from '{}'", uid, deviceURL);
231         queryCommand(DEVICE_CMD_CHILD_DEVICE_LIST, true);
232     }
233
234     /**
235      * Get energy usage from device
236      */
237     public void getEnergyUsage() {
238         queryCommand(DEVICE_CMD_GETENERGY, true);
239     }
240
241     /**
242      * Send Custom DeviceQuery
243      *
244      * @param queryCommand Command to be queried
245      * @param ignoreGap ignore gap to last query. query anyway
246      */
247     public void queryCommand(String queryCommand, boolean ignoreGap) {
248         logger.trace("({}) DeviceConnector_queryCommand '{}' from '{}'", uid, queryCommand, deviceURL);
249         long now = System.currentTimeMillis();
250         if (ignoreGap || now > this.lastQuery + TAPO_SEND_MIN_GAP_MS) {
251             this.lastQuery = now;
252
253             /* create payload */
254             PayloadBuilder plBuilder = new PayloadBuilder();
255             plBuilder.method = queryCommand;
256             String payload = plBuilder.getPayload();
257
258             sendSecurePasstrhroug(payload, queryCommand);
259         } else {
260             logger.debug("({}) command not sent because of min_gap: {}", uid, now + " <- " + lastQuery);
261         }
262     }
263
264     /**
265      * SEND SECUREPASSTHROUGH
266      * encprypt payload and send to device
267      *
268      * @param payload payload sent to device
269      * @param command command executed - this will handle result
270      */
271     protected void sendSecurePasstrhroug(String payload, String command) {
272         /* encrypt payload */
273         logger.trace("({}) encrypting payload '{}'", uid, payload);
274         String encryptedPayload = encryptPayload(payload);
275
276         /* create secured payload */
277         PayloadBuilder plBuilder = new PayloadBuilder();
278         plBuilder.method = "securePassthrough";
279         plBuilder.addParameter("request", encryptedPayload);
280         String securePassthroughPayload = plBuilder.getPayload();
281
282         sendAsyncRequest(deviceURL, securePassthroughPayload, command);
283     }
284
285     /***********************************
286      *
287      * HANDLE RESPONSES
288      *
289      ************************************/
290
291     /**
292      * Handle SuccessResponse (setDeviceInfo)
293      *
294      * @param responseBody String with responseBody from device
295      */
296     @Override
297     protected void handleSuccessResponse(String responseBody) {
298         JsonObject jsnResult = getJsonFromResponse(responseBody);
299         Integer errorCode = jsonObjectToInt(jsnResult, "error_code", ERR_API_JSON_DECODE_FAIL.getCode());
300         if (errorCode != 0) {
301             logger.debug("({}) set deviceInfo not successful: {}", uid, jsnResult);
302             this.device.handleConnectionState();
303         }
304         this.device.responsePasstrough(responseBody);
305     }
306
307     /**
308      *
309      * handle JsonResponse (getDeviceInfo)
310      *
311      * @param responseBody String with responseBody from device
312      */
313     @Override
314     protected void handleDeviceResult(String responseBody) {
315         JsonObject jsnResult = getJsonFromResponse(responseBody);
316         if (jsnResult.has(JSON_KEY_ID)) {
317             this.deviceInfo = new TapoDeviceInfo(jsnResult);
318             this.device.setDeviceInfo(deviceInfo);
319         } else {
320             this.deviceInfo = new TapoDeviceInfo();
321             this.device.handleConnectionState();
322         }
323         this.device.responsePasstrough(responseBody);
324     }
325
326     /**
327      * handle JsonResponse (getEnergyData)
328      *
329      * @param responseBody String with responseBody from device
330      */
331     @Override
332     protected void handleEnergyResult(String responseBody) {
333         JsonObject jsnResult = getJsonFromResponse(responseBody);
334         if (jsnResult.has(JSON_KEY_ENERGY_POWER)) {
335             this.energyData = new TapoEnergyData(jsnResult);
336             this.device.setEnergyData(energyData);
337         } else {
338             this.energyData = new TapoEnergyData();
339         }
340         this.device.responsePasstrough(responseBody);
341     }
342
343     /**
344      * handle JsonResponse (getChildDeviceList)
345      *
346      * @param responseBody String with responseBody from device
347      */
348     @Override
349     protected void handleChildDevices(String responseBody) {
350         JsonObject jsnResult = getJsonFromResponse(responseBody);
351         if (jsnResult.has(JSON_KEY_CHILD_START_INDEX)) {
352             this.childData = Objects.requireNonNull(GSON.fromJson(jsnResult, TapoChildData.class));
353             this.device.setChildData(childData);
354         } else {
355             this.childData = new TapoChildData();
356         }
357         this.device.responsePasstrough(responseBody);
358     }
359
360     /**
361      * handle custom response
362      *
363      * @param responseBody String with responseBody from device
364      */
365     @Override
366     protected void handleCustomResponse(String responseBody) {
367         this.device.responsePasstrough(responseBody);
368     }
369
370     /**
371      * handle error
372      *
373      * @param te TapoErrorHandler
374      */
375     @Override
376     protected void handleError(TapoErrorHandler tapoError) {
377         this.device.setError(tapoError);
378     }
379
380     /**
381      * get Json from response
382      *
383      * @param responseBody
384      * @return JsonObject with result
385      */
386     private JsonObject getJsonFromResponse(String responseBody) {
387         JsonObject jsonObject = GSON.fromJson(responseBody, JsonObject.class);
388         /* get errocode (0=success) */
389         if (jsonObject != null) {
390             Integer errorCode = jsonObjectToInt(jsonObject, "error_code");
391             if (errorCode == 0) {
392                 /* decrypt response */
393                 jsonObject = GSON.fromJson(responseBody, JsonObject.class);
394                 logger.trace("({}) received result: {}", uid, responseBody);
395                 if (jsonObject != null) {
396                     /* return result if set / else request was successful */
397                     if (jsonObject.has("result")) {
398                         return jsonObject.getAsJsonObject("result");
399                     } else {
400                         return jsonObject;
401                     }
402                 }
403             } else {
404                 /* return errorcode from device */
405                 TapoErrorHandler te = new TapoErrorHandler(errorCode, "device answers with errorcode");
406                 logger.debug("({}) device answers with errorcode {} - {}", uid, errorCode, te.getMessage());
407                 handleError(te);
408                 return jsonObject;
409             }
410         }
411         logger.debug("({}) sendPayload exception {}", uid, responseBody);
412         handleError(new TapoErrorHandler(ERR_BINDING_HTTP_RESPONSE));
413         return new JsonObject();
414     }
415
416     /***********************************
417      *
418      * GET RESULTS
419      *
420      ************************************/
421
422     /**
423      * Check if device is online
424      *
425      * @return true if device is online
426      */
427     public Boolean isOnline() {
428         return isOnline(false);
429     }
430
431     /**
432      * Check if device is online
433      *
434      * @param raiseError if true
435      * @return true if device is online
436      */
437     public Boolean isOnline(Boolean raiseError) {
438         if (pingDevice()) {
439             return true;
440         } else {
441             logger.trace("({})  device is offline (no ping)", uid);
442             if (raiseError) {
443                 handleError(new TapoErrorHandler(ERR_BINDING_DEVICE_OFFLINE));
444             }
445             logout();
446             return false;
447         }
448     }
449
450     /**
451      * IP-Adress
452      *
453      * @return String ipAdress
454      */
455     public String getIP() {
456         return this.ipAddress;
457     }
458
459     /**
460      * PING IP Adress
461      *
462      * @return true if ping successfull
463      */
464     public Boolean pingDevice() {
465         try {
466             InetAddress address = InetAddress.getByName(this.ipAddress);
467             return address.isReachable(TAPO_PING_TIMEOUT_MS);
468         } catch (Exception e) {
469             logger.debug("({}) InetAdress throws: {}", uid, e.getMessage());
470             return false;
471         }
472     }
473
474     private Optional<TapoChild> getChild(int position) {
475         return childData.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();
476     }
477 }