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.*;
20 import java.net.InetAddress;
21 import java.util.HashMap;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.openhab.binding.tapocontrol.internal.device.TapoBridgeHandler;
25 import org.openhab.binding.tapocontrol.internal.device.TapoDevice;
26 import org.openhab.binding.tapocontrol.internal.helpers.PayloadBuilder;
27 import org.openhab.binding.tapocontrol.internal.helpers.TapoErrorHandler;
28 import org.openhab.binding.tapocontrol.internal.structures.TapoDeviceInfo;
29 import org.openhab.binding.tapocontrol.internal.structures.TapoEnergyData;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
33 import com.google.gson.Gson;
34 import com.google.gson.JsonObject;
37 * Handler class for TAPO Smart Home device connections.
38 * This class uses asynchronous HttpClient-Requests
40 * @author Christian Wild - Initial contribution
43 public class TapoDeviceConnector extends TapoDeviceHttpApi {
44 private final Logger logger = LoggerFactory.getLogger(TapoDeviceConnector.class);
45 private final String uid;
46 private final TapoDevice device;
47 private TapoDeviceInfo deviceInfo;
48 private TapoEnergyData energyData;
50 private long lastQuery = 0L;
51 private long lastSent = 0L;
52 private long lastLogin = 0L;
57 * @param config TapoControlConfiguration class
59 public TapoDeviceConnector(TapoDevice device, TapoBridgeHandler bridgeThingHandler) {
60 super(device, bridgeThingHandler);
62 this.gson = new Gson();
63 this.deviceInfo = new TapoDeviceInfo();
64 this.energyData = new TapoEnergyData();
65 this.uid = device.getThingUID().getAsString();
68 /***********************************
72 ************************************/
76 * @return true if success
78 public boolean login() {
79 if (this.pingDevice()) {
80 logger.trace("({}) sending login to url '{}'", uid, deviceURL);
82 long now = System.currentTimeMillis();
83 if (now > this.lastLogin + TAPO_LOGIN_MIN_GAP_MS) {
88 /* create ssl-handschake (cookie) */
89 String cookie = createHandshake();
90 if (!cookie.isBlank()) {
92 String token = queryToken();
96 logger.trace("({}) not done cause of min_gap '{}'", uid, TAPO_LOGIN_MIN_GAP_MS);
98 return this.loggedIn();
100 logger.debug("({}) no ping while login '{}'", uid, this.ipAddress);
101 handleError(new TapoErrorHandler(ERR_DEVICE_OFFLINE, "no ping while login"));
106 /***********************************
110 ************************************/
113 * send custom command to device
115 * @param plBuilder Payloadbuilder with unencrypted payload
117 public void sendCustomQuery(String queryMethod) {
119 PayloadBuilder plBuilder = new PayloadBuilder();
120 plBuilder.method = queryMethod;
121 sendCustomPayload(plBuilder);
125 * send custom command to device
127 * @param plBuilder Payloadbuilder with unencrypted payload
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);
135 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
140 * send "set_device_info" command to device
142 * @param name Name of command to send
143 * @param value Value to send to control
145 public void sendDeviceCommand(String name, Object value) {
146 long now = System.currentTimeMillis();
147 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
151 PayloadBuilder plBuilder = new PayloadBuilder();
152 plBuilder.method = DEVICE_CMD_SETINFO;
153 plBuilder.addParameter(name, value);
154 String payload = plBuilder.getPayload();
156 sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
158 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
163 * send multiple "set_device_info" commands to device
165 * @param map HashMap<String, Object> (name, value of parameter)
167 public void sendDeviceCommands(HashMap<String, Object> map) {
168 long now = System.currentTimeMillis();
169 if (now > this.lastSent + TAPO_SEND_MIN_GAP_MS) {
173 PayloadBuilder plBuilder = new PayloadBuilder();
174 plBuilder.method = DEVICE_CMD_SETINFO;
175 for (HashMap.Entry<String, Object> entry : map.entrySet()) {
176 plBuilder.addParameter(entry.getKey(), entry.getValue());
178 String payload = plBuilder.getPayload();
180 sendSecurePasstrhroug(payload, DEVICE_CMD_SETINFO);
182 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastSent);
187 * Query Info from Device adn refresh deviceInfo
189 public void queryInfo() {
194 * Query Info from Device adn refresh deviceInfo
196 * @param ignoreGap ignore gap to last query. query anyway
198 public void queryInfo(boolean ignoreGap) {
199 logger.trace("({}) DeviceConnetor_queryInfo from '{}'", uid, deviceURL);
200 long now = System.currentTimeMillis();
201 if (ignoreGap || now > this.lastQuery + TAPO_SEND_MIN_GAP_MS) {
202 this.lastQuery = now;
205 PayloadBuilder plBuilder = new PayloadBuilder();
206 plBuilder.method = DEVICE_CMD_GETINFO;
207 String payload = plBuilder.getPayload();
209 sendSecurePasstrhroug(payload, DEVICE_CMD_GETINFO);
211 logger.debug("({}) command not sent becauso of min_gap: {}", uid, now + " <- " + lastQuery);
216 * Get energy usage from device
218 public void getEnergyUsage() {
219 logger.trace("({}) DeviceConnetor_getEnergyUsage from '{}'", uid, deviceURL);
222 PayloadBuilder plBuilder = new PayloadBuilder();
223 plBuilder.method = DEVICE_CMD_GETENERGY;
224 String payload = plBuilder.getPayload();
226 sendSecurePasstrhroug(payload, DEVICE_CMD_GETENERGY);
230 * SEND SECUREPASSTHROUGH
231 * encprypt payload and send to device
233 * @param payload payload sent to device
234 * @param command command executed - this will handle result
236 protected void sendSecurePasstrhroug(String payload, String command) {
237 /* encrypt payload */
238 logger.trace("({}) encrypting payload '{}'", uid, payload);
239 String encryptedPayload = encryptPayload(payload);
241 /* create secured payload */
242 PayloadBuilder plBuilder = new PayloadBuilder();
243 plBuilder.method = "securePassthrough";
244 plBuilder.addParameter("request", encryptedPayload);
245 String securePassthroughPayload = plBuilder.getPayload();
247 sendAsyncRequest(deviceURL, securePassthroughPayload, command);
250 /***********************************
254 ************************************/
257 * Handle SuccessResponse (setDeviceInfo)
259 * @param responseBody String with responseBody from device
262 protected void handleSuccessResponse(String responseBody) {
263 JsonObject jsnResult = getJsonFromResponse(responseBody);
264 Integer errorCode = jsonObjectToInt(jsnResult, "error_code", ERR_JSON_DECODE_FAIL);
265 if (errorCode != 0) {
266 logger.debug("({}) set deviceInfo not successful: {}", uid, jsnResult);
267 this.device.handleConnectionState();
269 this.device.responsePasstrough(responseBody);
274 * handle JsonResponse (getDeviceInfo)
276 * @param responseBody String with responseBody from device
279 protected void handleDeviceResult(String responseBody) {
280 JsonObject jsnResult = getJsonFromResponse(responseBody);
281 if (jsnResult.has(DEVICE_PROPERTY_ID)) {
282 this.deviceInfo = new TapoDeviceInfo(jsnResult);
283 this.device.setDeviceInfo(deviceInfo);
285 this.deviceInfo = new TapoDeviceInfo();
286 this.device.handleConnectionState();
288 this.device.responsePasstrough(responseBody);
292 * handle JsonResponse (getEnergyData)
294 * @param responseBody String with responseBody from device
297 protected void handleEnergyResult(String responseBody) {
298 JsonObject jsnResult = getJsonFromResponse(responseBody);
299 if (jsnResult.has(ENERGY_PROPERTY_POWER)) {
300 this.energyData = new TapoEnergyData(jsnResult);
301 this.device.setEnergyData(energyData);
303 this.energyData = new TapoEnergyData();
305 this.device.responsePasstrough(responseBody);
309 * handle custom response
311 * @param responseBody String with responseBody from device
314 protected void handleCustomResponse(String responseBody) {
315 this.device.responsePasstrough(responseBody);
321 * @param te TapoErrorHandler
324 protected void handleError(TapoErrorHandler tapoError) {
325 this.device.setError(tapoError);
329 * get Json from response
331 * @param responseBody
332 * @return JsonObject with result
334 private JsonObject getJsonFromResponse(String responseBody) {
335 JsonObject jsonObject = gson.fromJson(responseBody, JsonObject.class);
336 /* get errocode (0=success) */
337 if (jsonObject != null) {
338 Integer errorCode = jsonObjectToInt(jsonObject, "error_code");
339 if (errorCode == 0) {
340 /* decrypt response */
341 jsonObject = gson.fromJson(responseBody, JsonObject.class);
342 logger.trace("({}) received result: {}", uid, responseBody);
343 if (jsonObject != null) {
344 /* return result if set / else request was successful */
345 if (jsonObject.has("result")) {
346 return jsonObject.getAsJsonObject("result");
352 /* return errorcode from device */
353 TapoErrorHandler te = new TapoErrorHandler(errorCode, "device answers with errorcode");
354 logger.debug("({}) device answers with errorcode {} - {}", uid, errorCode, te.getMessage());
359 logger.debug("({}) sendPayload exception {}", uid, responseBody);
360 handleError(new TapoErrorHandler(ERR_HTTP_RESPONSE));
361 return new JsonObject();
364 /***********************************
368 ************************************/
371 * Check if device is online
373 * @return true if device is online
375 public Boolean isOnline() {
376 return isOnline(false);
380 * Check if device is online
382 * @param raiseError if true
383 * @return true if device is online
385 public Boolean isOnline(Boolean raiseError) {
389 logger.trace("({}) device is offline (no ping)", uid);
391 handleError(new TapoErrorHandler(ERR_DEVICE_OFFLINE));
401 * @return String ipAdress
403 public String getIP() {
404 return this.ipAddress;
410 * @return true if ping successfull
412 public Boolean pingDevice() {
414 InetAddress address = InetAddress.getByName(this.ipAddress);
415 return address.isReachable(TAPO_PING_TIMEOUT_MS);
416 } catch (Exception e) {
417 logger.debug("({}) InetAdress throws: {}", uid, e.getMessage());