]> git.basschouten.com Git - openhab-addons.git/blob
60239fa247b6900d86bf82b9ab2fb50d1192af9c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.tesla.internal.handler;
14
15 import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;
16
17 import java.io.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.InputStreamReader;
21 import java.math.BigDecimal;
22 import java.math.RoundingMode;
23 import java.text.SimpleDateFormat;
24 import java.util.Date;
25 import java.util.HashMap;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.locks.ReentrantLock;
31
32 import javax.measure.quantity.Temperature;
33 import javax.ws.rs.ProcessingException;
34 import javax.ws.rs.client.Client;
35 import javax.ws.rs.client.ClientBuilder;
36 import javax.ws.rs.client.WebTarget;
37 import javax.ws.rs.core.MediaType;
38 import javax.ws.rs.core.Response;
39
40 import org.apache.commons.lang.StringUtils;
41 import org.eclipse.jdt.annotation.NonNull;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.binding.tesla.internal.TeslaBindingConstants;
44 import org.openhab.binding.tesla.internal.TeslaBindingConstants.EventKeys;
45 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy;
46 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy.TeslaChannelSelector;
47 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Authenticator;
48 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Request;
49 import org.openhab.binding.tesla.internal.protocol.ChargeState;
50 import org.openhab.binding.tesla.internal.protocol.ClimateState;
51 import org.openhab.binding.tesla.internal.protocol.DriveState;
52 import org.openhab.binding.tesla.internal.protocol.GUIState;
53 import org.openhab.binding.tesla.internal.protocol.Vehicle;
54 import org.openhab.binding.tesla.internal.protocol.VehicleState;
55 import org.openhab.binding.tesla.internal.throttler.QueueChannelThrottler;
56 import org.openhab.binding.tesla.internal.throttler.Rate;
57 import org.openhab.core.library.types.IncreaseDecreaseType;
58 import org.openhab.core.library.types.OnOffType;
59 import org.openhab.core.library.types.PercentType;
60 import org.openhab.core.library.types.QuantityType;
61 import org.openhab.core.library.types.StringType;
62 import org.openhab.core.library.unit.SIUnits;
63 import org.openhab.core.thing.ChannelUID;
64 import org.openhab.core.thing.Thing;
65 import org.openhab.core.thing.ThingStatus;
66 import org.openhab.core.thing.ThingStatusDetail;
67 import org.openhab.core.thing.binding.BaseThingHandler;
68 import org.openhab.core.types.Command;
69 import org.openhab.core.types.RefreshType;
70 import org.openhab.core.types.State;
71 import org.openhab.core.types.UnDefType;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74
75 import com.google.gson.Gson;
76 import com.google.gson.JsonElement;
77 import com.google.gson.JsonObject;
78 import com.google.gson.JsonParser;
79
80 /**
81  * The {@link TeslaVehicleHandler} is responsible for handling commands, which are sent
82  * to one of the channels of a specific vehicle.
83  *
84  * @author Karel Goderis - Initial contribution
85  * @author Kai Kreuzer - Refactored to use separate account handler and improved configuration options
86  */
87 public class TeslaVehicleHandler extends BaseThingHandler {
88
89     private static final int EVENT_STREAM_PAUSE = 5000;
90     private static final int EVENT_TIMESTAMP_AGE_LIMIT = 3000;
91     private static final int EVENT_TIMESTAMP_MAX_DELTA = 10000;
92     private static final int FAST_STATUS_REFRESH_INTERVAL = 15000;
93     private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
94     private static final int EVENT_MAXIMUM_ERRORS_IN_INTERVAL = 10;
95     private static final int EVENT_ERROR_INTERVAL_SECONDS = 15;
96     private static final int API_SLEEP_INTERVAL_MINUTES = 20;
97     private static final int MOVE_THRESHOLD_INTERVAL_MINUTES = 5;
98
99     private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
100
101     protected WebTarget eventTarget;
102
103     // Vehicle state variables
104     protected Vehicle vehicle;
105     protected String vehicleJSON;
106     protected DriveState driveState;
107     protected GUIState guiState;
108     protected VehicleState vehicleState;
109     protected ChargeState chargeState;
110     protected ClimateState climateState;
111
112     protected boolean allowWakeUp;
113     protected boolean enableEvents = false;
114     protected long lastTimeStamp;
115     protected long apiIntervalTimestamp;
116     protected int apiIntervalErrors;
117     protected long eventIntervalTimestamp;
118     protected int eventIntervalErrors;
119     protected ReentrantLock lock;
120
121     protected double lastLongitude;
122     protected double lastLatitude;
123     protected long lastLocationChangeTimestamp;
124
125     protected long lastStateTimestamp = System.currentTimeMillis();
126     protected String lastState = "";
127     protected boolean isInactive = false;
128
129     protected TeslaAccountHandler account;
130
131     protected QueueChannelThrottler stateThrottler;
132     protected ClientBuilder clientBuilder;
133     protected Client eventClient;
134     protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
135     protected Thread eventThread;
136     protected ScheduledFuture<?> fastStateJob;
137     protected ScheduledFuture<?> slowStateJob;
138
139     private final Gson gson = new Gson();
140     private final JsonParser parser = new JsonParser();
141
142     public TeslaVehicleHandler(Thing thing, ClientBuilder clientBuilder) {
143         super(thing);
144         this.clientBuilder = clientBuilder;
145     }
146
147     @SuppressWarnings("null")
148     @Override
149     public void initialize() {
150         logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
151         updateStatus(ThingStatus.UNKNOWN);
152         allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
153
154         // the streaming API seems to be broken - let's keep the code, if it comes back one day
155         // enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
156
157         account = (TeslaAccountHandler) getBridge().getHandler();
158         lock = new ReentrantLock();
159         scheduler.execute(() -> queryVehicleAndUpdate());
160
161         lock.lock();
162         try {
163             Map<Object, Rate> channels = new HashMap<>();
164             channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
165             channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
166
167             Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
168             Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
169             stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
170             stateThrottler.addRate(secondRate);
171
172             if (fastStateJob == null || fastStateJob.isCancelled()) {
173                 fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
174                         TimeUnit.MILLISECONDS);
175             }
176
177             if (slowStateJob == null || slowStateJob.isCancelled()) {
178                 slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
179                         TimeUnit.MILLISECONDS);
180             }
181         } finally {
182             lock.unlock();
183         }
184
185         if (enableEvents) {
186             if (eventThread == null) {
187                 eventThread = new Thread(eventRunnable, "openHAB-Tesla-Events-" + getThing().getUID());
188                 eventThread.start();
189             }
190         }
191     }
192
193     @Override
194     public void dispose() {
195         logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
196         lock.lock();
197         try {
198             if (fastStateJob != null && !fastStateJob.isCancelled()) {
199                 fastStateJob.cancel(true);
200                 fastStateJob = null;
201             }
202
203             if (slowStateJob != null && !slowStateJob.isCancelled()) {
204                 slowStateJob.cancel(true);
205                 slowStateJob = null;
206             }
207
208             if (eventThread != null && !eventThread.isInterrupted()) {
209                 eventThread.interrupt();
210                 eventThread = null;
211             }
212         } finally {
213             lock.unlock();
214         }
215
216         if (eventClient != null) {
217             eventClient.close();
218         }
219     }
220
221     /**
222      * Retrieves the unique vehicle id this handler is associated with
223      *
224      * @return the vehicle id
225      */
226     public String getVehicleId() {
227         return vehicle.id;
228     }
229
230     @Override
231     public void handleCommand(ChannelUID channelUID, Command command) {
232         logger.debug("handleCommand {} {}", channelUID, command);
233         String channelID = channelUID.getId();
234         TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
235
236         if (command instanceof RefreshType) {
237             if (!isAwake()) {
238                 logger.debug("Waking vehicle to refresh all data");
239                 wakeUp();
240             }
241
242             setActive();
243
244             // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
245             // throttled so we are safe not to break the Tesla SLA
246             requestAllData();
247         } else {
248             if (selector != null) {
249                 try {
250                     switch (selector) {
251                         case CHARGE_LIMIT_SOC: {
252                             if (command instanceof PercentType) {
253                                 setChargeLimit(((PercentType) command).intValue());
254                             } else if (command instanceof OnOffType && command == OnOffType.ON) {
255                                 setChargeLimit(100);
256                             } else if (command instanceof OnOffType && command == OnOffType.OFF) {
257                                 setChargeLimit(0);
258                             } else if (command instanceof IncreaseDecreaseType
259                                     && command == IncreaseDecreaseType.INCREASE) {
260                                 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
261                             } else if (command instanceof IncreaseDecreaseType
262                                     && command == IncreaseDecreaseType.DECREASE) {
263                                 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
264                             }
265                             break;
266                         }
267                         case COMBINED_TEMP: {
268                             QuantityType<Temperature> quantity = commandToQuantityType(command);
269                             if (quantity != null) {
270                                 setCombinedTemperature(quanityToRoundedFloat(quantity));
271                             }
272                             break;
273                         }
274                         case DRIVER_TEMP: {
275                             QuantityType<Temperature> quantity = commandToQuantityType(command);
276                             if (quantity != null) {
277                                 setDriverTemperature(quanityToRoundedFloat(quantity));
278                             }
279                             break;
280                         }
281                         case PASSENGER_TEMP: {
282                             QuantityType<Temperature> quantity = commandToQuantityType(command);
283                             if (quantity != null) {
284                                 setPassengerTemperature(quanityToRoundedFloat(quantity));
285                             }
286                             break;
287                         }
288                         case SUN_ROOF_STATE: {
289                             if (command instanceof StringType) {
290                                 setSunroof(command.toString());
291                             }
292                             break;
293                         }
294                         case SUN_ROOF: {
295                             if (command instanceof PercentType) {
296                                 moveSunroof(((PercentType) command).intValue());
297                             } else if (command instanceof OnOffType && command == OnOffType.ON) {
298                                 moveSunroof(100);
299                             } else if (command instanceof OnOffType && command == OnOffType.OFF) {
300                                 moveSunroof(0);
301                             } else if (command instanceof IncreaseDecreaseType
302                                     && command == IncreaseDecreaseType.INCREASE) {
303                                 moveSunroof(Math.min(vehicleState.sun_roof_percent_open + 1, 100));
304                             } else if (command instanceof IncreaseDecreaseType
305                                     && command == IncreaseDecreaseType.DECREASE) {
306                                 moveSunroof(Math.max(vehicleState.sun_roof_percent_open - 1, 0));
307                             }
308                             break;
309                         }
310                         case CHARGE_TO_MAX: {
311                             if (command instanceof OnOffType) {
312                                 if (((OnOffType) command) == OnOffType.ON) {
313                                     setMaxRangeCharging(true);
314                                 } else {
315                                     setMaxRangeCharging(false);
316                                 }
317                             }
318                             break;
319                         }
320                         case CHARGE: {
321                             if (command instanceof OnOffType) {
322                                 if (((OnOffType) command) == OnOffType.ON) {
323                                     charge(true);
324                                 } else {
325                                     charge(false);
326                                 }
327                             }
328                             break;
329                         }
330                         case FLASH: {
331                             if (command instanceof OnOffType) {
332                                 if (((OnOffType) command) == OnOffType.ON) {
333                                     flashLights();
334                                 }
335                             }
336                             break;
337                         }
338                         case HONK_HORN: {
339                             if (command instanceof OnOffType) {
340                                 if (((OnOffType) command) == OnOffType.ON) {
341                                     honkHorn();
342                                 }
343                             }
344                             break;
345                         }
346                         case CHARGEPORT: {
347                             if (command instanceof OnOffType) {
348                                 if (((OnOffType) command) == OnOffType.ON) {
349                                     openChargePort();
350                                 }
351                             }
352                             break;
353                         }
354                         case DOOR_LOCK: {
355                             if (command instanceof OnOffType) {
356                                 if (((OnOffType) command) == OnOffType.ON) {
357                                     lockDoors(true);
358                                 } else {
359                                     lockDoors(false);
360                                 }
361                             }
362                             break;
363                         }
364                         case AUTO_COND: {
365                             if (command instanceof OnOffType) {
366                                 if (((OnOffType) command) == OnOffType.ON) {
367                                     autoConditioning(true);
368                                 } else {
369                                     autoConditioning(false);
370                                 }
371                             }
372                             break;
373                         }
374                         case WAKEUP: {
375                             if (command instanceof OnOffType) {
376                                 if (((OnOffType) command) == OnOffType.ON) {
377                                     wakeUp();
378                                 }
379                             }
380                             break;
381                         }
382                         case FT: {
383                             if (command instanceof OnOffType) {
384                                 if (((OnOffType) command) == OnOffType.ON) {
385                                     openFrunk();
386                                 }
387                             }
388                             break;
389                         }
390                         case RT: {
391                             if (command instanceof OnOffType) {
392                                 if (((OnOffType) command) == OnOffType.ON) {
393                                     if (vehicleState.rt == 0) {
394                                         openTrunk();
395                                     }
396                                 } else {
397                                     if (vehicleState.rt == 1) {
398                                         closeTrunk();
399                                     }
400                                 }
401                             }
402                             break;
403                         }
404                         case VALET_MODE: {
405                             if (command instanceof OnOffType) {
406                                 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
407                                 if (((OnOffType) command) == OnOffType.ON) {
408                                     setValetMode(true, valetpin);
409                                 } else {
410                                     setValetMode(false, valetpin);
411                                 }
412                             }
413                             break;
414                         }
415                         case RESET_VALET_PIN: {
416                             if (command instanceof OnOffType) {
417                                 if (((OnOffType) command) == OnOffType.ON) {
418                                     resetValetPin();
419                                 }
420                             }
421                             break;
422                         }
423                         default:
424                             break;
425                     }
426                     return;
427                 } catch (IllegalArgumentException e) {
428                     logger.warn(
429                             "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
430                             channelID, command.toString());
431                 }
432             }
433         }
434     }
435
436     public void sendCommand(String command, String payLoad, WebTarget target) {
437         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
438             Request request = account.newRequest(this, command, payLoad, target);
439             if (stateThrottler != null) {
440                 stateThrottler.submit(COMMAND_THROTTLE, request);
441             }
442         }
443     }
444
445     public void sendCommand(String command) {
446         sendCommand(command, "{}");
447     }
448
449     public void sendCommand(String command, String payLoad) {
450         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
451             Request request = account.newRequest(this, command, payLoad, account.commandTarget);
452             if (stateThrottler != null) {
453                 stateThrottler.submit(COMMAND_THROTTLE, request);
454             }
455         }
456     }
457
458     public void sendCommand(String command, WebTarget target) {
459         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
460             Request request = account.newRequest(this, command, "{}", target);
461             if (stateThrottler != null) {
462                 stateThrottler.submit(COMMAND_THROTTLE, request);
463             }
464         }
465     }
466
467     public void requestData(String command, String payLoad) {
468         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
469             Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget);
470             if (stateThrottler != null) {
471                 stateThrottler.submit(DATA_THROTTLE, request);
472             }
473         }
474     }
475
476     @Override
477     protected void updateStatus(@NonNull ThingStatus status) {
478         super.updateStatus(status);
479     }
480
481     @Override
482     protected void updateStatus(@NonNull ThingStatus status, @NonNull ThingStatusDetail statusDetail) {
483         super.updateStatus(status, statusDetail);
484     }
485
486     @Override
487     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
488         super.updateStatus(status, statusDetail, description);
489     }
490
491     public void requestData(String command) {
492         requestData(command, null);
493     }
494
495     public void queryVehicle(String parameter) {
496         WebTarget target = account.vehicleTarget.path(parameter);
497         sendCommand(parameter, null, target);
498     }
499
500     public void requestAllData() {
501         requestData(DRIVE_STATE);
502         requestData(VEHICLE_STATE);
503         requestData(CHARGE_STATE);
504         requestData(CLIMATE_STATE);
505         requestData(GUI_STATE);
506     }
507
508     protected boolean isAwake() {
509         return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
510     }
511
512     protected boolean isInMotion() {
513         if (driveState != null) {
514             if (driveState.speed != null && driveState.shift_state != null) {
515                 return !"Undefined".equals(driveState.speed)
516                         && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
517             }
518         }
519         return false;
520     }
521
522     protected boolean isInactive() {
523         // vehicle is inactive in case
524         // - it does not charge
525         // - it has not moved in the observation period
526         return isInactive && !isCharging() && !hasMovedInSleepInterval();
527     }
528
529     protected boolean isCharging() {
530         return chargeState != null && "Charging".equals(chargeState.charging_state);
531     }
532
533     protected boolean hasMovedInSleepInterval() {
534         return lastLocationChangeTimestamp > (System.currentTimeMillis()
535                 - (MOVE_THRESHOLD_INTERVAL_MINUTES * 60 * 1000));
536     }
537
538     protected boolean allowQuery() {
539         return (isAwake() && !isInactive());
540     }
541
542     protected void setActive() {
543         isInactive = false;
544         lastLocationChangeTimestamp = System.currentTimeMillis();
545         lastLatitude = 0;
546         lastLongitude = 0;
547     }
548
549     protected boolean checkResponse(Response response, boolean immediatelyFail) {
550         if (response != null && response.getStatus() == 200) {
551             return true;
552         } else {
553             apiIntervalErrors++;
554             if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
555                 if (immediatelyFail) {
556                     logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
557                 } else {
558                     logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
559                             TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
560                             TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
561                 }
562
563                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
564                 eventClient.close();
565             } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
566                     * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
567                 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
568                 apiIntervalTimestamp = System.currentTimeMillis();
569                 apiIntervalErrors = 0;
570             }
571         }
572
573         return false;
574     }
575
576     public void setChargeLimit(int percent) {
577         JsonObject payloadObject = new JsonObject();
578         payloadObject.addProperty("percent", percent);
579         sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
580         requestData(CHARGE_STATE);
581     }
582
583     public void setSunroof(String state) {
584         JsonObject payloadObject = new JsonObject();
585         payloadObject.addProperty("state", state);
586         sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
587         requestData(VEHICLE_STATE);
588     }
589
590     public void moveSunroof(int percent) {
591         JsonObject payloadObject = new JsonObject();
592         payloadObject.addProperty("state", "move");
593         payloadObject.addProperty("percent", percent);
594         sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
595         requestData(VEHICLE_STATE);
596     }
597
598     /**
599      * Sets the driver and passenger temperatures.
600      *
601      * While setting different temperature values is supported by the API, in practice this does not always work
602      * reliably, possibly if the the
603      * only reliable method is to set the driver and passenger temperature to the same value
604      *
605      * @param driverTemperature in Celsius
606      * @param passenegerTemperature in Celsius
607      */
608     public void setTemperature(float driverTemperature, float passenegerTemperature) {
609         JsonObject payloadObject = new JsonObject();
610         payloadObject.addProperty("driver_temp", driverTemperature);
611         payloadObject.addProperty("passenger_temp", passenegerTemperature);
612         sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
613         requestData(CLIMATE_STATE);
614     }
615
616     public void setCombinedTemperature(float temperature) {
617         setTemperature(temperature, temperature);
618     }
619
620     public void setDriverTemperature(float temperature) {
621         setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
622     }
623
624     public void setPassengerTemperature(float temperature) {
625         setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
626     }
627
628     public void openFrunk() {
629         JsonObject payloadObject = new JsonObject();
630         payloadObject.addProperty("which_trunk", "front");
631         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
632         requestData(VEHICLE_STATE);
633     }
634
635     public void openTrunk() {
636         JsonObject payloadObject = new JsonObject();
637         payloadObject.addProperty("which_trunk", "rear");
638         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
639         requestData(VEHICLE_STATE);
640     }
641
642     public void closeTrunk() {
643         openTrunk();
644     }
645
646     public void setValetMode(boolean b, Integer pin) {
647         JsonObject payloadObject = new JsonObject();
648         payloadObject.addProperty("on", b);
649         if (pin != null) {
650             payloadObject.addProperty("password", String.format("%04d", pin));
651         }
652         sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
653         requestData(VEHICLE_STATE);
654     }
655
656     public void resetValetPin() {
657         sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
658         requestData(VEHICLE_STATE);
659     }
660
661     public void setMaxRangeCharging(boolean b) {
662         sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
663         requestData(CHARGE_STATE);
664     }
665
666     public void charge(boolean b) {
667         sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
668         requestData(CHARGE_STATE);
669     }
670
671     public void flashLights() {
672         sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
673     }
674
675     public void honkHorn() {
676         sendCommand(COMMAND_HONK_HORN, account.commandTarget);
677     }
678
679     public void openChargePort() {
680         sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
681         requestData(CHARGE_STATE);
682     }
683
684     public void lockDoors(boolean b) {
685         sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
686         requestData(VEHICLE_STATE);
687     }
688
689     public void autoConditioning(boolean b) {
690         sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
691         requestData(CLIMATE_STATE);
692     }
693
694     public void wakeUp() {
695         sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
696     }
697
698     protected Vehicle queryVehicle() {
699         String authHeader = account.getAuthHeader();
700
701         if (authHeader != null) {
702             try {
703                 // get a list of vehicles
704                 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
705                         .header("Authorization", authHeader).get();
706
707                 logger.debug("Querying the vehicle : Response : {}:{}", response.getStatus(), response.getStatusInfo());
708
709                 if (!checkResponse(response, true)) {
710                     logger.error("An error occurred while querying the vehicle");
711                     return null;
712                 }
713
714                 JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject();
715                 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
716
717                 for (Vehicle vehicle : vehicleArray) {
718                     logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
719                     if (vehicle.vin.equals(getConfig().get(VIN))) {
720                         vehicleJSON = gson.toJson(vehicle);
721                         parseAndUpdate("queryVehicle", null, vehicleJSON);
722                         if (logger.isTraceEnabled()) {
723                             logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
724                                     vehicle.tokens);
725                         }
726                         return vehicle;
727                     }
728                 }
729             } catch (ProcessingException e) {
730                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
731             }
732         }
733         return null;
734     }
735
736     protected void queryVehicleAndUpdate() {
737         vehicle = queryVehicle();
738         if (vehicle != null) {
739             parseAndUpdate("queryVehicle", null, vehicleJSON);
740         }
741     }
742
743     public void parseAndUpdate(String request, String payLoad, String result) {
744         final Double LOCATION_THRESHOLD = .0000001;
745
746         JsonObject jsonObject = null;
747
748         try {
749             if (request != null && result != null && !"null".equals(result)) {
750                 updateStatus(ThingStatus.ONLINE);
751                 // first, update state objects
752                 switch (request) {
753                     case DRIVE_STATE: {
754                         driveState = gson.fromJson(result, DriveState.class);
755
756                         if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
757                                 || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
758                             logger.debug("Vehicle moved, resetting last location timestamp");
759
760                             lastLatitude = driveState.latitude;
761                             lastLongitude = driveState.longitude;
762                             lastLocationChangeTimestamp = System.currentTimeMillis();
763                         }
764
765                         break;
766                     }
767                     case GUI_STATE: {
768                         guiState = gson.fromJson(result, GUIState.class);
769                         break;
770                     }
771                     case VEHICLE_STATE: {
772                         vehicleState = gson.fromJson(result, VehicleState.class);
773                         break;
774                     }
775                     case CHARGE_STATE: {
776                         chargeState = gson.fromJson(result, ChargeState.class);
777                         if (isCharging()) {
778                             updateState(CHANNEL_CHARGE, OnOffType.ON);
779                         } else {
780                             updateState(CHANNEL_CHARGE, OnOffType.OFF);
781                         }
782
783                         break;
784                     }
785                     case CLIMATE_STATE: {
786                         climateState = gson.fromJson(result, ClimateState.class);
787                         BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
788                                 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
789                         updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
790                         break;
791                     }
792                     case "queryVehicle": {
793                         if (vehicle != null && !lastState.equals(vehicle.state)) {
794                             lastState = vehicle.state;
795
796                             // in case vehicle changed to awake, refresh all data
797                             if (isAwake()) {
798                                 logger.debug("Vehicle is now awake, updating all data");
799                                 lastLocationChangeTimestamp = System.currentTimeMillis();
800                                 requestAllData();
801                             }
802
803                             setActive();
804                         }
805
806                         // reset timestamp if elapsed and set inactive to false
807                         if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
808                                 .currentTimeMillis()) {
809                             logger.debug("Vehicle did not fall asleep within sleep period, checking again");
810                             setActive();
811                         } else {
812                             boolean wasInactive = isInactive;
813                             isInactive = !isCharging() && !hasMovedInSleepInterval();
814
815                             if (!wasInactive && isInactive) {
816                                 lastStateTimestamp = System.currentTimeMillis();
817                                 logger.debug("Vehicle is inactive");
818                             }
819                         }
820
821                         break;
822                     }
823                 }
824
825                 // secondly, reformat the response string to a JSON compliant
826                 // object for some specific non-JSON compatible requests
827                 switch (request) {
828                     case MOBILE_ENABLED_STATE: {
829                         jsonObject = new JsonObject();
830                         jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
831                         break;
832                     }
833                     default: {
834                         jsonObject = parser.parse(result).getAsJsonObject();
835                         break;
836                     }
837                 }
838             }
839
840             // process the result
841             if (jsonObject != null && result != null && !"null".equals(result)) {
842                 // deal with responses for "set" commands, which get confirmed
843                 // positively, or negatively, in which case a reason for failure
844                 // is provided
845                 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
846                     boolean requestResult = jsonObject.get("result").getAsBoolean();
847                     logger.debug("The request ({}) execution was {}, and reported '{}'", new Object[] { request,
848                             requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString() });
849                 } else {
850                     Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
851
852                     long resultTimeStamp = 0;
853                     for (Map.Entry<String, JsonElement> entry : entrySet) {
854                         if ("timestamp".equals(entry.getKey())) {
855                             resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
856                             if (logger.isTraceEnabled()) {
857                                 Date date = new Date(resultTimeStamp);
858                                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
859                                 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
860                             }
861                             break;
862                         }
863                     }
864
865                     try {
866                         lock.lock();
867
868                         boolean proceed = true;
869                         if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
870                             proceed = false;
871                         }
872
873                         if (proceed) {
874                             for (Map.Entry<String, JsonElement> entry : entrySet) {
875                                 try {
876                                     TeslaChannelSelector selector = TeslaChannelSelector
877                                             .getValueSelectorFromRESTID(entry.getKey());
878                                     if (!selector.isProperty()) {
879                                         if (!entry.getValue().isJsonNull()) {
880                                             updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
881                                                     entry.getValue().getAsString(), selector, editProperties()));
882                                             if (logger.isTraceEnabled()) {
883                                                 logger.trace(
884                                                         "The variable/value pair '{}':'{}' is successfully processed",
885                                                         entry.getKey(), entry.getValue());
886                                             }
887                                         } else {
888                                             updateState(selector.getChannelID(), UnDefType.UNDEF);
889                                         }
890                                     } else {
891                                         if (!entry.getValue().isJsonNull()) {
892                                             Map<String, String> properties = editProperties();
893                                             properties.put(selector.getChannelID(), entry.getValue().getAsString());
894                                             updateProperties(properties);
895                                             if (logger.isTraceEnabled()) {
896                                                 logger.trace(
897                                                         "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
898                                                         entry.getKey(), entry.getValue(), selector.getChannelID());
899                                             }
900                                         }
901                                     }
902                                 } catch (IllegalArgumentException e) {
903                                     logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
904                                             entry.getKey(), entry.getValue());
905                                 } catch (ClassCastException | IllegalStateException e) {
906                                     logger.trace("An exception occurred while converting the JSON data : '{}'",
907                                             e.getMessage(), e);
908                                 }
909                             }
910                         } else {
911                             logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
912                                     request);
913                         }
914                     } finally {
915                         lock.unlock();
916                     }
917                 }
918             }
919         } catch (Exception p) {
920             logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
921         }
922     }
923
924     @SuppressWarnings("unchecked")
925     protected QuantityType<Temperature> commandToQuantityType(Command command) {
926         if (command instanceof QuantityType) {
927             return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
928         }
929         return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
930     }
931
932     protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
933         return roundBigDecimal(quantity.toBigDecimal()).floatValue();
934     }
935
936     protected BigDecimal roundBigDecimal(BigDecimal value) {
937         return value.setScale(1, RoundingMode.HALF_EVEN);
938     }
939
940     protected Runnable slowStateRunnable = () -> {
941         queryVehicleAndUpdate();
942
943         boolean allowQuery = allowQuery();
944
945         if (allowQuery) {
946             requestData(CHARGE_STATE);
947             requestData(CLIMATE_STATE);
948             requestData(GUI_STATE);
949             queryVehicle(MOBILE_ENABLED_STATE);
950         } else {
951             if (allowWakeUp) {
952                 wakeUp();
953             } else {
954                 if (isAwake()) {
955                     logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
956                 }
957             }
958         }
959     };
960
961     protected Runnable fastStateRunnable = () -> {
962         if (getThing().getStatus() == ThingStatus.ONLINE) {
963             boolean allowQuery = allowQuery();
964
965             if (allowQuery) {
966                 requestData(DRIVE_STATE);
967                 requestData(VEHICLE_STATE);
968             } else {
969                 if (allowWakeUp) {
970                     wakeUp();
971                 } else {
972                     if (isAwake()) {
973                         logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
974                     }
975                 }
976             }
977         }
978     };
979
980     protected Runnable eventRunnable = new Runnable() {
981         Response eventResponse;
982         BufferedReader eventBufferedReader;
983         InputStreamReader eventInputStreamReader;
984         boolean isEstablished = false;
985
986         protected boolean establishEventStream() {
987             try {
988                 if (!isEstablished) {
989                     eventBufferedReader = null;
990
991                     eventClient = clientBuilder.build()
992                             .register(new Authenticator((String) getConfig().get(CONFIG_USERNAME), vehicle.tokens[0]));
993                     eventTarget = eventClient.target(URI_EVENT).path(vehicle.vehicle_id + "/").queryParam("values",
994                             StringUtils.join(EventKeys.values(), ',', 1, EventKeys.values().length));
995                     eventResponse = eventTarget.request(MediaType.TEXT_PLAIN_TYPE).get();
996
997                     logger.debug("Event Stream: Establishing the event stream: Response: {}:{}",
998                             eventResponse.getStatus(), eventResponse.getStatusInfo());
999
1000                     if (eventResponse.getStatus() == 200) {
1001                         InputStream dummy = (InputStream) eventResponse.getEntity();
1002                         eventInputStreamReader = new InputStreamReader(dummy);
1003                         eventBufferedReader = new BufferedReader(eventInputStreamReader);
1004                         isEstablished = true;
1005                     } else if (eventResponse.getStatus() == 401) {
1006                         isEstablished = false;
1007                     } else {
1008                         isEstablished = false;
1009                     }
1010
1011                     if (!isEstablished) {
1012                         eventIntervalErrors++;
1013                         if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
1014                             logger.warn(
1015                                     "Reached the maximum number of errors ({}) for the current interval ({} seconds)",
1016                                     EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
1017                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1018                             eventClient.close();
1019                         }
1020
1021                         if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
1022                                 * EVENT_ERROR_INTERVAL_SECONDS) {
1023                             logger.trace("Resetting the error counter. ({} errors in the last interval)",
1024                                     eventIntervalErrors);
1025                             eventIntervalTimestamp = System.currentTimeMillis();
1026                             eventIntervalErrors = 0;
1027                         }
1028                     }
1029                 }
1030             } catch (Exception e) {
1031                 logger.error(
1032                         "Event stream: An exception occurred while establishing the event stream for the vehicle: '{}'",
1033                         e.getMessage());
1034                 isEstablished = false;
1035             }
1036
1037             return isEstablished;
1038         }
1039
1040         @Override
1041         public void run() {
1042             while (true) {
1043                 try {
1044                     if (getThing().getStatus() == ThingStatus.ONLINE) {
1045                         if (isAwake()) {
1046                             if (establishEventStream()) {
1047                                 String line = eventBufferedReader.readLine();
1048
1049                                 while (line != null) {
1050                                     logger.debug("Event stream: Received an event: '{}'", line);
1051                                     String vals[] = line.split(",");
1052                                     long currentTimeStamp = Long.valueOf(vals[0]);
1053                                     long systemTimeStamp = System.currentTimeMillis();
1054                                     if (logger.isDebugEnabled()) {
1055                                         SimpleDateFormat dateFormatter = new SimpleDateFormat(
1056                                                 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1057                                         logger.debug("STS {} CTS {} Delta {}",
1058                                                 dateFormatter.format(new Date(systemTimeStamp)),
1059                                                 dateFormatter.format(new Date(currentTimeStamp)),
1060                                                 systemTimeStamp - currentTimeStamp);
1061                                     }
1062                                     if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
1063                                         if (currentTimeStamp > lastTimeStamp) {
1064                                             lastTimeStamp = Long.valueOf(vals[0]);
1065                                             if (logger.isDebugEnabled()) {
1066                                                 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1067                                                         "yyyy-MM-dd'T'HH:mm:ss.SSS");
1068                                                 logger.debug("Event Stream: Event stamp is {}",
1069                                                         dateFormatter.format(new Date(lastTimeStamp)));
1070                                             }
1071                                             for (int i = 0; i < EventKeys.values().length; i++) {
1072                                                 TeslaChannelSelector selector = TeslaChannelSelector
1073                                                         .getValueSelectorFromRESTID((EventKeys.values()[i]).toString());
1074                                                 if (!selector.isProperty()) {
1075                                                     State newState = teslaChannelSelectorProxy.getState(vals[i],
1076                                                             selector, editProperties());
1077                                                     if (newState != null && !"".equals(vals[i])) {
1078                                                         updateState(selector.getChannelID(), newState);
1079                                                     } else {
1080                                                         updateState(selector.getChannelID(), UnDefType.UNDEF);
1081                                                     }
1082                                                 } else {
1083                                                     Map<String, String> properties = editProperties();
1084                                                     properties.put(selector.getChannelID(),
1085                                                             (selector.getState(vals[i])).toString());
1086                                                     updateProperties(properties);
1087                                                 }
1088                                             }
1089                                         } else {
1090                                             if (logger.isDebugEnabled()) {
1091                                                 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1092                                                         "yyyy-MM-dd'T'HH:mm:ss.SSS");
1093                                                 logger.debug(
1094                                                         "Event stream: Discarding an event with an out of sync timestamp {} (last is {})",
1095                                                         dateFormatter.format(new Date(currentTimeStamp)),
1096                                                         dateFormatter.format(new Date(lastTimeStamp)));
1097                                             }
1098                                         }
1099                                     } else {
1100                                         if (logger.isDebugEnabled()) {
1101                                             SimpleDateFormat dateFormatter = new SimpleDateFormat(
1102                                                     "yyyy-MM-dd'T'HH:mm:ss.SSS");
1103                                             logger.debug(
1104                                                     "Event Stream: Discarding an event that differs {} ms from the system time: {} (system is {})",
1105                                                     systemTimeStamp - currentTimeStamp,
1106                                                     dateFormatter.format(currentTimeStamp),
1107                                                     dateFormatter.format(systemTimeStamp));
1108                                         }
1109                                         if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
1110                                             logger.trace("Event stream: The event stream will be reset");
1111                                             isEstablished = false;
1112                                         }
1113                                     }
1114                                     line = eventBufferedReader.readLine();
1115                                 }
1116                                 logger.trace("Event stream: The end of stream was reached");
1117                                 isEstablished = false;
1118                             }
1119                         } else {
1120                             logger.debug("Event stream: The vehicle is not awake");
1121                             if (vehicle != null) {
1122                                 if (allowWakeUp) {
1123                                     // wake up the vehicle until streaming token <> 0
1124                                     logger.debug("Event stream: Waking up the vehicle");
1125                                     wakeUp();
1126                                 }
1127                             } else {
1128                                 vehicle = queryVehicle();
1129                             }
1130                             Thread.sleep(EVENT_STREAM_PAUSE);
1131                         }
1132                     }
1133                 } catch (IOException | NumberFormatException e) {
1134                     logger.debug("Event stream: An exception occurred while reading events: '{}'", e.getMessage());
1135                     isEstablished = false;
1136                 } catch (InterruptedException e) {
1137                     isEstablished = false;
1138                 }
1139
1140                 if (Thread.interrupted()) {
1141                     logger.debug("Event stream: the event stream was interrupted");
1142                     return;
1143                 }
1144             }
1145         }
1146     };
1147 }