]> git.basschouten.com Git - openhab-addons.git/blob
8903a961020493b0465460a06063fe6a56c50a4b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.tesla.internal.handler;
14
15 import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;
16
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
20 import java.net.URI;
21 import java.net.URISyntaxException;
22 import java.text.SimpleDateFormat;
23 import java.util.Arrays;
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 import java.util.stream.Collectors;
32
33 import javax.measure.quantity.Temperature;
34 import javax.ws.rs.ProcessingException;
35 import javax.ws.rs.client.WebTarget;
36 import javax.ws.rs.core.MediaType;
37 import javax.ws.rs.core.Response;
38
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.tesla.internal.TeslaBindingConstants;
41 import org.openhab.binding.tesla.internal.TeslaBindingConstants.EventKeys;
42 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy;
43 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy.TeslaChannelSelector;
44 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Request;
45 import org.openhab.binding.tesla.internal.protocol.ChargeState;
46 import org.openhab.binding.tesla.internal.protocol.ClimateState;
47 import org.openhab.binding.tesla.internal.protocol.DriveState;
48 import org.openhab.binding.tesla.internal.protocol.Event;
49 import org.openhab.binding.tesla.internal.protocol.GUIState;
50 import org.openhab.binding.tesla.internal.protocol.Vehicle;
51 import org.openhab.binding.tesla.internal.protocol.VehicleState;
52 import org.openhab.binding.tesla.internal.throttler.QueueChannelThrottler;
53 import org.openhab.binding.tesla.internal.throttler.Rate;
54 import org.openhab.core.io.net.http.WebSocketFactory;
55 import org.openhab.core.library.types.DecimalType;
56 import org.openhab.core.library.types.IncreaseDecreaseType;
57 import org.openhab.core.library.types.OnOffType;
58 import org.openhab.core.library.types.PercentType;
59 import org.openhab.core.library.types.QuantityType;
60 import org.openhab.core.library.types.StringType;
61 import org.openhab.core.library.unit.SIUnits;
62 import org.openhab.core.library.unit.Units;
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 FAST_STATUS_REFRESH_INTERVAL = 15000;
90     private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
91     private static final int API_SLEEP_INTERVAL_MINUTES = 20;
92     private static final int MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT = 5;
93     private static final int THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES = 60;
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 EVENT_STREAM_PAUSE = 3000;
97     private static final int EVENT_TIMESTAMP_AGE_LIMIT = 3000;
98     private static final int EVENT_TIMESTAMP_MAX_DELTA = 10000;
99     private static final int EVENT_PING_INTERVAL = 10000;
100
101     private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
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 allowWakeUpForCommands;
114     protected boolean enableEvents = false;
115     protected boolean useDriveState = false;
116     protected boolean useAdvancedStates = false;
117     protected boolean lastValidDriveStateNotNull = true;
118
119     protected long lastTimeStamp;
120     protected long apiIntervalTimestamp;
121     protected int apiIntervalErrors;
122     protected long eventIntervalTimestamp;
123     protected int eventIntervalErrors;
124     protected int inactivity = MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT;
125     protected ReentrantLock lock;
126
127     protected double lastLongitude;
128     protected double lastLatitude;
129     protected long lastLocationChangeTimestamp;
130     protected long lastDriveStateChangeToNullTimestamp;
131     protected long lastAdvModesTimestamp = System.currentTimeMillis();
132     protected long lastStateTimestamp = System.currentTimeMillis();
133     protected int backOffCounter = 0;
134
135     protected String lastState = "";
136     protected boolean isInactive = false;
137
138     protected TeslaAccountHandler account;
139
140     protected QueueChannelThrottler stateThrottler;
141     protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
142     protected Thread eventThread;
143     protected ScheduledFuture<?> fastStateJob;
144     protected ScheduledFuture<?> slowStateJob;
145     protected WebSocketFactory webSocketFactory;
146
147     private final Gson gson = new Gson();
148
149     public TeslaVehicleHandler(Thing thing, WebSocketFactory webSocketFactory) {
150         super(thing);
151         this.webSocketFactory = webSocketFactory;
152     }
153
154     @SuppressWarnings("null")
155     @Override
156     public void initialize() {
157         logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
158         updateStatus(ThingStatus.UNKNOWN);
159         allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
160         allowWakeUpForCommands = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUPFORCOMMANDS);
161         enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
162         Number inactivityParam = (Number) getConfig().get(TeslaBindingConstants.CONFIG_INACTIVITY);
163         inactivity = inactivityParam == null ? MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT : inactivityParam.intValue();
164         Boolean useDriveStateParam = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDRIVESTATE);
165         useDriveState = useDriveStateParam == null ? false : useDriveStateParam;
166         Boolean useAdvancedStatesParam = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDADVANCEDSTATES);
167         useAdvancedStates = useAdvancedStatesParam == null ? false : useAdvancedStatesParam;
168
169         account = (TeslaAccountHandler) getBridge().getHandler();
170         lock = new ReentrantLock();
171         scheduler.execute(() -> queryVehicleAndUpdate());
172
173         lock.lock();
174         try {
175             Map<Object, Rate> channels = new HashMap<>();
176             channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
177             channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
178
179             Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
180             Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
181             stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
182             stateThrottler.addRate(secondRate);
183
184             if (fastStateJob == null || fastStateJob.isCancelled()) {
185                 fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
186                         TimeUnit.MILLISECONDS);
187             }
188
189             if (slowStateJob == null || slowStateJob.isCancelled()) {
190                 slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
191                         TimeUnit.MILLISECONDS);
192             }
193
194             if (enableEvents) {
195                 if (eventThread == null) {
196                     eventThread = new Thread(eventRunnable, "OH-binding-" + getThing().getUID() + "-events");
197                     eventThread.start();
198                 }
199             }
200
201         } finally {
202             lock.unlock();
203         }
204     }
205
206     @Override
207     public void dispose() {
208         logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
209         lock.lock();
210         try {
211             if (fastStateJob != null && !fastStateJob.isCancelled()) {
212                 fastStateJob.cancel(true);
213                 fastStateJob = null;
214             }
215
216             if (slowStateJob != null && !slowStateJob.isCancelled()) {
217                 slowStateJob.cancel(true);
218                 slowStateJob = null;
219             }
220
221             if (eventThread != null && !eventThread.isInterrupted()) {
222                 eventThread.interrupt();
223                 eventThread = null;
224             }
225         } finally {
226             lock.unlock();
227         }
228     }
229
230     /**
231      * Retrieves the unique vehicle id this handler is associated with
232      *
233      * @return the vehicle id
234      */
235     public String getVehicleId() {
236         if (vehicle != null) {
237             return vehicle.id;
238         } else {
239             return null;
240         }
241     }
242
243     @Override
244     public void handleCommand(ChannelUID channelUID, Command command) {
245         logger.debug("handleCommand {} {}", channelUID, command);
246         String channelID = channelUID.getId();
247         TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
248
249         if (command instanceof RefreshType) {
250             if (!isAwake()) {
251                 logger.debug("Waking vehicle to refresh all data");
252                 wakeUp();
253             }
254
255             setActive();
256
257             // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
258             // throttled so we are safe not to break the Tesla SLA
259             requestAllData();
260         } else {
261             if (selector != null) {
262                 if (!isAwake() && allowWakeUpForCommands) {
263                     logger.debug("Waking vehicle to send command.");
264                     wakeUp();
265                     setActive();
266                 }
267                 try {
268                     switch (selector) {
269                         case CHARGE_LIMIT_SOC: {
270                             if (command instanceof PercentType) {
271                                 setChargeLimit(((PercentType) command).intValue());
272                             } else if (command instanceof OnOffType && command == OnOffType.ON) {
273                                 setChargeLimit(100);
274                             } else if (command instanceof OnOffType && command == OnOffType.OFF) {
275                                 setChargeLimit(0);
276                             } else if (command instanceof IncreaseDecreaseType
277                                     && command == IncreaseDecreaseType.INCREASE) {
278                                 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
279                             } else if (command instanceof IncreaseDecreaseType
280                                     && command == IncreaseDecreaseType.DECREASE) {
281                                 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
282                             }
283                             break;
284                         }
285                         case CHARGE_AMPS:
286                             Integer amps = null;
287                             if (command instanceof DecimalType) {
288                                 amps = ((DecimalType) command).intValue();
289                             }
290                             if (command instanceof QuantityType<?>) {
291                                 QuantityType<?> qamps = ((QuantityType<?>) command).toUnit(Units.AMPERE);
292                                 if (qamps != null) {
293                                     amps = qamps.intValue();
294                                 }
295                             }
296                             if (amps != null) {
297                                 if (amps < 5 || amps > 32) {
298                                     logger.warn("Charging amps can only be set in a range of 5-32A, but not to {}A.",
299                                             amps);
300                                     return;
301                                 }
302                                 setChargingAmps(amps);
303                             }
304                             break;
305                         case COMBINED_TEMP: {
306                             QuantityType<Temperature> quantity = commandToQuantityType(command);
307                             if (quantity != null) {
308                                 setCombinedTemperature(quanityToRoundedFloat(quantity));
309                             }
310                             break;
311                         }
312                         case DRIVER_TEMP: {
313                             QuantityType<Temperature> quantity = commandToQuantityType(command);
314                             if (quantity != null) {
315                                 setDriverTemperature(quanityToRoundedFloat(quantity));
316                             }
317                             break;
318                         }
319                         case PASSENGER_TEMP: {
320                             QuantityType<Temperature> quantity = commandToQuantityType(command);
321                             if (quantity != null) {
322                                 setPassengerTemperature(quanityToRoundedFloat(quantity));
323                             }
324                             break;
325                         }
326                         case SENTRY_MODE: {
327                             if (command instanceof OnOffType) {
328                                 setSentryMode(command == OnOffType.ON);
329                             }
330                             break;
331                         }
332                         case SUN_ROOF_STATE: {
333                             if (command instanceof StringType) {
334                                 setSunroof(command.toString());
335                             }
336                             break;
337                         }
338                         case CHARGE_TO_MAX: {
339                             if (command instanceof OnOffType) {
340                                 if (((OnOffType) command) == OnOffType.ON) {
341                                     setMaxRangeCharging(true);
342                                 } else {
343                                     setMaxRangeCharging(false);
344                                 }
345                             }
346                             break;
347                         }
348                         case CHARGE: {
349                             if (command instanceof OnOffType) {
350                                 if (((OnOffType) command) == OnOffType.ON) {
351                                     charge(true);
352                                 } else {
353                                     charge(false);
354                                 }
355                             }
356                             break;
357                         }
358                         case FLASH: {
359                             if (command instanceof OnOffType) {
360                                 if (((OnOffType) command) == OnOffType.ON) {
361                                     flashLights();
362                                 }
363                             }
364                             break;
365                         }
366                         case HONK_HORN: {
367                             if (command instanceof OnOffType) {
368                                 if (((OnOffType) command) == OnOffType.ON) {
369                                     honkHorn();
370                                 }
371                             }
372                             break;
373                         }
374                         case CHARGEPORT: {
375                             if (command instanceof OnOffType) {
376                                 if (((OnOffType) command) == OnOffType.ON) {
377                                     openChargePort();
378                                 }
379                             }
380                             break;
381                         }
382                         case DOOR_LOCK: {
383                             if (command instanceof OnOffType) {
384                                 if (((OnOffType) command) == OnOffType.ON) {
385                                     lockDoors(true);
386                                 } else {
387                                     lockDoors(false);
388                                 }
389                             }
390                             break;
391                         }
392                         case AUTO_COND: {
393                             if (command instanceof OnOffType) {
394                                 if (((OnOffType) command) == OnOffType.ON) {
395                                     autoConditioning(true);
396                                 } else {
397                                     autoConditioning(false);
398                                 }
399                             }
400                             break;
401                         }
402                         case WAKEUP: {
403                             if (command instanceof OnOffType) {
404                                 if (((OnOffType) command) == OnOffType.ON) {
405                                     wakeUp();
406                                 }
407                             }
408                             break;
409                         }
410                         case FT: {
411                             if (command instanceof OnOffType) {
412                                 if (((OnOffType) command) == OnOffType.ON) {
413                                     openFrunk();
414                                 }
415                             }
416                             break;
417                         }
418                         case RT: {
419                             if (command instanceof OnOffType) {
420                                 if (((OnOffType) command) == OnOffType.ON) {
421                                     if (vehicleState.rt == 0) {
422                                         openTrunk();
423                                     }
424                                 } else {
425                                     if (vehicleState.rt == 1) {
426                                         closeTrunk();
427                                     }
428                                 }
429                             }
430                             break;
431                         }
432                         case VALET_MODE: {
433                             if (command instanceof OnOffType) {
434                                 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
435                                 if (((OnOffType) command) == OnOffType.ON) {
436                                     setValetMode(true, valetpin);
437                                 } else {
438                                     setValetMode(false, valetpin);
439                                 }
440                             }
441                             break;
442                         }
443                         case RESET_VALET_PIN: {
444                             if (command instanceof OnOffType) {
445                                 if (((OnOffType) command) == OnOffType.ON) {
446                                     resetValetPin();
447                                 }
448                             }
449                             break;
450                         }
451                         case STEERINGWHEEL_HEATER: {
452                             if (command instanceof OnOffType) {
453                                 boolean commandBooleanValue = ((OnOffType) command) == OnOffType.ON ? true : false;
454                                 setSteeringWheelHeater(commandBooleanValue);
455                             }
456                             break;
457                         }
458                         default:
459                             break;
460                     }
461                     return;
462                 } catch (IllegalArgumentException e) {
463                     logger.warn(
464                             "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
465                             channelID, command.toString());
466                 }
467             }
468         }
469     }
470
471     public void sendCommand(String command, String payLoad, WebTarget target) {
472         if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
473             Request request = account.newRequest(this, command, payLoad, target, allowWakeUpForCommands);
474             if (stateThrottler != null) {
475                 stateThrottler.submit(COMMAND_THROTTLE, request);
476             }
477         }
478     }
479
480     public void sendCommand(String command) {
481         sendCommand(command, "{}");
482     }
483
484     public void sendCommand(String command, String payLoad) {
485         if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
486             Request request = account.newRequest(this, command, payLoad, account.commandTarget, allowWakeUpForCommands);
487             if (stateThrottler != null) {
488                 stateThrottler.submit(COMMAND_THROTTLE, request);
489             }
490         }
491     }
492
493     public void sendCommand(String command, WebTarget target) {
494         if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
495             Request request = account.newRequest(this, command, "{}", target, allowWakeUpForCommands);
496             if (stateThrottler != null) {
497                 stateThrottler.submit(COMMAND_THROTTLE, request);
498             }
499         }
500     }
501
502     public void requestData(String command, String payLoad) {
503         if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
504             Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget, false);
505             if (stateThrottler != null) {
506                 stateThrottler.submit(DATA_THROTTLE, request);
507             }
508         }
509     }
510
511     @Override
512     protected void updateStatus(ThingStatus status) {
513         super.updateStatus(status);
514     }
515
516     @Override
517     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
518         super.updateStatus(status, statusDetail);
519     }
520
521     @Override
522     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
523         super.updateStatus(status, statusDetail, description);
524     }
525
526     public void requestData(String command) {
527         requestData(command, null);
528     }
529
530     public void queryVehicle(String parameter) {
531         WebTarget target = account.vehicleTarget.path(parameter);
532         sendCommand(parameter, null, target);
533     }
534
535     public void requestAllData() {
536         requestData(DRIVE_STATE);
537         requestData(VEHICLE_STATE);
538         requestData(CHARGE_STATE);
539         requestData(CLIMATE_STATE);
540         requestData(GUI_STATE);
541     }
542
543     protected boolean isAwake() {
544         return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
545     }
546
547     protected boolean isInMotion() {
548         if (driveState != null) {
549             if (driveState.speed != null && driveState.shift_state != null) {
550                 return !"Undefined".equals(driveState.speed)
551                         && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
552             }
553         }
554         return false;
555     }
556
557     protected boolean isInactive() {
558         // vehicle is inactive in case
559         // - it does not charge
560         // - it has not moved or optionally stopped reporting drive state, in the observation period
561         // - it is not in dog, camp, keep, sentry or any other mode that keeps it online
562         return isInactive && !isCharging() && !notReadyForSleep();
563     }
564
565     protected boolean isCharging() {
566         return chargeState != null && "Charging".equals(chargeState.charging_state);
567     }
568
569     protected boolean notReadyForSleep() {
570         boolean status;
571         int computedInactivityPeriod = inactivity;
572
573         if (useAdvancedStates) {
574             if (vehicleState.is_user_present && !isInMotion()) {
575                 logger.debug("Car is occupied but stationary.");
576                 if (lastAdvModesTimestamp < (System.currentTimeMillis()
577                         - (THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES * 60 * 1000))) {
578                     logger.debug("Ignoring after {} minutes.", THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES);
579                 } else {
580                     return (backOffCounter++ % 6 == 0); // using 6 should make sure 1 out of 5 pollers get serviced,
581                                                         // about every min.
582                 }
583             } else if (vehicleState.sentry_mode) {
584                 logger.debug("Car is in sentry mode.");
585                 if (lastAdvModesTimestamp < (System.currentTimeMillis()
586                         - (THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES * 60 * 1000))) {
587                     logger.debug("Ignoring after {} minutes.", THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES);
588                 } else {
589                     return (backOffCounter++ % 6 == 0);
590                 }
591             } else if ((vehicleState.center_display_state != 0) && (!isInMotion())) {
592                 logger.debug("Car is in camp, climate keep, dog, or other mode preventing sleep. Mode {}",
593                         vehicleState.center_display_state);
594                 return (backOffCounter++ % 6 == 0);
595             } else {
596                 lastAdvModesTimestamp = System.currentTimeMillis();
597             }
598         }
599
600         if (vehicleState.homelink_nearby) {
601             computedInactivityPeriod = MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT;
602             logger.debug("Car is at home. Movement or drive state threshold is {} min.",
603                     MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT);
604         }
605
606         if (useDriveState) {
607             if (driveState.shift_state != null) {
608                 logger.debug("Car drive state not null and not ready to sleep.");
609                 return true;
610             } else {
611                 status = lastDriveStateChangeToNullTimestamp > (System.currentTimeMillis()
612                         - (computedInactivityPeriod * 60 * 1000));
613                 if (status) {
614                     logger.debug("Drivestate is null but has changed recently, therefore continuing to poll.");
615                     return status;
616                 } else {
617                     logger.debug("Drivestate has changed to null after interval {} min and can now be put to sleep.",
618                             computedInactivityPeriod);
619                     return status;
620                 }
621             }
622         } else {
623             status = lastLocationChangeTimestamp > (System.currentTimeMillis()
624                     - (computedInactivityPeriod * 60 * 1000));
625             if (status) {
626                 logger.debug("Car has moved recently and can not sleep");
627                 return status;
628             } else {
629                 logger.debug("Car has not moved in {} min, and can sleep", computedInactivityPeriod);
630                 return status;
631             }
632         }
633     }
634
635     protected boolean allowQuery() {
636         return (isAwake() && !isInactive());
637     }
638
639     protected void setActive() {
640         isInactive = false;
641         lastLocationChangeTimestamp = System.currentTimeMillis();
642         lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
643         lastLatitude = 0;
644         lastLongitude = 0;
645     }
646
647     protected boolean checkResponse(Response response, boolean immediatelyFail) {
648         if (response != null && response.getStatus() == 200) {
649             return true;
650         } else if (response != null && response.getStatus() == 401) {
651             logger.debug("The access token has expired, trying to get a new one.");
652             account.authenticate();
653             return false;
654         } else {
655             apiIntervalErrors++;
656             if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
657                 if (immediatelyFail) {
658                     logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
659                 } else {
660                     logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
661                             TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
662                             TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
663                 }
664
665                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
666             } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
667                     * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
668                 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
669                 apiIntervalTimestamp = System.currentTimeMillis();
670                 apiIntervalErrors = 0;
671             }
672         }
673
674         return false;
675     }
676
677     public void setChargeLimit(int percent) {
678         JsonObject payloadObject = new JsonObject();
679         payloadObject.addProperty("percent", percent);
680         sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
681         requestData(CHARGE_STATE);
682     }
683
684     public void setChargingAmps(int amps) {
685         JsonObject payloadObject = new JsonObject();
686         payloadObject.addProperty("charging_amps", amps);
687         sendCommand(COMMAND_SET_CHARGING_AMPS, gson.toJson(payloadObject), account.commandTarget);
688         requestData(CHARGE_STATE);
689     }
690
691     public void setSentryMode(boolean b) {
692         JsonObject payloadObject = new JsonObject();
693         payloadObject.addProperty("on", b);
694         sendCommand(COMMAND_SET_SENTRY_MODE, gson.toJson(payloadObject), account.commandTarget);
695         requestData(VEHICLE_STATE);
696     }
697
698     public void setSunroof(String state) {
699         if (state.equals("vent") || state.equals("close")) {
700             JsonObject payloadObject = new JsonObject();
701             payloadObject.addProperty("state", state);
702             sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
703             requestData(VEHICLE_STATE);
704         } else {
705             logger.warn("Ignoring invalid command '{}' for sunroof.", state);
706         }
707     }
708
709     /**
710      * Sets the driver and passenger temperatures.
711      *
712      * While setting different temperature values is supported by the API, in practice this does not always work
713      * reliably, possibly if the the
714      * only reliable method is to set the driver and passenger temperature to the same value
715      *
716      * @param driverTemperature in Celsius
717      * @param passenegerTemperature in Celsius
718      */
719     public void setTemperature(float driverTemperature, float passenegerTemperature) {
720         JsonObject payloadObject = new JsonObject();
721         payloadObject.addProperty("driver_temp", driverTemperature);
722         payloadObject.addProperty("passenger_temp", passenegerTemperature);
723         sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
724         requestData(CLIMATE_STATE);
725     }
726
727     public void setCombinedTemperature(float temperature) {
728         setTemperature(temperature, temperature);
729     }
730
731     public void setDriverTemperature(float temperature) {
732         setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
733     }
734
735     public void setPassengerTemperature(float temperature) {
736         setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
737     }
738
739     public void openFrunk() {
740         JsonObject payloadObject = new JsonObject();
741         payloadObject.addProperty("which_trunk", "front");
742         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
743         requestData(VEHICLE_STATE);
744     }
745
746     public void openTrunk() {
747         JsonObject payloadObject = new JsonObject();
748         payloadObject.addProperty("which_trunk", "rear");
749         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
750         requestData(VEHICLE_STATE);
751     }
752
753     public void closeTrunk() {
754         openTrunk();
755     }
756
757     public void setValetMode(boolean b, Integer pin) {
758         JsonObject payloadObject = new JsonObject();
759         payloadObject.addProperty("on", b);
760         if (pin != null) {
761             payloadObject.addProperty("password", String.format("%04d", pin));
762         }
763         sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
764         requestData(VEHICLE_STATE);
765     }
766
767     public void resetValetPin() {
768         sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
769         requestData(VEHICLE_STATE);
770     }
771
772     public void setMaxRangeCharging(boolean b) {
773         sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
774         requestData(CHARGE_STATE);
775     }
776
777     public void charge(boolean b) {
778         sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
779         requestData(CHARGE_STATE);
780     }
781
782     public void flashLights() {
783         sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
784     }
785
786     public void honkHorn() {
787         sendCommand(COMMAND_HONK_HORN, account.commandTarget);
788     }
789
790     public void openChargePort() {
791         sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
792         requestData(CHARGE_STATE);
793     }
794
795     public void lockDoors(boolean b) {
796         sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
797         requestData(VEHICLE_STATE);
798     }
799
800     public void autoConditioning(boolean b) {
801         sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
802         requestData(CLIMATE_STATE);
803     }
804
805     public void wakeUp() {
806         sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
807     }
808
809     public void setSteeringWheelHeater(boolean isOn) {
810         JsonObject payloadObject = new JsonObject();
811         payloadObject.addProperty("on", isOn);
812         sendCommand(COMMAND_STEERING_WHEEL_HEATER, gson.toJson(payloadObject), account.commandTarget);
813     }
814
815     protected Vehicle queryVehicle() {
816         String authHeader = account.getAuthHeader();
817
818         if (authHeader != null) {
819             try {
820                 // get a list of vehicles
821                 synchronized (account.vehiclesTarget) {
822                     Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
823                             .header("Authorization", authHeader).get();
824
825                     logger.debug("Querying the vehicle, response : {}, {}", response.getStatus(),
826                             response.getStatusInfo().getReasonPhrase());
827
828                     if (!checkResponse(response, true)) {
829                         logger.debug("An error occurred while querying the vehicle");
830                         return null;
831                     }
832
833                     JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
834                     Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
835
836                     for (Vehicle vehicle : vehicleArray) {
837                         logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
838                         if (vehicle.vin.equals(getConfig().get(VIN))) {
839                             vehicleJSON = gson.toJson(vehicle);
840                             parseAndUpdate("queryVehicle", null, vehicleJSON);
841                             if (logger.isTraceEnabled()) {
842                                 logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
843                                         vehicle.tokens);
844                             }
845                             return vehicle;
846                         }
847                     }
848
849                 }
850             } catch (ProcessingException e) {
851                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
852             }
853         }
854         return null;
855     }
856
857     protected void queryVehicleAndUpdate() {
858         vehicle = queryVehicle();
859     }
860
861     public void parseAndUpdate(String request, String payLoad, String result) {
862         final Double LOCATION_THRESHOLD = .0000001;
863
864         JsonObject jsonObject = null;
865
866         try {
867             if (request != null && result != null && !"null".equals(result)) {
868                 updateStatus(ThingStatus.ONLINE);
869                 // first, update state objects
870                 switch (request) {
871                     case DRIVE_STATE: {
872                         driveState = gson.fromJson(result, DriveState.class);
873
874                         if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
875                                 || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
876                             logger.debug("Vehicle moved, resetting last location timestamp");
877
878                             lastLatitude = driveState.latitude;
879                             lastLongitude = driveState.longitude;
880                             lastLocationChangeTimestamp = System.currentTimeMillis();
881                         }
882                         logger.trace("Drive state: {}", driveState.shift_state);
883
884                         if ((driveState.shift_state == null) && (lastValidDriveStateNotNull)) {
885                             logger.debug("Set NULL shiftstate time");
886                             lastValidDriveStateNotNull = false;
887                             lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
888                         } else if (driveState.shift_state != null) {
889                             logger.trace("Clear NULL shiftstate time");
890                             lastValidDriveStateNotNull = true;
891                         }
892
893                         break;
894                     }
895                     case GUI_STATE: {
896                         guiState = gson.fromJson(result, GUIState.class);
897                         break;
898                     }
899                     case VEHICLE_STATE: {
900                         vehicleState = gson.fromJson(result, VehicleState.class);
901                         break;
902                     }
903                     case CHARGE_STATE: {
904                         chargeState = gson.fromJson(result, ChargeState.class);
905                         if (isCharging()) {
906                             updateState(CHANNEL_CHARGE, OnOffType.ON);
907                         } else {
908                             updateState(CHANNEL_CHARGE, OnOffType.OFF);
909                         }
910
911                         break;
912                     }
913                     case CLIMATE_STATE: {
914                         climateState = gson.fromJson(result, ClimateState.class);
915                         BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
916                                 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
917                         updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
918                         break;
919                     }
920                     case "queryVehicle": {
921                         if (vehicle != null) {
922                             logger.debug("Vehicle state is {}", vehicle.state);
923                         } else {
924                             logger.debug("Vehicle state is initializing or unknown");
925                             break;
926                         }
927
928                         if (vehicle != null && "asleep".equals(vehicle.state)) {
929                             logger.debug("Vehicle is asleep.");
930                             break;
931                         }
932
933                         if (vehicle != null && !lastState.equals(vehicle.state)) {
934                             lastState = vehicle.state;
935
936                             // in case vehicle changed to awake, refresh all data
937                             if (isAwake()) {
938                                 logger.debug("Vehicle is now awake, updating all data");
939                                 lastLocationChangeTimestamp = System.currentTimeMillis();
940                                 lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
941                                 requestAllData();
942                             }
943
944                             setActive();
945                         }
946
947                         // reset timestamp if elapsed and set inactive to false
948                         if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
949                                 .currentTimeMillis()) {
950                             logger.debug("Vehicle did not fall asleep within sleep period, checking again");
951                             setActive();
952                         } else {
953                             boolean wasInactive = isInactive;
954                             isInactive = !isCharging() && !notReadyForSleep();
955
956                             if (!wasInactive && isInactive) {
957                                 lastStateTimestamp = System.currentTimeMillis();
958                                 logger.debug("Vehicle is inactive");
959                             }
960                         }
961
962                         break;
963                     }
964                 }
965
966                 // secondly, reformat the response string to a JSON compliant
967                 // object for some specific non-JSON compatible requests
968                 switch (request) {
969                     case MOBILE_ENABLED_STATE: {
970                         jsonObject = new JsonObject();
971                         jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
972                         break;
973                     }
974                     default: {
975                         jsonObject = JsonParser.parseString(result).getAsJsonObject();
976                         break;
977                     }
978                 }
979             }
980
981             // process the result
982             if (jsonObject != null && result != null && !"null".equals(result)) {
983                 // deal with responses for "set" commands, which get confirmed
984                 // positively, or negatively, in which case a reason for failure
985                 // is provided
986                 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
987                     boolean requestResult = jsonObject.get("result").getAsBoolean();
988                     logger.debug("The request ({}) execution was {}, and reported '{}'", new Object[] { request,
989                             requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString() });
990                 } else {
991                     Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
992
993                     long resultTimeStamp = 0;
994                     for (Map.Entry<String, JsonElement> entry : entrySet) {
995                         if ("timestamp".equals(entry.getKey())) {
996                             resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
997                             if (logger.isTraceEnabled()) {
998                                 Date date = new Date(resultTimeStamp);
999                                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
1000                                 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
1001                             }
1002                             break;
1003                         }
1004                     }
1005
1006                     try {
1007                         lock.lock();
1008
1009                         boolean proceed = true;
1010                         if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
1011                             proceed = false;
1012                         }
1013
1014                         if (proceed) {
1015                             for (Map.Entry<String, JsonElement> entry : entrySet) {
1016                                 try {
1017                                     TeslaChannelSelector selector = TeslaChannelSelector
1018                                             .getValueSelectorFromRESTID(entry.getKey());
1019                                     if (!selector.isProperty()) {
1020                                         if (!entry.getValue().isJsonNull()) {
1021                                             updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
1022                                                     entry.getValue().getAsString(), selector, editProperties()));
1023                                             if (logger.isTraceEnabled()) {
1024                                                 logger.trace(
1025                                                         "The variable/value pair '{}':'{}' is successfully processed",
1026                                                         entry.getKey(), entry.getValue());
1027                                             }
1028                                         } else {
1029                                             updateState(selector.getChannelID(), UnDefType.UNDEF);
1030                                         }
1031                                     } else {
1032                                         if (!entry.getValue().isJsonNull()) {
1033                                             Map<String, String> properties = editProperties();
1034                                             properties.put(selector.getChannelID(), entry.getValue().getAsString());
1035                                             updateProperties(properties);
1036                                             if (logger.isTraceEnabled()) {
1037                                                 logger.trace(
1038                                                         "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
1039                                                         entry.getKey(), entry.getValue(), selector.getChannelID());
1040                                             }
1041                                         }
1042                                     }
1043                                 } catch (IllegalArgumentException e) {
1044                                     logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
1045                                             entry.getKey(), entry.getValue());
1046                                 } catch (ClassCastException | IllegalStateException e) {
1047                                     logger.trace("An exception occurred while converting the JSON data : '{}'",
1048                                             e.getMessage(), e);
1049                                 }
1050                             }
1051                         } else {
1052                             logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
1053                                     request);
1054                         }
1055                     } finally {
1056                         lock.unlock();
1057                     }
1058                 }
1059             }
1060         } catch (Exception p) {
1061             logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
1062         }
1063     }
1064
1065     @SuppressWarnings("unchecked")
1066     protected QuantityType<Temperature> commandToQuantityType(Command command) {
1067         if (command instanceof QuantityType) {
1068             return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
1069         }
1070         return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
1071     }
1072
1073     protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
1074         return roundBigDecimal(quantity.toBigDecimal()).floatValue();
1075     }
1076
1077     protected BigDecimal roundBigDecimal(BigDecimal value) {
1078         return value.setScale(1, RoundingMode.HALF_EVEN);
1079     }
1080
1081     protected Runnable slowStateRunnable = () -> {
1082         try {
1083             queryVehicleAndUpdate();
1084             boolean allowQuery = allowQuery();
1085
1086             if (allowQuery) {
1087                 requestData(CHARGE_STATE);
1088                 requestData(CLIMATE_STATE);
1089                 requestData(GUI_STATE);
1090                 queryVehicle(MOBILE_ENABLED_STATE);
1091             } else {
1092                 if (allowWakeUp) {
1093                     wakeUp();
1094                 } else {
1095                     if (isAwake()) {
1096                         logger.debug("slowpoll: Throttled to allow sleep, occupied/idle, or in a console mode");
1097                     } else {
1098                         lastAdvModesTimestamp = System.currentTimeMillis();
1099                     }
1100                 }
1101             }
1102         } catch (Exception e) {
1103             logger.warn("Exception occurred in slowStateRunnable", e);
1104         }
1105     };
1106
1107     protected Runnable fastStateRunnable = () -> {
1108         if (getThing().getStatus() == ThingStatus.ONLINE) {
1109             boolean allowQuery = allowQuery();
1110
1111             if (allowQuery) {
1112                 requestData(DRIVE_STATE);
1113                 requestData(VEHICLE_STATE);
1114             } else {
1115                 if (allowWakeUp) {
1116                     wakeUp();
1117                 } else {
1118                     if (isAwake()) {
1119                         logger.debug("fastpoll: Throttled to allow sleep, occupied/idle, or in a console mode");
1120                     }
1121                 }
1122             }
1123         }
1124     };
1125
1126     protected Runnable eventRunnable = new Runnable() {
1127         TeslaEventEndpoint eventEndpoint;
1128         boolean isAuthenticated = false;
1129         long lastPingTimestamp = 0;
1130
1131         @Override
1132         public void run() {
1133             eventEndpoint = new TeslaEventEndpoint(getThing().getUID(), webSocketFactory);
1134             eventEndpoint.addEventHandler(new TeslaEventEndpoint.EventHandler() {
1135                 @Override
1136                 public void handleEvent(Event event) {
1137                     if (event != null) {
1138                         switch (event.msg_type) {
1139                             case "control:hello":
1140                                 logger.debug("Event : Received hello");
1141                                 break;
1142                             case "data:update":
1143                                 logger.debug("Event : Received an update: '{}'", event.value);
1144
1145                                 String vals[] = event.value.split(",");
1146                                 long currentTimeStamp = Long.valueOf(vals[0]);
1147                                 long systemTimeStamp = System.currentTimeMillis();
1148                                 if (logger.isDebugEnabled()) {
1149                                     SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
1150                                     logger.debug("STS {} CTS {} Delta {}",
1151                                             dateFormatter.format(new Date(systemTimeStamp)),
1152                                             dateFormatter.format(new Date(currentTimeStamp)),
1153                                             systemTimeStamp - currentTimeStamp);
1154                                 }
1155                                 if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
1156                                     if (currentTimeStamp > lastTimeStamp) {
1157                                         lastTimeStamp = Long.valueOf(vals[0]);
1158                                         if (logger.isDebugEnabled()) {
1159                                             SimpleDateFormat dateFormatter = new SimpleDateFormat(
1160                                                     "yyyy-MM-dd'T'HH:mm:ss.SSS");
1161                                             logger.debug("Event : Event stamp is {}",
1162                                                     dateFormatter.format(new Date(lastTimeStamp)));
1163                                         }
1164                                         for (int i = 0; i < EventKeys.values().length; i++) {
1165                                             TeslaChannelSelector selector = TeslaChannelSelector
1166                                                     .getValueSelectorFromRESTID((EventKeys.values()[i]).toString());
1167
1168                                             if (!selector.isProperty()) {
1169                                                 State newState = teslaChannelSelectorProxy.getState(vals[i], selector,
1170                                                         editProperties());
1171                                                 if (newState != null && !"".equals(vals[i])) {
1172                                                     updateState(selector.getChannelID(), newState);
1173                                                 } else {
1174                                                     updateState(selector.getChannelID(), UnDefType.UNDEF);
1175                                                 }
1176                                                 if (logger.isTraceEnabled()) {
1177                                                     logger.trace(
1178                                                             "The variable/value pair '{}':'{}' is successfully processed",
1179                                                             EventKeys.values()[i], vals[i]);
1180                                                 }
1181                                             } else {
1182                                                 Map<String, String> properties = editProperties();
1183                                                 properties.put(selector.getChannelID(),
1184                                                         (selector.getState(vals[i])).toString());
1185                                                 updateProperties(properties);
1186                                                 if (logger.isTraceEnabled()) {
1187                                                     logger.trace(
1188                                                             "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
1189                                                             EventKeys.values()[i], vals[i], selector.getChannelID());
1190                                                 }
1191                                             }
1192                                         }
1193                                     } else {
1194                                         if (logger.isDebugEnabled()) {
1195                                             SimpleDateFormat dateFormatter = new SimpleDateFormat(
1196                                                     "yyyy-MM-dd'T'HH:mm:ss.SSS");
1197                                             logger.debug(
1198                                                     "Event : Discarding an event with an out of sync timestamp {} (last is {})",
1199                                                     dateFormatter.format(new Date(currentTimeStamp)),
1200                                                     dateFormatter.format(new Date(lastTimeStamp)));
1201                                         }
1202                                     }
1203                                 } else {
1204                                     if (logger.isDebugEnabled()) {
1205                                         SimpleDateFormat dateFormatter = new SimpleDateFormat(
1206                                                 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1207                                         logger.debug(
1208                                                 "Event : Discarding an event that differs {} ms from the system time: {} (system is {})",
1209                                                 systemTimeStamp - currentTimeStamp,
1210                                                 dateFormatter.format(currentTimeStamp),
1211                                                 dateFormatter.format(systemTimeStamp));
1212                                     }
1213                                     if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
1214                                         logger.trace("Event : The event endpoint will be reset");
1215                                         eventEndpoint.close();
1216                                     }
1217                                 }
1218                                 break;
1219                             case "data:error":
1220                                 logger.debug("Event : Received an error: '{}'/'{}'", event.value, event.error_type);
1221                                 eventEndpoint.close();
1222                                 break;
1223                         }
1224                     }
1225                 }
1226             });
1227
1228             while (true) {
1229                 try {
1230                     if (getThing().getStatus() == ThingStatus.ONLINE) {
1231                         if (isAwake()) {
1232                             eventEndpoint.connect(new URI(URI_EVENT));
1233
1234                             if (eventEndpoint.isConnected()) {
1235                                 if (!isAuthenticated) {
1236                                     logger.debug("Event : Authenticating vehicle {}", vehicle.vehicle_id);
1237                                     JsonObject payloadObject = new JsonObject();
1238                                     payloadObject.addProperty("msg_type", "data:subscribe_oauth");
1239                                     payloadObject.addProperty("token", account.getAccessToken());
1240                                     payloadObject.addProperty("value", Arrays.asList(EventKeys.values()).stream()
1241                                             .skip(1).map(Enum::toString).collect(Collectors.joining(",")));
1242                                     payloadObject.addProperty("tag", vehicle.vehicle_id);
1243
1244                                     eventEndpoint.sendMessage(gson.toJson(payloadObject));
1245                                     isAuthenticated = true;
1246
1247                                     lastPingTimestamp = System.nanoTime();
1248                                 }
1249
1250                                 if (TimeUnit.MILLISECONDS.convert(System.nanoTime() - lastPingTimestamp,
1251                                         TimeUnit.NANOSECONDS) > EVENT_PING_INTERVAL) {
1252                                     logger.trace("Event : Pinging the Tesla event stream infrastructure");
1253                                     eventEndpoint.ping();
1254                                     lastPingTimestamp = System.nanoTime();
1255                                 }
1256                             }
1257
1258                             if (!eventEndpoint.isConnected()) {
1259                                 isAuthenticated = false;
1260                                 eventIntervalErrors++;
1261                                 if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
1262                                     logger.warn(
1263                                             "Event : Reached the maximum number of errors ({}) for the current interval ({} seconds)",
1264                                             EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
1265                                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1266                                     eventEndpoint.close();
1267                                 }
1268
1269                                 if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
1270                                         * EVENT_ERROR_INTERVAL_SECONDS) {
1271                                     logger.trace(
1272                                             "Event : Resetting the error counter. ({} errors in the last interval)",
1273                                             eventIntervalErrors);
1274                                     eventIntervalTimestamp = System.currentTimeMillis();
1275                                     eventIntervalErrors = 0;
1276                                 }
1277                             }
1278                         } else {
1279                             logger.debug("Event : The vehicle is not awake");
1280                             if (vehicle != null) {
1281                                 if (allowWakeUp) {
1282                                     // wake up the vehicle until streaming token <> 0
1283                                     logger.debug("Event : Waking up the vehicle");
1284                                     wakeUp();
1285                                 }
1286                             } else {
1287                                 vehicle = queryVehicle();
1288                             }
1289                         }
1290                     }
1291                 } catch (URISyntaxException | NumberFormatException | IOException e) {
1292                     logger.debug("Event : An exception occurred while processing events: '{}'", e.getMessage());
1293                 }
1294
1295                 try {
1296                     Thread.sleep(EVENT_STREAM_PAUSE);
1297                 } catch (InterruptedException e) {
1298                     logger.debug("Event : An exception occurred while putting the event thread to sleep: '{}'",
1299                             e.getMessage());
1300                 }
1301
1302                 if (Thread.interrupted()) {
1303                     logger.debug("Event : The event thread was interrupted");
1304                     return;
1305                 }
1306             }
1307         }
1308     };
1309 }