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