]> git.basschouten.com Git - openhab-addons.git/blob
44d55912531296474b662e70f0a2f6aef2610edf
[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.Arrays;
25 import java.util.Date;
26 import java.util.HashMap;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.locks.ReentrantLock;
32 import java.util.stream.Collectors;
33
34 import javax.measure.quantity.Temperature;
35 import javax.ws.rs.ProcessingException;
36 import javax.ws.rs.client.Client;
37 import javax.ws.rs.client.ClientBuilder;
38 import javax.ws.rs.client.WebTarget;
39 import javax.ws.rs.core.MediaType;
40 import javax.ws.rs.core.Response;
41
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
141     public TeslaVehicleHandler(Thing thing, ClientBuilder clientBuilder) {
142         super(thing);
143         this.clientBuilder = clientBuilder;
144     }
145
146     @SuppressWarnings("null")
147     @Override
148     public void initialize() {
149         logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
150         updateStatus(ThingStatus.UNKNOWN);
151         allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
152
153         // the streaming API seems to be broken - let's keep the code, if it comes back one day
154         // enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
155
156         account = (TeslaAccountHandler) getBridge().getHandler();
157         lock = new ReentrantLock();
158         scheduler.execute(() -> queryVehicleAndUpdate());
159
160         lock.lock();
161         try {
162             Map<Object, Rate> channels = new HashMap<>();
163             channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
164             channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
165
166             Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
167             Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
168             stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
169             stateThrottler.addRate(secondRate);
170
171             if (fastStateJob == null || fastStateJob.isCancelled()) {
172                 fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
173                         TimeUnit.MILLISECONDS);
174             }
175
176             if (slowStateJob == null || slowStateJob.isCancelled()) {
177                 slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
178                         TimeUnit.MILLISECONDS);
179             }
180         } finally {
181             lock.unlock();
182         }
183
184         if (enableEvents) {
185             if (eventThread == null) {
186                 eventThread = new Thread(eventRunnable, "openHAB-Tesla-Events-" + getThing().getUID());
187                 eventThread.start();
188             }
189         }
190     }
191
192     @Override
193     public void dispose() {
194         logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
195         lock.lock();
196         try {
197             if (fastStateJob != null && !fastStateJob.isCancelled()) {
198                 fastStateJob.cancel(true);
199                 fastStateJob = null;
200             }
201
202             if (slowStateJob != null && !slowStateJob.isCancelled()) {
203                 slowStateJob.cancel(true);
204                 slowStateJob = null;
205             }
206
207             if (eventThread != null && !eventThread.isInterrupted()) {
208                 eventThread.interrupt();
209                 eventThread = null;
210             }
211         } finally {
212             lock.unlock();
213         }
214
215         if (eventClient != null) {
216             eventClient.close();
217         }
218     }
219
220     /**
221      * Retrieves the unique vehicle id this handler is associated with
222      *
223      * @return the vehicle id
224      */
225     public String getVehicleId() {
226         return vehicle.id;
227     }
228
229     @Override
230     public void handleCommand(ChannelUID channelUID, Command command) {
231         logger.debug("handleCommand {} {}", channelUID, command);
232         String channelID = channelUID.getId();
233         TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
234
235         if (command instanceof RefreshType) {
236             if (!isAwake()) {
237                 logger.debug("Waking vehicle to refresh all data");
238                 wakeUp();
239             }
240
241             setActive();
242
243             // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
244             // throttled so we are safe not to break the Tesla SLA
245             requestAllData();
246         } else {
247             if (selector != null) {
248                 try {
249                     switch (selector) {
250                         case CHARGE_LIMIT_SOC: {
251                             if (command instanceof PercentType) {
252                                 setChargeLimit(((PercentType) command).intValue());
253                             } else if (command instanceof OnOffType && command == OnOffType.ON) {
254                                 setChargeLimit(100);
255                             } else if (command instanceof OnOffType && command == OnOffType.OFF) {
256                                 setChargeLimit(0);
257                             } else if (command instanceof IncreaseDecreaseType
258                                     && command == IncreaseDecreaseType.INCREASE) {
259                                 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
260                             } else if (command instanceof IncreaseDecreaseType
261                                     && command == IncreaseDecreaseType.DECREASE) {
262                                 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
263                             }
264                             break;
265                         }
266                         case COMBINED_TEMP: {
267                             QuantityType<Temperature> quantity = commandToQuantityType(command);
268                             if (quantity != null) {
269                                 setCombinedTemperature(quanityToRoundedFloat(quantity));
270                             }
271                             break;
272                         }
273                         case DRIVER_TEMP: {
274                             QuantityType<Temperature> quantity = commandToQuantityType(command);
275                             if (quantity != null) {
276                                 setDriverTemperature(quanityToRoundedFloat(quantity));
277                             }
278                             break;
279                         }
280                         case PASSENGER_TEMP: {
281                             QuantityType<Temperature> quantity = commandToQuantityType(command);
282                             if (quantity != null) {
283                                 setPassengerTemperature(quanityToRoundedFloat(quantity));
284                             }
285                             break;
286                         }
287                         case SUN_ROOF_STATE: {
288                             if (command instanceof StringType) {
289                                 setSunroof(command.toString());
290                             }
291                             break;
292                         }
293                         case SUN_ROOF: {
294                             if (command instanceof PercentType) {
295                                 moveSunroof(((PercentType) command).intValue());
296                             } else if (command instanceof OnOffType && command == OnOffType.ON) {
297                                 moveSunroof(100);
298                             } else if (command instanceof OnOffType && command == OnOffType.OFF) {
299                                 moveSunroof(0);
300                             } else if (command instanceof IncreaseDecreaseType
301                                     && command == IncreaseDecreaseType.INCREASE) {
302                                 moveSunroof(Math.min(vehicleState.sun_roof_percent_open + 1, 100));
303                             } else if (command instanceof IncreaseDecreaseType
304                                     && command == IncreaseDecreaseType.DECREASE) {
305                                 moveSunroof(Math.max(vehicleState.sun_roof_percent_open - 1, 0));
306                             }
307                             break;
308                         }
309                         case CHARGE_TO_MAX: {
310                             if (command instanceof OnOffType) {
311                                 if (((OnOffType) command) == OnOffType.ON) {
312                                     setMaxRangeCharging(true);
313                                 } else {
314                                     setMaxRangeCharging(false);
315                                 }
316                             }
317                             break;
318                         }
319                         case CHARGE: {
320                             if (command instanceof OnOffType) {
321                                 if (((OnOffType) command) == OnOffType.ON) {
322                                     charge(true);
323                                 } else {
324                                     charge(false);
325                                 }
326                             }
327                             break;
328                         }
329                         case FLASH: {
330                             if (command instanceof OnOffType) {
331                                 if (((OnOffType) command) == OnOffType.ON) {
332                                     flashLights();
333                                 }
334                             }
335                             break;
336                         }
337                         case HONK_HORN: {
338                             if (command instanceof OnOffType) {
339                                 if (((OnOffType) command) == OnOffType.ON) {
340                                     honkHorn();
341                                 }
342                             }
343                             break;
344                         }
345                         case CHARGEPORT: {
346                             if (command instanceof OnOffType) {
347                                 if (((OnOffType) command) == OnOffType.ON) {
348                                     openChargePort();
349                                 }
350                             }
351                             break;
352                         }
353                         case DOOR_LOCK: {
354                             if (command instanceof OnOffType) {
355                                 if (((OnOffType) command) == OnOffType.ON) {
356                                     lockDoors(true);
357                                 } else {
358                                     lockDoors(false);
359                                 }
360                             }
361                             break;
362                         }
363                         case AUTO_COND: {
364                             if (command instanceof OnOffType) {
365                                 if (((OnOffType) command) == OnOffType.ON) {
366                                     autoConditioning(true);
367                                 } else {
368                                     autoConditioning(false);
369                                 }
370                             }
371                             break;
372                         }
373                         case WAKEUP: {
374                             if (command instanceof OnOffType) {
375                                 if (((OnOffType) command) == OnOffType.ON) {
376                                     wakeUp();
377                                 }
378                             }
379                             break;
380                         }
381                         case FT: {
382                             if (command instanceof OnOffType) {
383                                 if (((OnOffType) command) == OnOffType.ON) {
384                                     openFrunk();
385                                 }
386                             }
387                             break;
388                         }
389                         case RT: {
390                             if (command instanceof OnOffType) {
391                                 if (((OnOffType) command) == OnOffType.ON) {
392                                     if (vehicleState.rt == 0) {
393                                         openTrunk();
394                                     }
395                                 } else {
396                                     if (vehicleState.rt == 1) {
397                                         closeTrunk();
398                                     }
399                                 }
400                             }
401                             break;
402                         }
403                         case VALET_MODE: {
404                             if (command instanceof OnOffType) {
405                                 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
406                                 if (((OnOffType) command) == OnOffType.ON) {
407                                     setValetMode(true, valetpin);
408                                 } else {
409                                     setValetMode(false, valetpin);
410                                 }
411                             }
412                             break;
413                         }
414                         case RESET_VALET_PIN: {
415                             if (command instanceof OnOffType) {
416                                 if (((OnOffType) command) == OnOffType.ON) {
417                                     resetValetPin();
418                                 }
419                             }
420                             break;
421                         }
422                         default:
423                             break;
424                     }
425                     return;
426                 } catch (IllegalArgumentException e) {
427                     logger.warn(
428                             "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
429                             channelID, command.toString());
430                 }
431             }
432         }
433     }
434
435     public void sendCommand(String command, String payLoad, WebTarget target) {
436         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
437             Request request = account.newRequest(this, command, payLoad, target);
438             if (stateThrottler != null) {
439                 stateThrottler.submit(COMMAND_THROTTLE, request);
440             }
441         }
442     }
443
444     public void sendCommand(String command) {
445         sendCommand(command, "{}");
446     }
447
448     public void sendCommand(String command, String payLoad) {
449         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
450             Request request = account.newRequest(this, command, payLoad, account.commandTarget);
451             if (stateThrottler != null) {
452                 stateThrottler.submit(COMMAND_THROTTLE, request);
453             }
454         }
455     }
456
457     public void sendCommand(String command, WebTarget target) {
458         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
459             Request request = account.newRequest(this, command, "{}", target);
460             if (stateThrottler != null) {
461                 stateThrottler.submit(COMMAND_THROTTLE, request);
462             }
463         }
464     }
465
466     public void requestData(String command, String payLoad) {
467         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
468             Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget);
469             if (stateThrottler != null) {
470                 stateThrottler.submit(DATA_THROTTLE, request);
471             }
472         }
473     }
474
475     @Override
476     protected void updateStatus(ThingStatus status) {
477         super.updateStatus(status);
478     }
479
480     @Override
481     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
482         super.updateStatus(status, statusDetail);
483     }
484
485     @Override
486     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
487         super.updateStatus(status, statusDetail, description);
488     }
489
490     public void requestData(String command) {
491         requestData(command, null);
492     }
493
494     public void queryVehicle(String parameter) {
495         WebTarget target = account.vehicleTarget.path(parameter);
496         sendCommand(parameter, null, target);
497     }
498
499     public void requestAllData() {
500         requestData(DRIVE_STATE);
501         requestData(VEHICLE_STATE);
502         requestData(CHARGE_STATE);
503         requestData(CLIMATE_STATE);
504         requestData(GUI_STATE);
505     }
506
507     protected boolean isAwake() {
508         return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
509     }
510
511     protected boolean isInMotion() {
512         if (driveState != null) {
513             if (driveState.speed != null && driveState.shift_state != null) {
514                 return !"Undefined".equals(driveState.speed)
515                         && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
516             }
517         }
518         return false;
519     }
520
521     protected boolean isInactive() {
522         // vehicle is inactive in case
523         // - it does not charge
524         // - it has not moved in the observation period
525         return isInactive && !isCharging() && !hasMovedInSleepInterval();
526     }
527
528     protected boolean isCharging() {
529         return chargeState != null && "Charging".equals(chargeState.charging_state);
530     }
531
532     protected boolean hasMovedInSleepInterval() {
533         return lastLocationChangeTimestamp > (System.currentTimeMillis()
534                 - (MOVE_THRESHOLD_INTERVAL_MINUTES * 60 * 1000));
535     }
536
537     protected boolean allowQuery() {
538         return (isAwake() && !isInactive());
539     }
540
541     protected void setActive() {
542         isInactive = false;
543         lastLocationChangeTimestamp = System.currentTimeMillis();
544         lastLatitude = 0;
545         lastLongitude = 0;
546     }
547
548     protected boolean checkResponse(Response response, boolean immediatelyFail) {
549         if (response != null && response.getStatus() == 200) {
550             return true;
551         } else {
552             apiIntervalErrors++;
553             if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
554                 if (immediatelyFail) {
555                     logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
556                 } else {
557                     logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
558                             TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
559                             TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
560                 }
561
562                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
563                 eventClient.close();
564             } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
565                     * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
566                 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
567                 apiIntervalTimestamp = System.currentTimeMillis();
568                 apiIntervalErrors = 0;
569             }
570         }
571
572         return false;
573     }
574
575     public void setChargeLimit(int percent) {
576         JsonObject payloadObject = new JsonObject();
577         payloadObject.addProperty("percent", percent);
578         sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
579         requestData(CHARGE_STATE);
580     }
581
582     public void setSunroof(String state) {
583         JsonObject payloadObject = new JsonObject();
584         payloadObject.addProperty("state", state);
585         sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
586         requestData(VEHICLE_STATE);
587     }
588
589     public void moveSunroof(int percent) {
590         JsonObject payloadObject = new JsonObject();
591         payloadObject.addProperty("state", "move");
592         payloadObject.addProperty("percent", percent);
593         sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
594         requestData(VEHICLE_STATE);
595     }
596
597     /**
598      * Sets the driver and passenger temperatures.
599      *
600      * While setting different temperature values is supported by the API, in practice this does not always work
601      * reliably, possibly if the the
602      * only reliable method is to set the driver and passenger temperature to the same value
603      *
604      * @param driverTemperature in Celsius
605      * @param passenegerTemperature in Celsius
606      */
607     public void setTemperature(float driverTemperature, float passenegerTemperature) {
608         JsonObject payloadObject = new JsonObject();
609         payloadObject.addProperty("driver_temp", driverTemperature);
610         payloadObject.addProperty("passenger_temp", passenegerTemperature);
611         sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
612         requestData(CLIMATE_STATE);
613     }
614
615     public void setCombinedTemperature(float temperature) {
616         setTemperature(temperature, temperature);
617     }
618
619     public void setDriverTemperature(float temperature) {
620         setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
621     }
622
623     public void setPassengerTemperature(float temperature) {
624         setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
625     }
626
627     public void openFrunk() {
628         JsonObject payloadObject = new JsonObject();
629         payloadObject.addProperty("which_trunk", "front");
630         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
631         requestData(VEHICLE_STATE);
632     }
633
634     public void openTrunk() {
635         JsonObject payloadObject = new JsonObject();
636         payloadObject.addProperty("which_trunk", "rear");
637         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
638         requestData(VEHICLE_STATE);
639     }
640
641     public void closeTrunk() {
642         openTrunk();
643     }
644
645     public void setValetMode(boolean b, Integer pin) {
646         JsonObject payloadObject = new JsonObject();
647         payloadObject.addProperty("on", b);
648         if (pin != null) {
649             payloadObject.addProperty("password", String.format("%04d", pin));
650         }
651         sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
652         requestData(VEHICLE_STATE);
653     }
654
655     public void resetValetPin() {
656         sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
657         requestData(VEHICLE_STATE);
658     }
659
660     public void setMaxRangeCharging(boolean b) {
661         sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
662         requestData(CHARGE_STATE);
663     }
664
665     public void charge(boolean b) {
666         sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
667         requestData(CHARGE_STATE);
668     }
669
670     public void flashLights() {
671         sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
672     }
673
674     public void honkHorn() {
675         sendCommand(COMMAND_HONK_HORN, account.commandTarget);
676     }
677
678     public void openChargePort() {
679         sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
680         requestData(CHARGE_STATE);
681     }
682
683     public void lockDoors(boolean b) {
684         sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
685         requestData(VEHICLE_STATE);
686     }
687
688     public void autoConditioning(boolean b) {
689         sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
690         requestData(CLIMATE_STATE);
691     }
692
693     public void wakeUp() {
694         sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
695     }
696
697     protected Vehicle queryVehicle() {
698         String authHeader = account.getAuthHeader();
699
700         if (authHeader != null) {
701             try {
702                 // get a list of vehicles
703                 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
704                         .header("Authorization", authHeader).get();
705
706                 logger.debug("Querying the vehicle : Response : {}:{}", response.getStatus(), response.getStatusInfo());
707
708                 if (!checkResponse(response, true)) {
709                     logger.error("An error occurred while querying the vehicle");
710                     return null;
711                 }
712
713                 JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
714                 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
715
716                 for (Vehicle vehicle : vehicleArray) {
717                     logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
718                     if (vehicle.vin.equals(getConfig().get(VIN))) {
719                         vehicleJSON = gson.toJson(vehicle);
720                         parseAndUpdate("queryVehicle", null, vehicleJSON);
721                         if (logger.isTraceEnabled()) {
722                             logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
723                                     vehicle.tokens);
724                         }
725                         return vehicle;
726                     }
727                 }
728             } catch (ProcessingException e) {
729                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
730             }
731         }
732         return null;
733     }
734
735     protected void queryVehicleAndUpdate() {
736         vehicle = queryVehicle();
737         if (vehicle != null) {
738             parseAndUpdate("queryVehicle", null, vehicleJSON);
739         }
740     }
741
742     public void parseAndUpdate(String request, String payLoad, String result) {
743         final Double LOCATION_THRESHOLD = .0000001;
744
745         JsonObject jsonObject = null;
746
747         try {
748             if (request != null && result != null && !"null".equals(result)) {
749                 updateStatus(ThingStatus.ONLINE);
750                 // first, update state objects
751                 switch (request) {
752                     case DRIVE_STATE: {
753                         driveState = gson.fromJson(result, DriveState.class);
754
755                         if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
756                                 || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
757                             logger.debug("Vehicle moved, resetting last location timestamp");
758
759                             lastLatitude = driveState.latitude;
760                             lastLongitude = driveState.longitude;
761                             lastLocationChangeTimestamp = System.currentTimeMillis();
762                         }
763
764                         break;
765                     }
766                     case GUI_STATE: {
767                         guiState = gson.fromJson(result, GUIState.class);
768                         break;
769                     }
770                     case VEHICLE_STATE: {
771                         vehicleState = gson.fromJson(result, VehicleState.class);
772                         break;
773                     }
774                     case CHARGE_STATE: {
775                         chargeState = gson.fromJson(result, ChargeState.class);
776                         if (isCharging()) {
777                             updateState(CHANNEL_CHARGE, OnOffType.ON);
778                         } else {
779                             updateState(CHANNEL_CHARGE, OnOffType.OFF);
780                         }
781
782                         break;
783                     }
784                     case CLIMATE_STATE: {
785                         climateState = gson.fromJson(result, ClimateState.class);
786                         BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
787                                 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
788                         updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
789                         break;
790                     }
791                     case "queryVehicle": {
792                         if (vehicle != null && !lastState.equals(vehicle.state)) {
793                             lastState = vehicle.state;
794
795                             // in case vehicle changed to awake, refresh all data
796                             if (isAwake()) {
797                                 logger.debug("Vehicle is now awake, updating all data");
798                                 lastLocationChangeTimestamp = System.currentTimeMillis();
799                                 requestAllData();
800                             }
801
802                             setActive();
803                         }
804
805                         // reset timestamp if elapsed and set inactive to false
806                         if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
807                                 .currentTimeMillis()) {
808                             logger.debug("Vehicle did not fall asleep within sleep period, checking again");
809                             setActive();
810                         } else {
811                             boolean wasInactive = isInactive;
812                             isInactive = !isCharging() && !hasMovedInSleepInterval();
813
814                             if (!wasInactive && isInactive) {
815                                 lastStateTimestamp = System.currentTimeMillis();
816                                 logger.debug("Vehicle is inactive");
817                             }
818                         }
819
820                         break;
821                     }
822                 }
823
824                 // secondly, reformat the response string to a JSON compliant
825                 // object for some specific non-JSON compatible requests
826                 switch (request) {
827                     case MOBILE_ENABLED_STATE: {
828                         jsonObject = new JsonObject();
829                         jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
830                         break;
831                     }
832                     default: {
833                         jsonObject = JsonParser.parseString(result).getAsJsonObject();
834                         break;
835                     }
836                 }
837             }
838
839             // process the result
840             if (jsonObject != null && result != null && !"null".equals(result)) {
841                 // deal with responses for "set" commands, which get confirmed
842                 // positively, or negatively, in which case a reason for failure
843                 // is provided
844                 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
845                     boolean requestResult = jsonObject.get("result").getAsBoolean();
846                     logger.debug("The request ({}) execution was {}, and reported '{}'", new Object[] { request,
847                             requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString() });
848                 } else {
849                     Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
850
851                     long resultTimeStamp = 0;
852                     for (Map.Entry<String, JsonElement> entry : entrySet) {
853                         if ("timestamp".equals(entry.getKey())) {
854                             resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
855                             if (logger.isTraceEnabled()) {
856                                 Date date = new Date(resultTimeStamp);
857                                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
858                                 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
859                             }
860                             break;
861                         }
862                     }
863
864                     try {
865                         lock.lock();
866
867                         boolean proceed = true;
868                         if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
869                             proceed = false;
870                         }
871
872                         if (proceed) {
873                             for (Map.Entry<String, JsonElement> entry : entrySet) {
874                                 try {
875                                     TeslaChannelSelector selector = TeslaChannelSelector
876                                             .getValueSelectorFromRESTID(entry.getKey());
877                                     if (!selector.isProperty()) {
878                                         if (!entry.getValue().isJsonNull()) {
879                                             updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
880                                                     entry.getValue().getAsString(), selector, editProperties()));
881                                             if (logger.isTraceEnabled()) {
882                                                 logger.trace(
883                                                         "The variable/value pair '{}':'{}' is successfully processed",
884                                                         entry.getKey(), entry.getValue());
885                                             }
886                                         } else {
887                                             updateState(selector.getChannelID(), UnDefType.UNDEF);
888                                         }
889                                     } else {
890                                         if (!entry.getValue().isJsonNull()) {
891                                             Map<String, String> properties = editProperties();
892                                             properties.put(selector.getChannelID(), entry.getValue().getAsString());
893                                             updateProperties(properties);
894                                             if (logger.isTraceEnabled()) {
895                                                 logger.trace(
896                                                         "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
897                                                         entry.getKey(), entry.getValue(), selector.getChannelID());
898                                             }
899                                         }
900                                     }
901                                 } catch (IllegalArgumentException e) {
902                                     logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
903                                             entry.getKey(), entry.getValue());
904                                 } catch (ClassCastException | IllegalStateException e) {
905                                     logger.trace("An exception occurred while converting the JSON data : '{}'",
906                                             e.getMessage(), e);
907                                 }
908                             }
909                         } else {
910                             logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
911                                     request);
912                         }
913                     } finally {
914                         lock.unlock();
915                     }
916                 }
917             }
918         } catch (Exception p) {
919             logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
920         }
921     }
922
923     @SuppressWarnings("unchecked")
924     protected QuantityType<Temperature> commandToQuantityType(Command command) {
925         if (command instanceof QuantityType) {
926             return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
927         }
928         return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
929     }
930
931     protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
932         return roundBigDecimal(quantity.toBigDecimal()).floatValue();
933     }
934
935     protected BigDecimal roundBigDecimal(BigDecimal value) {
936         return value.setScale(1, RoundingMode.HALF_EVEN);
937     }
938
939     protected Runnable slowStateRunnable = () -> {
940         queryVehicleAndUpdate();
941
942         boolean allowQuery = allowQuery();
943
944         if (allowQuery) {
945             requestData(CHARGE_STATE);
946             requestData(CLIMATE_STATE);
947             requestData(GUI_STATE);
948             queryVehicle(MOBILE_ENABLED_STATE);
949         } else {
950             if (allowWakeUp) {
951                 wakeUp();
952             } else {
953                 if (isAwake()) {
954                     logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
955                 }
956             }
957         }
958     };
959
960     protected Runnable fastStateRunnable = () -> {
961         if (getThing().getStatus() == ThingStatus.ONLINE) {
962             boolean allowQuery = allowQuery();
963
964             if (allowQuery) {
965                 requestData(DRIVE_STATE);
966                 requestData(VEHICLE_STATE);
967             } else {
968                 if (allowWakeUp) {
969                     wakeUp();
970                 } else {
971                     if (isAwake()) {
972                         logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
973                     }
974                 }
975             }
976         }
977     };
978
979     protected Runnable eventRunnable = new Runnable() {
980         Response eventResponse;
981         BufferedReader eventBufferedReader;
982         InputStreamReader eventInputStreamReader;
983         boolean isEstablished = false;
984
985         protected boolean establishEventStream() {
986             try {
987                 if (!isEstablished) {
988                     eventBufferedReader = null;
989
990                     eventClient = clientBuilder.build()
991                             .register(new Authenticator((String) getConfig().get(CONFIG_USERNAME), vehicle.tokens[0]));
992                     eventTarget = eventClient.target(URI_EVENT).path(vehicle.vehicle_id + "/").queryParam("values",
993                             Arrays.asList(EventKeys.values()).stream().skip(1).map(Enum::toString)
994                                     .collect(Collectors.joining(",")));
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 }