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