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