]> git.basschouten.com Git - openhab-addons.git/blob
83deb99b422f5b0ae36400364290ac2bf1577935
[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.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.InputStreamReader;
21 import java.math.BigDecimal;
22 import java.math.RoundingMode;
23 import java.text.SimpleDateFormat;
24 import java.util.Arrays;
25 import java.util.Date;
26 import java.util.HashMap;
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.Client;
37 import javax.ws.rs.client.ClientBuilder;
38 import javax.ws.rs.client.WebTarget;
39 import javax.ws.rs.core.MediaType;
40 import javax.ws.rs.core.Response;
41
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.binding.tesla.internal.TeslaBindingConstants;
44 import org.openhab.binding.tesla.internal.TeslaBindingConstants.EventKeys;
45 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy;
46 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy.TeslaChannelSelector;
47 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Authenticator;
48 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Request;
49 import org.openhab.binding.tesla.internal.protocol.ChargeState;
50 import org.openhab.binding.tesla.internal.protocol.ClimateState;
51 import org.openhab.binding.tesla.internal.protocol.DriveState;
52 import org.openhab.binding.tesla.internal.protocol.GUIState;
53 import org.openhab.binding.tesla.internal.protocol.Vehicle;
54 import org.openhab.binding.tesla.internal.protocol.VehicleState;
55 import org.openhab.binding.tesla.internal.throttler.QueueChannelThrottler;
56 import org.openhab.binding.tesla.internal.throttler.Rate;
57 import org.openhab.core.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 EVENT_STREAM_PAUSE = 5000;
92     private static final int EVENT_TIMESTAMP_AGE_LIMIT = 3000;
93     private static final int EVENT_TIMESTAMP_MAX_DELTA = 10000;
94     private static final int FAST_STATUS_REFRESH_INTERVAL = 15000;
95     private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
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 API_SLEEP_INTERVAL_MINUTES = 20;
99     private static final int MOVE_THRESHOLD_INTERVAL_MINUTES = 5;
100
101     private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
102
103     protected WebTarget eventTarget;
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 enableEvents = false;
116     protected long lastTimeStamp;
117     protected long apiIntervalTimestamp;
118     protected int apiIntervalErrors;
119     protected long eventIntervalTimestamp;
120     protected int eventIntervalErrors;
121     protected ReentrantLock lock;
122
123     protected double lastLongitude;
124     protected double lastLatitude;
125     protected long lastLocationChangeTimestamp;
126
127     protected long lastStateTimestamp = System.currentTimeMillis();
128     protected String lastState = "";
129     protected boolean isInactive = false;
130
131     protected TeslaAccountHandler account;
132
133     protected QueueChannelThrottler stateThrottler;
134     protected ClientBuilder clientBuilder;
135     protected Client eventClient;
136     protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
137     protected Thread eventThread;
138     protected ScheduledFuture<?> fastStateJob;
139     protected ScheduledFuture<?> slowStateJob;
140
141     private final Gson gson = new Gson();
142
143     public TeslaVehicleHandler(Thing thing, ClientBuilder clientBuilder) {
144         super(thing);
145         this.clientBuilder = clientBuilder;
146     }
147
148     @SuppressWarnings("null")
149     @Override
150     public void initialize() {
151         logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
152         updateStatus(ThingStatus.UNKNOWN);
153         allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
154
155         // the streaming API seems to be broken - let's keep the code, if it comes back one day
156         // enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
157
158         account = (TeslaAccountHandler) getBridge().getHandler();
159         lock = new ReentrantLock();
160         scheduler.execute(() -> queryVehicleAndUpdate());
161
162         lock.lock();
163         try {
164             Map<Object, Rate> channels = new HashMap<>();
165             channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
166             channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
167
168             Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
169             Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
170             stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
171             stateThrottler.addRate(secondRate);
172
173             if (fastStateJob == null || fastStateJob.isCancelled()) {
174                 fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
175                         TimeUnit.MILLISECONDS);
176             }
177
178             if (slowStateJob == null || slowStateJob.isCancelled()) {
179                 slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
180                         TimeUnit.MILLISECONDS);
181             }
182         } finally {
183             lock.unlock();
184         }
185
186         if (enableEvents) {
187             if (eventThread == null) {
188                 eventThread = new Thread(eventRunnable, "openHAB-Tesla-Events-" + getThing().getUID());
189                 eventThread.start();
190             }
191         }
192     }
193
194     @Override
195     public void dispose() {
196         logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
197         lock.lock();
198         try {
199             if (fastStateJob != null && !fastStateJob.isCancelled()) {
200                 fastStateJob.cancel(true);
201                 fastStateJob = null;
202             }
203
204             if (slowStateJob != null && !slowStateJob.isCancelled()) {
205                 slowStateJob.cancel(true);
206                 slowStateJob = null;
207             }
208
209             if (eventThread != null && !eventThread.isInterrupted()) {
210                 eventThread.interrupt();
211                 eventThread = null;
212             }
213         } finally {
214             lock.unlock();
215         }
216
217         if (eventClient != null) {
218             eventClient.close();
219         }
220     }
221
222     /**
223      * Retrieves the unique vehicle id this handler is associated with
224      *
225      * @return the vehicle id
226      */
227     public String getVehicleId() {
228         if (vehicle != null) {
229             return vehicle.id;
230         } else {
231             return null;
232         }
233     }
234
235     @Override
236     public void handleCommand(ChannelUID channelUID, Command command) {
237         logger.debug("handleCommand {} {}", channelUID, command);
238         String channelID = channelUID.getId();
239         TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
240
241         if (command instanceof RefreshType) {
242             if (!isAwake()) {
243                 logger.debug("Waking vehicle to refresh all data");
244                 wakeUp();
245             }
246
247             setActive();
248
249             // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
250             // throttled so we are safe not to break the Tesla SLA
251             requestAllData();
252         } else {
253             if (selector != null) {
254                 try {
255                     switch (selector) {
256                         case CHARGE_LIMIT_SOC: {
257                             if (command instanceof PercentType) {
258                                 setChargeLimit(((PercentType) command).intValue());
259                             } else if (command instanceof OnOffType && command == OnOffType.ON) {
260                                 setChargeLimit(100);
261                             } else if (command instanceof OnOffType && command == OnOffType.OFF) {
262                                 setChargeLimit(0);
263                             } else if (command instanceof IncreaseDecreaseType
264                                     && command == IncreaseDecreaseType.INCREASE) {
265                                 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
266                             } else if (command instanceof IncreaseDecreaseType
267                                     && command == IncreaseDecreaseType.DECREASE) {
268                                 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
269                             }
270                             break;
271                         }
272                         case CHARGE_AMPS:
273                             Integer amps = null;
274                             if (command instanceof DecimalType) {
275                                 amps = ((DecimalType) command).intValue();
276                             }
277                             if (command instanceof QuantityType<?>) {
278                                 QuantityType<?> qamps = ((QuantityType<?>) command).toUnit(Units.AMPERE);
279                                 if (qamps != null) {
280                                     amps = qamps.intValue();
281                                 }
282                             }
283                             if (amps != null) {
284                                 if (amps < 5 || amps > 32) {
285                                     logger.warn("Charging amps can only be set in a range of 5-32A, but not to {}A.",
286                                             amps);
287                                     return;
288                                 }
289                                 setChargingAmps(amps);
290                             }
291                             break;
292                         case COMBINED_TEMP: {
293                             QuantityType<Temperature> quantity = commandToQuantityType(command);
294                             if (quantity != null) {
295                                 setCombinedTemperature(quanityToRoundedFloat(quantity));
296                             }
297                             break;
298                         }
299                         case DRIVER_TEMP: {
300                             QuantityType<Temperature> quantity = commandToQuantityType(command);
301                             if (quantity != null) {
302                                 setDriverTemperature(quanityToRoundedFloat(quantity));
303                             }
304                             break;
305                         }
306                         case PASSENGER_TEMP: {
307                             QuantityType<Temperature> quantity = commandToQuantityType(command);
308                             if (quantity != null) {
309                                 setPassengerTemperature(quanityToRoundedFloat(quantity));
310                             }
311                             break;
312                         }
313                         case SENTRY_MODE: {
314                             if (command instanceof OnOffType) {
315                                 setSentryMode(command == OnOffType.ON);
316                             }
317                             break;
318                         }
319                         case SUN_ROOF_STATE: {
320                             if (command instanceof StringType) {
321                                 setSunroof(command.toString());
322                             }
323                             break;
324                         }
325                         case CHARGE_TO_MAX: {
326                             if (command instanceof OnOffType) {
327                                 if (((OnOffType) command) == OnOffType.ON) {
328                                     setMaxRangeCharging(true);
329                                 } else {
330                                     setMaxRangeCharging(false);
331                                 }
332                             }
333                             break;
334                         }
335                         case CHARGE: {
336                             if (command instanceof OnOffType) {
337                                 if (((OnOffType) command) == OnOffType.ON) {
338                                     charge(true);
339                                 } else {
340                                     charge(false);
341                                 }
342                             }
343                             break;
344                         }
345                         case FLASH: {
346                             if (command instanceof OnOffType) {
347                                 if (((OnOffType) command) == OnOffType.ON) {
348                                     flashLights();
349                                 }
350                             }
351                             break;
352                         }
353                         case HONK_HORN: {
354                             if (command instanceof OnOffType) {
355                                 if (((OnOffType) command) == OnOffType.ON) {
356                                     honkHorn();
357                                 }
358                             }
359                             break;
360                         }
361                         case CHARGEPORT: {
362                             if (command instanceof OnOffType) {
363                                 if (((OnOffType) command) == OnOffType.ON) {
364                                     openChargePort();
365                                 }
366                             }
367                             break;
368                         }
369                         case DOOR_LOCK: {
370                             if (command instanceof OnOffType) {
371                                 if (((OnOffType) command) == OnOffType.ON) {
372                                     lockDoors(true);
373                                 } else {
374                                     lockDoors(false);
375                                 }
376                             }
377                             break;
378                         }
379                         case AUTO_COND: {
380                             if (command instanceof OnOffType) {
381                                 if (((OnOffType) command) == OnOffType.ON) {
382                                     autoConditioning(true);
383                                 } else {
384                                     autoConditioning(false);
385                                 }
386                             }
387                             break;
388                         }
389                         case WAKEUP: {
390                             if (command instanceof OnOffType) {
391                                 if (((OnOffType) command) == OnOffType.ON) {
392                                     wakeUp();
393                                 }
394                             }
395                             break;
396                         }
397                         case FT: {
398                             if (command instanceof OnOffType) {
399                                 if (((OnOffType) command) == OnOffType.ON) {
400                                     openFrunk();
401                                 }
402                             }
403                             break;
404                         }
405                         case RT: {
406                             if (command instanceof OnOffType) {
407                                 if (((OnOffType) command) == OnOffType.ON) {
408                                     if (vehicleState.rt == 0) {
409                                         openTrunk();
410                                     }
411                                 } else {
412                                     if (vehicleState.rt == 1) {
413                                         closeTrunk();
414                                     }
415                                 }
416                             }
417                             break;
418                         }
419                         case VALET_MODE: {
420                             if (command instanceof OnOffType) {
421                                 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
422                                 if (((OnOffType) command) == OnOffType.ON) {
423                                     setValetMode(true, valetpin);
424                                 } else {
425                                     setValetMode(false, valetpin);
426                                 }
427                             }
428                             break;
429                         }
430                         case RESET_VALET_PIN: {
431                             if (command instanceof OnOffType) {
432                                 if (((OnOffType) command) == OnOffType.ON) {
433                                     resetValetPin();
434                                 }
435                             }
436                             break;
437                         }
438                         default:
439                             break;
440                     }
441                     return;
442                 } catch (IllegalArgumentException e) {
443                     logger.warn(
444                             "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
445                             channelID, command.toString());
446                 }
447             }
448         }
449     }
450
451     public void sendCommand(String command, String payLoad, WebTarget target) {
452         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
453             Request request = account.newRequest(this, command, payLoad, target);
454             if (stateThrottler != null) {
455                 stateThrottler.submit(COMMAND_THROTTLE, request);
456             }
457         }
458     }
459
460     public void sendCommand(String command) {
461         sendCommand(command, "{}");
462     }
463
464     public void sendCommand(String command, String payLoad) {
465         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
466             Request request = account.newRequest(this, command, payLoad, account.commandTarget);
467             if (stateThrottler != null) {
468                 stateThrottler.submit(COMMAND_THROTTLE, request);
469             }
470         }
471     }
472
473     public void sendCommand(String command, WebTarget target) {
474         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
475             Request request = account.newRequest(this, command, "{}", target);
476             if (stateThrottler != null) {
477                 stateThrottler.submit(COMMAND_THROTTLE, request);
478             }
479         }
480     }
481
482     public void requestData(String command, String payLoad) {
483         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
484             Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget);
485             if (stateThrottler != null) {
486                 stateThrottler.submit(DATA_THROTTLE, request);
487             }
488         }
489     }
490
491     @Override
492     protected void updateStatus(ThingStatus status) {
493         super.updateStatus(status);
494     }
495
496     @Override
497     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
498         super.updateStatus(status, statusDetail);
499     }
500
501     @Override
502     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
503         super.updateStatus(status, statusDetail, description);
504     }
505
506     public void requestData(String command) {
507         requestData(command, null);
508     }
509
510     public void queryVehicle(String parameter) {
511         WebTarget target = account.vehicleTarget.path(parameter);
512         sendCommand(parameter, null, target);
513     }
514
515     public void requestAllData() {
516         requestData(DRIVE_STATE);
517         requestData(VEHICLE_STATE);
518         requestData(CHARGE_STATE);
519         requestData(CLIMATE_STATE);
520         requestData(GUI_STATE);
521     }
522
523     protected boolean isAwake() {
524         return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
525     }
526
527     protected boolean isInMotion() {
528         if (driveState != null) {
529             if (driveState.speed != null && driveState.shift_state != null) {
530                 return !"Undefined".equals(driveState.speed)
531                         && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
532             }
533         }
534         return false;
535     }
536
537     protected boolean isInactive() {
538         // vehicle is inactive in case
539         // - it does not charge
540         // - it has not moved in the observation period
541         return isInactive && !isCharging() && !hasMovedInSleepInterval();
542     }
543
544     protected boolean isCharging() {
545         return chargeState != null && "Charging".equals(chargeState.charging_state);
546     }
547
548     protected boolean hasMovedInSleepInterval() {
549         return lastLocationChangeTimestamp > (System.currentTimeMillis()
550                 - (MOVE_THRESHOLD_INTERVAL_MINUTES * 60 * 1000));
551     }
552
553     protected boolean allowQuery() {
554         return (isAwake() && !isInactive());
555     }
556
557     protected void setActive() {
558         isInactive = false;
559         lastLocationChangeTimestamp = System.currentTimeMillis();
560         lastLatitude = 0;
561         lastLongitude = 0;
562     }
563
564     protected boolean checkResponse(Response response, boolean immediatelyFail) {
565         if (response != null && response.getStatus() == 200) {
566             return true;
567         } else {
568             apiIntervalErrors++;
569             if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
570                 if (immediatelyFail) {
571                     logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
572                 } else {
573                     logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
574                             TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
575                             TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
576                 }
577
578                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
579                 if (eventClient != null) {
580                     eventClient.close();
581                 }
582             } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
583                     * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
584                 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
585                 apiIntervalTimestamp = System.currentTimeMillis();
586                 apiIntervalErrors = 0;
587             }
588         }
589
590         return false;
591     }
592
593     public void setChargeLimit(int percent) {
594         JsonObject payloadObject = new JsonObject();
595         payloadObject.addProperty("percent", percent);
596         sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
597         requestData(CHARGE_STATE);
598     }
599
600     public void setChargingAmps(int amps) {
601         JsonObject payloadObject = new JsonObject();
602         payloadObject.addProperty("charging_amps", amps);
603         sendCommand(COMMAND_SET_CHARGING_AMPS, gson.toJson(payloadObject), account.commandTarget);
604         requestData(CHARGE_STATE);
605     }
606
607     public void setSentryMode(boolean b) {
608         JsonObject payloadObject = new JsonObject();
609         payloadObject.addProperty("on", b);
610         sendCommand(COMMAND_SET_SENTRY_MODE, gson.toJson(payloadObject), account.commandTarget);
611         requestData(VEHICLE_STATE);
612     }
613
614     public void setSunroof(String state) {
615         if (state.equals("vent") || state.equals("close")) {
616             JsonObject payloadObject = new JsonObject();
617             payloadObject.addProperty("state", state);
618             sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
619             requestData(VEHICLE_STATE);
620         } else {
621             logger.warn("Ignoring invalid command '{}' for sunroof.", state);
622         }
623     }
624
625     /**
626      * Sets the driver and passenger temperatures.
627      *
628      * While setting different temperature values is supported by the API, in practice this does not always work
629      * reliably, possibly if the the
630      * only reliable method is to set the driver and passenger temperature to the same value
631      *
632      * @param driverTemperature in Celsius
633      * @param passenegerTemperature in Celsius
634      */
635     public void setTemperature(float driverTemperature, float passenegerTemperature) {
636         JsonObject payloadObject = new JsonObject();
637         payloadObject.addProperty("driver_temp", driverTemperature);
638         payloadObject.addProperty("passenger_temp", passenegerTemperature);
639         sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
640         requestData(CLIMATE_STATE);
641     }
642
643     public void setCombinedTemperature(float temperature) {
644         setTemperature(temperature, temperature);
645     }
646
647     public void setDriverTemperature(float temperature) {
648         setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
649     }
650
651     public void setPassengerTemperature(float temperature) {
652         setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
653     }
654
655     public void openFrunk() {
656         JsonObject payloadObject = new JsonObject();
657         payloadObject.addProperty("which_trunk", "front");
658         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
659         requestData(VEHICLE_STATE);
660     }
661
662     public void openTrunk() {
663         JsonObject payloadObject = new JsonObject();
664         payloadObject.addProperty("which_trunk", "rear");
665         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
666         requestData(VEHICLE_STATE);
667     }
668
669     public void closeTrunk() {
670         openTrunk();
671     }
672
673     public void setValetMode(boolean b, Integer pin) {
674         JsonObject payloadObject = new JsonObject();
675         payloadObject.addProperty("on", b);
676         if (pin != null) {
677             payloadObject.addProperty("password", String.format("%04d", pin));
678         }
679         sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
680         requestData(VEHICLE_STATE);
681     }
682
683     public void resetValetPin() {
684         sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
685         requestData(VEHICLE_STATE);
686     }
687
688     public void setMaxRangeCharging(boolean b) {
689         sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
690         requestData(CHARGE_STATE);
691     }
692
693     public void charge(boolean b) {
694         sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
695         requestData(CHARGE_STATE);
696     }
697
698     public void flashLights() {
699         sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
700     }
701
702     public void honkHorn() {
703         sendCommand(COMMAND_HONK_HORN, account.commandTarget);
704     }
705
706     public void openChargePort() {
707         sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
708         requestData(CHARGE_STATE);
709     }
710
711     public void lockDoors(boolean b) {
712         sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
713         requestData(VEHICLE_STATE);
714     }
715
716     public void autoConditioning(boolean b) {
717         sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
718         requestData(CLIMATE_STATE);
719     }
720
721     public void wakeUp() {
722         sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
723     }
724
725     protected Vehicle queryVehicle() {
726         String authHeader = account.getAuthHeader();
727
728         if (authHeader != null) {
729             try {
730                 // get a list of vehicles
731                 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
732                         .header("Authorization", authHeader).get();
733
734                 logger.debug("Querying the vehicle : Response : {}:{}", response.getStatus(), response.getStatusInfo());
735
736                 if (!checkResponse(response, true)) {
737                     logger.error("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         queryVehicleAndUpdate();
969
970         boolean allowQuery = allowQuery();
971
972         if (allowQuery) {
973             requestData(CHARGE_STATE);
974             requestData(CLIMATE_STATE);
975             requestData(GUI_STATE);
976             queryVehicle(MOBILE_ENABLED_STATE);
977         } else {
978             if (allowWakeUp) {
979                 wakeUp();
980             } else {
981                 if (isAwake()) {
982                     logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
983                 }
984             }
985         }
986     };
987
988     protected Runnable fastStateRunnable = () -> {
989         if (getThing().getStatus() == ThingStatus.ONLINE) {
990             boolean allowQuery = allowQuery();
991
992             if (allowQuery) {
993                 requestData(DRIVE_STATE);
994                 requestData(VEHICLE_STATE);
995             } else {
996                 if (allowWakeUp) {
997                     wakeUp();
998                 } else {
999                     if (isAwake()) {
1000                         logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
1001                     }
1002                 }
1003             }
1004         }
1005     };
1006
1007     protected Runnable eventRunnable = new Runnable() {
1008         Response eventResponse;
1009         BufferedReader eventBufferedReader;
1010         InputStreamReader eventInputStreamReader;
1011         boolean isEstablished = false;
1012
1013         protected boolean establishEventStream() {
1014             try {
1015                 if (!isEstablished) {
1016                     eventBufferedReader = null;
1017
1018                     eventClient = clientBuilder.build()
1019                             .register(new Authenticator((String) getConfig().get(CONFIG_USERNAME), vehicle.tokens[0]));
1020                     eventTarget = eventClient.target(URI_EVENT).path(vehicle.vehicle_id + "/").queryParam("values",
1021                             Arrays.asList(EventKeys.values()).stream().skip(1).map(Enum::toString)
1022                                     .collect(Collectors.joining(",")));
1023                     eventResponse = eventTarget.request(MediaType.TEXT_PLAIN_TYPE).get();
1024
1025                     logger.debug("Event Stream: Establishing the event stream: Response: {}:{}",
1026                             eventResponse.getStatus(), eventResponse.getStatusInfo());
1027
1028                     if (eventResponse.getStatus() == 200) {
1029                         InputStream dummy = (InputStream) eventResponse.getEntity();
1030                         eventInputStreamReader = new InputStreamReader(dummy);
1031                         eventBufferedReader = new BufferedReader(eventInputStreamReader);
1032                         isEstablished = true;
1033                     } else if (eventResponse.getStatus() == 401) {
1034                         isEstablished = false;
1035                     } else {
1036                         isEstablished = false;
1037                     }
1038
1039                     if (!isEstablished) {
1040                         eventIntervalErrors++;
1041                         if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
1042                             logger.warn(
1043                                     "Reached the maximum number of errors ({}) for the current interval ({} seconds)",
1044                                     EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
1045                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1046                             eventClient.close();
1047                         }
1048
1049                         if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
1050                                 * EVENT_ERROR_INTERVAL_SECONDS) {
1051                             logger.trace("Resetting the error counter. ({} errors in the last interval)",
1052                                     eventIntervalErrors);
1053                             eventIntervalTimestamp = System.currentTimeMillis();
1054                             eventIntervalErrors = 0;
1055                         }
1056                     }
1057                 }
1058             } catch (Exception e) {
1059                 logger.error(
1060                         "Event stream: An exception occurred while establishing the event stream for the vehicle: '{}'",
1061                         e.getMessage());
1062                 isEstablished = false;
1063             }
1064
1065             return isEstablished;
1066         }
1067
1068         @Override
1069         public void run() {
1070             while (true) {
1071                 try {
1072                     if (getThing().getStatus() == ThingStatus.ONLINE) {
1073                         if (isAwake()) {
1074                             if (establishEventStream()) {
1075                                 String line = eventBufferedReader.readLine();
1076
1077                                 while (line != null) {
1078                                     logger.debug("Event stream: Received an event: '{}'", line);
1079                                     String vals[] = line.split(",");
1080                                     long currentTimeStamp = Long.valueOf(vals[0]);
1081                                     long systemTimeStamp = System.currentTimeMillis();
1082                                     if (logger.isDebugEnabled()) {
1083                                         SimpleDateFormat dateFormatter = new SimpleDateFormat(
1084                                                 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1085                                         logger.debug("STS {} CTS {} Delta {}",
1086                                                 dateFormatter.format(new Date(systemTimeStamp)),
1087                                                 dateFormatter.format(new Date(currentTimeStamp)),
1088                                                 systemTimeStamp - currentTimeStamp);
1089                                     }
1090                                     if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
1091                                         if (currentTimeStamp > lastTimeStamp) {
1092                                             lastTimeStamp = Long.valueOf(vals[0]);
1093                                             if (logger.isDebugEnabled()) {
1094                                                 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1095                                                         "yyyy-MM-dd'T'HH:mm:ss.SSS");
1096                                                 logger.debug("Event Stream: Event stamp is {}",
1097                                                         dateFormatter.format(new Date(lastTimeStamp)));
1098                                             }
1099                                             for (int i = 0; i < EventKeys.values().length; i++) {
1100                                                 TeslaChannelSelector selector = TeslaChannelSelector
1101                                                         .getValueSelectorFromRESTID((EventKeys.values()[i]).toString());
1102                                                 if (!selector.isProperty()) {
1103                                                     State newState = teslaChannelSelectorProxy.getState(vals[i],
1104                                                             selector, editProperties());
1105                                                     if (newState != null && !"".equals(vals[i])) {
1106                                                         updateState(selector.getChannelID(), newState);
1107                                                     } else {
1108                                                         updateState(selector.getChannelID(), UnDefType.UNDEF);
1109                                                     }
1110                                                 } else {
1111                                                     Map<String, String> properties = editProperties();
1112                                                     properties.put(selector.getChannelID(),
1113                                                             (selector.getState(vals[i])).toString());
1114                                                     updateProperties(properties);
1115                                                 }
1116                                             }
1117                                         } else {
1118                                             if (logger.isDebugEnabled()) {
1119                                                 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1120                                                         "yyyy-MM-dd'T'HH:mm:ss.SSS");
1121                                                 logger.debug(
1122                                                         "Event stream: Discarding an event with an out of sync timestamp {} (last is {})",
1123                                                         dateFormatter.format(new Date(currentTimeStamp)),
1124                                                         dateFormatter.format(new Date(lastTimeStamp)));
1125                                             }
1126                                         }
1127                                     } else {
1128                                         if (logger.isDebugEnabled()) {
1129                                             SimpleDateFormat dateFormatter = new SimpleDateFormat(
1130                                                     "yyyy-MM-dd'T'HH:mm:ss.SSS");
1131                                             logger.debug(
1132                                                     "Event Stream: Discarding an event that differs {} ms from the system time: {} (system is {})",
1133                                                     systemTimeStamp - currentTimeStamp,
1134                                                     dateFormatter.format(currentTimeStamp),
1135                                                     dateFormatter.format(systemTimeStamp));
1136                                         }
1137                                         if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
1138                                             logger.trace("Event stream: The event stream will be reset");
1139                                             isEstablished = false;
1140                                         }
1141                                     }
1142                                     line = eventBufferedReader.readLine();
1143                                 }
1144                                 logger.trace("Event stream: The end of stream was reached");
1145                                 isEstablished = false;
1146                             }
1147                         } else {
1148                             logger.debug("Event stream: The vehicle is not awake");
1149                             if (vehicle != null) {
1150                                 if (allowWakeUp) {
1151                                     // wake up the vehicle until streaming token <> 0
1152                                     logger.debug("Event stream: Waking up the vehicle");
1153                                     wakeUp();
1154                                 }
1155                             } else {
1156                                 vehicle = queryVehicle();
1157                             }
1158                             Thread.sleep(EVENT_STREAM_PAUSE);
1159                         }
1160                     }
1161                 } catch (IOException | NumberFormatException e) {
1162                     logger.debug("Event stream: An exception occurred while reading events: '{}'", e.getMessage());
1163                     isEstablished = false;
1164                 } catch (InterruptedException e) {
1165                     isEstablished = false;
1166                 }
1167
1168                 if (Thread.interrupted()) {
1169                     logger.debug("Event stream: the event stream was interrupted");
1170                     return;
1171                 }
1172             }
1173         }
1174     };
1175 }