]> git.basschouten.com Git - openhab-addons.git/blob
c1c992338bdb6a69b585e5fbdca8603ac37e2e18
[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.TapoErrorConstants.*;
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_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      * @param ignoreGap ignore gap to last query. query anyway
218      */
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;
224
225             /* create payload */
226             PayloadBuilder plBuilder = new PayloadBuilder();
227             plBuilder.method = DEVICE_CMD_GETINFO;
228             String payload = plBuilder.getPayload();
229
230             sendSecurePasstrhroug(payload, DEVICE_CMD_GETINFO);
231         } else {
232             logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastQuery);
233         }
234     }
235
236     /**
237      * Query Info from Child Devices and refresh deviceInfo
238      */
239     @Override
240     public void queryChildDevices() {
241         logger.trace("({}) DeviceConnetor_queryChildDevices from '{}'", uid, deviceURL);
242
243         /* create payload */
244         PayloadBuilder plBuilder = new PayloadBuilder();
245         plBuilder.method = DEVICE_CMD_CHILD_DEVICE_LIST;
246         String payload = plBuilder.getPayload();
247
248         sendSecurePasstrhroug(payload, DEVICE_CMD_CHILD_DEVICE_LIST);
249     }
250
251     /**
252      * Get energy usage from device
253      */
254     public void getEnergyUsage() {
255         logger.trace("({}) DeviceConnetor_getEnergyUsage from '{}'", uid, deviceURL);
256
257         /* create payload */
258         PayloadBuilder plBuilder = new PayloadBuilder();
259         plBuilder.method = DEVICE_CMD_GETENERGY;
260         String payload = plBuilder.getPayload();
261
262         sendSecurePasstrhroug(payload, DEVICE_CMD_GETENERGY);
263     }
264
265     /**
266      * SEND SECUREPASSTHROUGH
267      * encprypt payload and send to device
268      *
269      * @param payload payload sent to device
270      * @param command command executed - this will handle result
271      */
272     protected void sendSecurePasstrhroug(String payload, String command) {
273         /* encrypt payload */
274         logger.trace("({}) encrypting payload '{}'", uid, payload);
275         String encryptedPayload = encryptPayload(payload);
276
277         /* create secured payload */
278         PayloadBuilder plBuilder = new PayloadBuilder();
279         plBuilder.method = "securePassthrough";
280         plBuilder.addParameter("request", encryptedPayload);
281         String securePassthroughPayload = plBuilder.getPayload();
282
283         sendAsyncRequest(deviceURL, securePassthroughPayload, command);
284     }
285
286     /***********************************
287      *
288      * HANDLE RESPONSES
289      *
290      ************************************/
291
292     /**
293      * Handle SuccessResponse (setDeviceInfo)
294      *
295      * @param responseBody String with responseBody from device
296      */
297     @Override
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();
304         }
305         this.device.responsePasstrough(responseBody);
306     }
307
308     /**
309      *
310      * handle JsonResponse (getDeviceInfo)
311      *
312      * @param responseBody String with responseBody from device
313      */
314     @Override
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);
320         } else {
321             this.deviceInfo = new TapoDeviceInfo();
322             this.device.handleConnectionState();
323         }
324         this.device.responsePasstrough(responseBody);
325     }
326
327     /**
328      * handle JsonResponse (getEnergyData)
329      *
330      * @param responseBody String with responseBody from device
331      */
332     @Override
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);
338         } else {
339             this.energyData = new TapoEnergyData();
340         }
341         this.device.responsePasstrough(responseBody);
342     }
343
344     /**
345      * handle JsonResponse (getChildDeviceList)
346      *
347      * @param responseBody String with responseBody from device
348      */
349     @Override
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);
355         } else {
356             this.childData = new TapoChildData();
357         }
358         this.device.responsePasstrough(responseBody);
359     }
360
361     /**
362      * handle custom response
363      *
364      * @param responseBody String with responseBody from device
365      */
366     @Override
367     protected void handleCustomResponse(String responseBody) {
368         this.device.responsePasstrough(responseBody);
369     }
370
371     /**
372      * handle error
373      *
374      * @param te TapoErrorHandler
375      */
376     @Override
377     protected void handleError(TapoErrorHandler tapoError) {
378         this.device.setError(tapoError);
379     }
380
381     /**
382      * get Json from response
383      *
384      * @param responseBody
385      * @return JsonObject with result
386      */
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");
400                     } else {
401                         return jsonObject;
402                     }
403                 }
404             } else {
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());
408                 handleError(te);
409                 return jsonObject;
410             }
411         }
412         logger.debug("({}) sendPayload exception {}", uid, responseBody);
413         handleError(new TapoErrorHandler(ERR_HTTP_RESPONSE));
414         return new JsonObject();
415     }
416
417     /***********************************
418      *
419      * GET RESULTS
420      *
421      ************************************/
422
423     /**
424      * Check if device is online
425      *
426      * @return true if device is online
427      */
428     public Boolean isOnline() {
429         return isOnline(false);
430     }
431
432     /**
433      * Check if device is online
434      *
435      * @param raiseError if true
436      * @return true if device is online
437      */
438     public Boolean isOnline(Boolean raiseError) {
439         if (pingDevice()) {
440             return true;
441         } else {
442             logger.trace("({})  device is offline (no ping)", uid);
443             if (raiseError) {
444                 handleError(new TapoErrorHandler(ERR_DEVICE_OFFLINE));
445             }
446             logout();
447             return false;
448         }
449     }
450
451     /**
452      * IP-Adress
453      *
454      * @return String ipAdress
455      */
456     public String getIP() {
457         return this.ipAddress;
458     }
459
460     /**
461      * PING IP Adress
462      *
463      * @return true if ping successfull
464      */
465     public Boolean pingDevice() {
466         try {
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());
471             return false;
472         }
473     }
474
475     private Optional<TapoChild> getChild(int position) {
476         return childData.getChildDeviceList().stream().filter(child -> child.getPosition() == position).findFirst();
477     }
478 }