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