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