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