]> git.basschouten.com Git - openhab-addons.git/blob
e81a006a518318fa3b117a39d414db592478dec6
[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 if (response != null && response.getStatus() == 401) {
553             logger.debug("The access token has expired, trying to get a new one.");
554             account.authenticate();
555             return false;
556         } else {
557             apiIntervalErrors++;
558             if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
559                 if (immediatelyFail) {
560                     logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
561                 } else {
562                     logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
563                             TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
564                             TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
565                 }
566
567                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
568                 if (eventClient != null) {
569                     eventClient.close();
570                 }
571             } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
572                     * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
573                 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
574                 apiIntervalTimestamp = System.currentTimeMillis();
575                 apiIntervalErrors = 0;
576             }
577         }
578
579         return false;
580     }
581
582     public void setChargeLimit(int percent) {
583         JsonObject payloadObject = new JsonObject();
584         payloadObject.addProperty("percent", percent);
585         sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
586         requestData(CHARGE_STATE);
587     }
588
589     public void setChargingAmps(int amps) {
590         JsonObject payloadObject = new JsonObject();
591         payloadObject.addProperty("charging_amps", amps);
592         sendCommand(COMMAND_SET_CHARGING_AMPS, gson.toJson(payloadObject), account.commandTarget);
593         requestData(CHARGE_STATE);
594     }
595
596     public void setSentryMode(boolean b) {
597         JsonObject payloadObject = new JsonObject();
598         payloadObject.addProperty("on", b);
599         sendCommand(COMMAND_SET_SENTRY_MODE, gson.toJson(payloadObject), account.commandTarget);
600         requestData(VEHICLE_STATE);
601     }
602
603     public void setSunroof(String state) {
604         if (state.equals("vent") || state.equals("close")) {
605             JsonObject payloadObject = new JsonObject();
606             payloadObject.addProperty("state", state);
607             sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
608             requestData(VEHICLE_STATE);
609         } else {
610             logger.warn("Ignoring invalid command '{}' for sunroof.", state);
611         }
612     }
613
614     /**
615      * Sets the driver and passenger temperatures.
616      *
617      * While setting different temperature values is supported by the API, in practice this does not always work
618      * reliably, possibly if the the
619      * only reliable method is to set the driver and passenger temperature to the same value
620      *
621      * @param driverTemperature in Celsius
622      * @param passenegerTemperature in Celsius
623      */
624     public void setTemperature(float driverTemperature, float passenegerTemperature) {
625         JsonObject payloadObject = new JsonObject();
626         payloadObject.addProperty("driver_temp", driverTemperature);
627         payloadObject.addProperty("passenger_temp", passenegerTemperature);
628         sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
629         requestData(CLIMATE_STATE);
630     }
631
632     public void setCombinedTemperature(float temperature) {
633         setTemperature(temperature, temperature);
634     }
635
636     public void setDriverTemperature(float temperature) {
637         setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
638     }
639
640     public void setPassengerTemperature(float temperature) {
641         setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
642     }
643
644     public void openFrunk() {
645         JsonObject payloadObject = new JsonObject();
646         payloadObject.addProperty("which_trunk", "front");
647         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
648         requestData(VEHICLE_STATE);
649     }
650
651     public void openTrunk() {
652         JsonObject payloadObject = new JsonObject();
653         payloadObject.addProperty("which_trunk", "rear");
654         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
655         requestData(VEHICLE_STATE);
656     }
657
658     public void closeTrunk() {
659         openTrunk();
660     }
661
662     public void setValetMode(boolean b, Integer pin) {
663         JsonObject payloadObject = new JsonObject();
664         payloadObject.addProperty("on", b);
665         if (pin != null) {
666             payloadObject.addProperty("password", String.format("%04d", pin));
667         }
668         sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
669         requestData(VEHICLE_STATE);
670     }
671
672     public void resetValetPin() {
673         sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
674         requestData(VEHICLE_STATE);
675     }
676
677     public void setMaxRangeCharging(boolean b) {
678         sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
679         requestData(CHARGE_STATE);
680     }
681
682     public void charge(boolean b) {
683         sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
684         requestData(CHARGE_STATE);
685     }
686
687     public void flashLights() {
688         sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
689     }
690
691     public void honkHorn() {
692         sendCommand(COMMAND_HONK_HORN, account.commandTarget);
693     }
694
695     public void openChargePort() {
696         sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
697         requestData(CHARGE_STATE);
698     }
699
700     public void lockDoors(boolean b) {
701         sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
702         requestData(VEHICLE_STATE);
703     }
704
705     public void autoConditioning(boolean b) {
706         sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
707         requestData(CLIMATE_STATE);
708     }
709
710     public void wakeUp() {
711         sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
712     }
713
714     protected Vehicle queryVehicle() {
715         String authHeader = account.getAuthHeader();
716
717         if (authHeader != null) {
718             try {
719                 // get a list of vehicles
720                 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
721                         .header("Authorization", authHeader).get();
722
723                 logger.debug("Querying the vehicle, response : {}, {}", response.getStatus(),
724                         response.getStatusInfo().getReasonPhrase());
725
726                 if (!checkResponse(response, true)) {
727                     logger.debug("An error occurred while querying the vehicle");
728                     return null;
729                 }
730
731                 JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
732                 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
733
734                 for (Vehicle vehicle : vehicleArray) {
735                     logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
736                     if (vehicle.vin.equals(getConfig().get(VIN))) {
737                         vehicleJSON = gson.toJson(vehicle);
738                         parseAndUpdate("queryVehicle", null, vehicleJSON);
739                         if (logger.isTraceEnabled()) {
740                             logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
741                                     vehicle.tokens);
742                         }
743                         return vehicle;
744                     }
745                 }
746             } catch (ProcessingException e) {
747                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
748             }
749         }
750         return null;
751     }
752
753     protected void queryVehicleAndUpdate() {
754         vehicle = queryVehicle();
755         if (vehicle != null) {
756             parseAndUpdate("queryVehicle", null, vehicleJSON);
757         }
758     }
759
760     public void parseAndUpdate(String request, String payLoad, String result) {
761         final Double LOCATION_THRESHOLD = .0000001;
762
763         JsonObject jsonObject = null;
764
765         try {
766             if (request != null && result != null && !"null".equals(result)) {
767                 updateStatus(ThingStatus.ONLINE);
768                 // first, update state objects
769                 switch (request) {
770                     case DRIVE_STATE: {
771                         driveState = gson.fromJson(result, DriveState.class);
772
773                         if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
774                                 || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
775                             logger.debug("Vehicle moved, resetting last location timestamp");
776
777                             lastLatitude = driveState.latitude;
778                             lastLongitude = driveState.longitude;
779                             lastLocationChangeTimestamp = System.currentTimeMillis();
780                         }
781
782                         break;
783                     }
784                     case GUI_STATE: {
785                         guiState = gson.fromJson(result, GUIState.class);
786                         break;
787                     }
788                     case VEHICLE_STATE: {
789                         vehicleState = gson.fromJson(result, VehicleState.class);
790                         break;
791                     }
792                     case CHARGE_STATE: {
793                         chargeState = gson.fromJson(result, ChargeState.class);
794                         if (isCharging()) {
795                             updateState(CHANNEL_CHARGE, OnOffType.ON);
796                         } else {
797                             updateState(CHANNEL_CHARGE, OnOffType.OFF);
798                         }
799
800                         break;
801                     }
802                     case CLIMATE_STATE: {
803                         climateState = gson.fromJson(result, ClimateState.class);
804                         BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
805                                 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
806                         updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
807                         break;
808                     }
809                     case "queryVehicle": {
810                         if (vehicle != null && !lastState.equals(vehicle.state)) {
811                             lastState = vehicle.state;
812
813                             // in case vehicle changed to awake, refresh all data
814                             if (isAwake()) {
815                                 logger.debug("Vehicle is now awake, updating all data");
816                                 lastLocationChangeTimestamp = System.currentTimeMillis();
817                                 requestAllData();
818                             }
819
820                             setActive();
821                         }
822
823                         // reset timestamp if elapsed and set inactive to false
824                         if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
825                                 .currentTimeMillis()) {
826                             logger.debug("Vehicle did not fall asleep within sleep period, checking again");
827                             setActive();
828                         } else {
829                             boolean wasInactive = isInactive;
830                             isInactive = !isCharging() && !hasMovedInSleepInterval();
831
832                             if (!wasInactive && isInactive) {
833                                 lastStateTimestamp = System.currentTimeMillis();
834                                 logger.debug("Vehicle is inactive");
835                             }
836                         }
837
838                         break;
839                     }
840                 }
841
842                 // secondly, reformat the response string to a JSON compliant
843                 // object for some specific non-JSON compatible requests
844                 switch (request) {
845                     case MOBILE_ENABLED_STATE: {
846                         jsonObject = new JsonObject();
847                         jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
848                         break;
849                     }
850                     default: {
851                         jsonObject = JsonParser.parseString(result).getAsJsonObject();
852                         break;
853                     }
854                 }
855             }
856
857             // process the result
858             if (jsonObject != null && result != null && !"null".equals(result)) {
859                 // deal with responses for "set" commands, which get confirmed
860                 // positively, or negatively, in which case a reason for failure
861                 // is provided
862                 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
863                     boolean requestResult = jsonObject.get("result").getAsBoolean();
864                     logger.debug("The request ({}) execution was {}, and reported '{}'", new Object[] { request,
865                             requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString() });
866                 } else {
867                     Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
868
869                     long resultTimeStamp = 0;
870                     for (Map.Entry<String, JsonElement> entry : entrySet) {
871                         if ("timestamp".equals(entry.getKey())) {
872                             resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
873                             if (logger.isTraceEnabled()) {
874                                 Date date = new Date(resultTimeStamp);
875                                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
876                                 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
877                             }
878                             break;
879                         }
880                     }
881
882                     try {
883                         lock.lock();
884
885                         boolean proceed = true;
886                         if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
887                             proceed = false;
888                         }
889
890                         if (proceed) {
891                             for (Map.Entry<String, JsonElement> entry : entrySet) {
892                                 try {
893                                     TeslaChannelSelector selector = TeslaChannelSelector
894                                             .getValueSelectorFromRESTID(entry.getKey());
895                                     if (!selector.isProperty()) {
896                                         if (!entry.getValue().isJsonNull()) {
897                                             updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
898                                                     entry.getValue().getAsString(), selector, editProperties()));
899                                             if (logger.isTraceEnabled()) {
900                                                 logger.trace(
901                                                         "The variable/value pair '{}':'{}' is successfully processed",
902                                                         entry.getKey(), entry.getValue());
903                                             }
904                                         } else {
905                                             updateState(selector.getChannelID(), UnDefType.UNDEF);
906                                         }
907                                     } else {
908                                         if (!entry.getValue().isJsonNull()) {
909                                             Map<String, String> properties = editProperties();
910                                             properties.put(selector.getChannelID(), entry.getValue().getAsString());
911                                             updateProperties(properties);
912                                             if (logger.isTraceEnabled()) {
913                                                 logger.trace(
914                                                         "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
915                                                         entry.getKey(), entry.getValue(), selector.getChannelID());
916                                             }
917                                         }
918                                     }
919                                 } catch (IllegalArgumentException e) {
920                                     logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
921                                             entry.getKey(), entry.getValue());
922                                 } catch (ClassCastException | IllegalStateException e) {
923                                     logger.trace("An exception occurred while converting the JSON data : '{}'",
924                                             e.getMessage(), e);
925                                 }
926                             }
927                         } else {
928                             logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
929                                     request);
930                         }
931                     } finally {
932                         lock.unlock();
933                     }
934                 }
935             }
936         } catch (Exception p) {
937             logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
938         }
939     }
940
941     @SuppressWarnings("unchecked")
942     protected QuantityType<Temperature> commandToQuantityType(Command command) {
943         if (command instanceof QuantityType) {
944             return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
945         }
946         return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
947     }
948
949     protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
950         return roundBigDecimal(quantity.toBigDecimal()).floatValue();
951     }
952
953     protected BigDecimal roundBigDecimal(BigDecimal value) {
954         return value.setScale(1, RoundingMode.HALF_EVEN);
955     }
956
957     protected Runnable slowStateRunnable = () -> {
958         try {
959             queryVehicleAndUpdate();
960
961             boolean allowQuery = allowQuery();
962
963             if (allowQuery) {
964                 requestData(CHARGE_STATE);
965                 requestData(CLIMATE_STATE);
966                 requestData(GUI_STATE);
967                 queryVehicle(MOBILE_ENABLED_STATE);
968             } else {
969                 if (allowWakeUp) {
970                     wakeUp();
971                 } else {
972                     if (isAwake()) {
973                         logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
974                     }
975                 }
976             }
977         } catch (Exception e) {
978             logger.warn("Exception occurred in slowStateRunnable", e);
979         }
980     };
981
982     protected Runnable fastStateRunnable = () -> {
983         if (getThing().getStatus() == ThingStatus.ONLINE) {
984             boolean allowQuery = allowQuery();
985
986             if (allowQuery) {
987                 requestData(DRIVE_STATE);
988                 requestData(VEHICLE_STATE);
989             } else {
990                 if (allowWakeUp) {
991                     wakeUp();
992                 } else {
993                     if (isAwake()) {
994                         logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
995                     }
996                 }
997             }
998         }
999     };
1000 }