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