]> git.basschouten.com Git - openhab-addons.git/blob
91785802168350a26e9505380e91c2907bb68f25
[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 CHARGE_TO_MAX: {
294                             if (command instanceof OnOffType) {
295                                 if (((OnOffType) command) == OnOffType.ON) {
296                                     setMaxRangeCharging(true);
297                                 } else {
298                                     setMaxRangeCharging(false);
299                                 }
300                             }
301                             break;
302                         }
303                         case CHARGE: {
304                             if (command instanceof OnOffType) {
305                                 if (((OnOffType) command) == OnOffType.ON) {
306                                     charge(true);
307                                 } else {
308                                     charge(false);
309                                 }
310                             }
311                             break;
312                         }
313                         case FLASH: {
314                             if (command instanceof OnOffType) {
315                                 if (((OnOffType) command) == OnOffType.ON) {
316                                     flashLights();
317                                 }
318                             }
319                             break;
320                         }
321                         case HONK_HORN: {
322                             if (command instanceof OnOffType) {
323                                 if (((OnOffType) command) == OnOffType.ON) {
324                                     honkHorn();
325                                 }
326                             }
327                             break;
328                         }
329                         case CHARGEPORT: {
330                             if (command instanceof OnOffType) {
331                                 if (((OnOffType) command) == OnOffType.ON) {
332                                     openChargePort();
333                                 }
334                             }
335                             break;
336                         }
337                         case DOOR_LOCK: {
338                             if (command instanceof OnOffType) {
339                                 if (((OnOffType) command) == OnOffType.ON) {
340                                     lockDoors(true);
341                                 } else {
342                                     lockDoors(false);
343                                 }
344                             }
345                             break;
346                         }
347                         case AUTO_COND: {
348                             if (command instanceof OnOffType) {
349                                 if (((OnOffType) command) == OnOffType.ON) {
350                                     autoConditioning(true);
351                                 } else {
352                                     autoConditioning(false);
353                                 }
354                             }
355                             break;
356                         }
357                         case WAKEUP: {
358                             if (command instanceof OnOffType) {
359                                 if (((OnOffType) command) == OnOffType.ON) {
360                                     wakeUp();
361                                 }
362                             }
363                             break;
364                         }
365                         case FT: {
366                             if (command instanceof OnOffType) {
367                                 if (((OnOffType) command) == OnOffType.ON) {
368                                     openFrunk();
369                                 }
370                             }
371                             break;
372                         }
373                         case RT: {
374                             if (command instanceof OnOffType) {
375                                 if (((OnOffType) command) == OnOffType.ON) {
376                                     if (vehicleState.rt == 0) {
377                                         openTrunk();
378                                     }
379                                 } else {
380                                     if (vehicleState.rt == 1) {
381                                         closeTrunk();
382                                     }
383                                 }
384                             }
385                             break;
386                         }
387                         case VALET_MODE: {
388                             if (command instanceof OnOffType) {
389                                 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
390                                 if (((OnOffType) command) == OnOffType.ON) {
391                                     setValetMode(true, valetpin);
392                                 } else {
393                                     setValetMode(false, valetpin);
394                                 }
395                             }
396                             break;
397                         }
398                         case RESET_VALET_PIN: {
399                             if (command instanceof OnOffType) {
400                                 if (((OnOffType) command) == OnOffType.ON) {
401                                     resetValetPin();
402                                 }
403                             }
404                             break;
405                         }
406                         default:
407                             break;
408                     }
409                     return;
410                 } catch (IllegalArgumentException e) {
411                     logger.warn(
412                             "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
413                             channelID, command.toString());
414                 }
415             }
416         }
417     }
418
419     public void sendCommand(String command, String payLoad, WebTarget target) {
420         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
421             Request request = account.newRequest(this, command, payLoad, target);
422             if (stateThrottler != null) {
423                 stateThrottler.submit(COMMAND_THROTTLE, request);
424             }
425         }
426     }
427
428     public void sendCommand(String command) {
429         sendCommand(command, "{}");
430     }
431
432     public void sendCommand(String command, String payLoad) {
433         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
434             Request request = account.newRequest(this, command, payLoad, account.commandTarget);
435             if (stateThrottler != null) {
436                 stateThrottler.submit(COMMAND_THROTTLE, request);
437             }
438         }
439     }
440
441     public void sendCommand(String command, WebTarget target) {
442         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
443             Request request = account.newRequest(this, command, "{}", target);
444             if (stateThrottler != null) {
445                 stateThrottler.submit(COMMAND_THROTTLE, request);
446             }
447         }
448     }
449
450     public void requestData(String command, String payLoad) {
451         if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
452             Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget);
453             if (stateThrottler != null) {
454                 stateThrottler.submit(DATA_THROTTLE, request);
455             }
456         }
457     }
458
459     @Override
460     protected void updateStatus(ThingStatus status) {
461         super.updateStatus(status);
462     }
463
464     @Override
465     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
466         super.updateStatus(status, statusDetail);
467     }
468
469     @Override
470     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
471         super.updateStatus(status, statusDetail, description);
472     }
473
474     public void requestData(String command) {
475         requestData(command, null);
476     }
477
478     public void queryVehicle(String parameter) {
479         WebTarget target = account.vehicleTarget.path(parameter);
480         sendCommand(parameter, null, target);
481     }
482
483     public void requestAllData() {
484         requestData(DRIVE_STATE);
485         requestData(VEHICLE_STATE);
486         requestData(CHARGE_STATE);
487         requestData(CLIMATE_STATE);
488         requestData(GUI_STATE);
489     }
490
491     protected boolean isAwake() {
492         return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
493     }
494
495     protected boolean isInMotion() {
496         if (driveState != null) {
497             if (driveState.speed != null && driveState.shift_state != null) {
498                 return !"Undefined".equals(driveState.speed)
499                         && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
500             }
501         }
502         return false;
503     }
504
505     protected boolean isInactive() {
506         // vehicle is inactive in case
507         // - it does not charge
508         // - it has not moved in the observation period
509         return isInactive && !isCharging() && !hasMovedInSleepInterval();
510     }
511
512     protected boolean isCharging() {
513         return chargeState != null && "Charging".equals(chargeState.charging_state);
514     }
515
516     protected boolean hasMovedInSleepInterval() {
517         return lastLocationChangeTimestamp > (System.currentTimeMillis()
518                 - (MOVE_THRESHOLD_INTERVAL_MINUTES * 60 * 1000));
519     }
520
521     protected boolean allowQuery() {
522         return (isAwake() && !isInactive());
523     }
524
525     protected void setActive() {
526         isInactive = false;
527         lastLocationChangeTimestamp = System.currentTimeMillis();
528         lastLatitude = 0;
529         lastLongitude = 0;
530     }
531
532     protected boolean checkResponse(Response response, boolean immediatelyFail) {
533         if (response != null && response.getStatus() == 200) {
534             return true;
535         } else {
536             apiIntervalErrors++;
537             if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
538                 if (immediatelyFail) {
539                     logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
540                 } else {
541                     logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
542                             TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
543                             TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
544                 }
545
546                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
547                 if (eventClient != null) {
548                     eventClient.close();
549                 }
550             } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
551                     * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
552                 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
553                 apiIntervalTimestamp = System.currentTimeMillis();
554                 apiIntervalErrors = 0;
555             }
556         }
557
558         return false;
559     }
560
561     public void setChargeLimit(int percent) {
562         JsonObject payloadObject = new JsonObject();
563         payloadObject.addProperty("percent", percent);
564         sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
565         requestData(CHARGE_STATE);
566     }
567
568     public void setSunroof(String state) {
569         if (state.equals("vent") || state.equals("close")) {
570             JsonObject payloadObject = new JsonObject();
571             payloadObject.addProperty("state", state);
572             sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
573             requestData(VEHICLE_STATE);
574         } else {
575             logger.warn("Ignoring invalid command '{}' for sunroof.", state);
576         }
577     }
578
579     /**
580      * Sets the driver and passenger temperatures.
581      *
582      * While setting different temperature values is supported by the API, in practice this does not always work
583      * reliably, possibly if the the
584      * only reliable method is to set the driver and passenger temperature to the same value
585      *
586      * @param driverTemperature in Celsius
587      * @param passenegerTemperature in Celsius
588      */
589     public void setTemperature(float driverTemperature, float passenegerTemperature) {
590         JsonObject payloadObject = new JsonObject();
591         payloadObject.addProperty("driver_temp", driverTemperature);
592         payloadObject.addProperty("passenger_temp", passenegerTemperature);
593         sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
594         requestData(CLIMATE_STATE);
595     }
596
597     public void setCombinedTemperature(float temperature) {
598         setTemperature(temperature, temperature);
599     }
600
601     public void setDriverTemperature(float temperature) {
602         setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
603     }
604
605     public void setPassengerTemperature(float temperature) {
606         setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
607     }
608
609     public void openFrunk() {
610         JsonObject payloadObject = new JsonObject();
611         payloadObject.addProperty("which_trunk", "front");
612         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
613         requestData(VEHICLE_STATE);
614     }
615
616     public void openTrunk() {
617         JsonObject payloadObject = new JsonObject();
618         payloadObject.addProperty("which_trunk", "rear");
619         sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
620         requestData(VEHICLE_STATE);
621     }
622
623     public void closeTrunk() {
624         openTrunk();
625     }
626
627     public void setValetMode(boolean b, Integer pin) {
628         JsonObject payloadObject = new JsonObject();
629         payloadObject.addProperty("on", b);
630         if (pin != null) {
631             payloadObject.addProperty("password", String.format("%04d", pin));
632         }
633         sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
634         requestData(VEHICLE_STATE);
635     }
636
637     public void resetValetPin() {
638         sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
639         requestData(VEHICLE_STATE);
640     }
641
642     public void setMaxRangeCharging(boolean b) {
643         sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
644         requestData(CHARGE_STATE);
645     }
646
647     public void charge(boolean b) {
648         sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
649         requestData(CHARGE_STATE);
650     }
651
652     public void flashLights() {
653         sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
654     }
655
656     public void honkHorn() {
657         sendCommand(COMMAND_HONK_HORN, account.commandTarget);
658     }
659
660     public void openChargePort() {
661         sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
662         requestData(CHARGE_STATE);
663     }
664
665     public void lockDoors(boolean b) {
666         sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
667         requestData(VEHICLE_STATE);
668     }
669
670     public void autoConditioning(boolean b) {
671         sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
672         requestData(CLIMATE_STATE);
673     }
674
675     public void wakeUp() {
676         sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
677     }
678
679     protected Vehicle queryVehicle() {
680         String authHeader = account.getAuthHeader();
681
682         if (authHeader != null) {
683             try {
684                 // get a list of vehicles
685                 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
686                         .header("Authorization", authHeader).get();
687
688                 logger.debug("Querying the vehicle : Response : {}:{}", response.getStatus(), response.getStatusInfo());
689
690                 if (!checkResponse(response, true)) {
691                     logger.error("An error occurred while querying the vehicle");
692                     return null;
693                 }
694
695                 JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
696                 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
697
698                 for (Vehicle vehicle : vehicleArray) {
699                     logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
700                     if (vehicle.vin.equals(getConfig().get(VIN))) {
701                         vehicleJSON = gson.toJson(vehicle);
702                         parseAndUpdate("queryVehicle", null, vehicleJSON);
703                         if (logger.isTraceEnabled()) {
704                             logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
705                                     vehicle.tokens);
706                         }
707                         return vehicle;
708                     }
709                 }
710             } catch (ProcessingException e) {
711                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
712             }
713         }
714         return null;
715     }
716
717     protected void queryVehicleAndUpdate() {
718         vehicle = queryVehicle();
719         if (vehicle != null) {
720             parseAndUpdate("queryVehicle", null, vehicleJSON);
721         }
722     }
723
724     public void parseAndUpdate(String request, String payLoad, String result) {
725         final Double LOCATION_THRESHOLD = .0000001;
726
727         JsonObject jsonObject = null;
728
729         try {
730             if (request != null && result != null && !"null".equals(result)) {
731                 updateStatus(ThingStatus.ONLINE);
732                 // first, update state objects
733                 switch (request) {
734                     case DRIVE_STATE: {
735                         driveState = gson.fromJson(result, DriveState.class);
736
737                         if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
738                                 || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
739                             logger.debug("Vehicle moved, resetting last location timestamp");
740
741                             lastLatitude = driveState.latitude;
742                             lastLongitude = driveState.longitude;
743                             lastLocationChangeTimestamp = System.currentTimeMillis();
744                         }
745
746                         break;
747                     }
748                     case GUI_STATE: {
749                         guiState = gson.fromJson(result, GUIState.class);
750                         break;
751                     }
752                     case VEHICLE_STATE: {
753                         vehicleState = gson.fromJson(result, VehicleState.class);
754                         break;
755                     }
756                     case CHARGE_STATE: {
757                         chargeState = gson.fromJson(result, ChargeState.class);
758                         if (isCharging()) {
759                             updateState(CHANNEL_CHARGE, OnOffType.ON);
760                         } else {
761                             updateState(CHANNEL_CHARGE, OnOffType.OFF);
762                         }
763
764                         break;
765                     }
766                     case CLIMATE_STATE: {
767                         climateState = gson.fromJson(result, ClimateState.class);
768                         BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
769                                 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
770                         updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
771                         break;
772                     }
773                     case "queryVehicle": {
774                         if (vehicle != null && !lastState.equals(vehicle.state)) {
775                             lastState = vehicle.state;
776
777                             // in case vehicle changed to awake, refresh all data
778                             if (isAwake()) {
779                                 logger.debug("Vehicle is now awake, updating all data");
780                                 lastLocationChangeTimestamp = System.currentTimeMillis();
781                                 requestAllData();
782                             }
783
784                             setActive();
785                         }
786
787                         // reset timestamp if elapsed and set inactive to false
788                         if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
789                                 .currentTimeMillis()) {
790                             logger.debug("Vehicle did not fall asleep within sleep period, checking again");
791                             setActive();
792                         } else {
793                             boolean wasInactive = isInactive;
794                             isInactive = !isCharging() && !hasMovedInSleepInterval();
795
796                             if (!wasInactive && isInactive) {
797                                 lastStateTimestamp = System.currentTimeMillis();
798                                 logger.debug("Vehicle is inactive");
799                             }
800                         }
801
802                         break;
803                     }
804                 }
805
806                 // secondly, reformat the response string to a JSON compliant
807                 // object for some specific non-JSON compatible requests
808                 switch (request) {
809                     case MOBILE_ENABLED_STATE: {
810                         jsonObject = new JsonObject();
811                         jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
812                         break;
813                     }
814                     default: {
815                         jsonObject = JsonParser.parseString(result).getAsJsonObject();
816                         break;
817                     }
818                 }
819             }
820
821             // process the result
822             if (jsonObject != null && result != null && !"null".equals(result)) {
823                 // deal with responses for "set" commands, which get confirmed
824                 // positively, or negatively, in which case a reason for failure
825                 // is provided
826                 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
827                     boolean requestResult = jsonObject.get("result").getAsBoolean();
828                     logger.debug("The request ({}) execution was {}, and reported '{}'", new Object[] { request,
829                             requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString() });
830                 } else {
831                     Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
832
833                     long resultTimeStamp = 0;
834                     for (Map.Entry<String, JsonElement> entry : entrySet) {
835                         if ("timestamp".equals(entry.getKey())) {
836                             resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
837                             if (logger.isTraceEnabled()) {
838                                 Date date = new Date(resultTimeStamp);
839                                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
840                                 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
841                             }
842                             break;
843                         }
844                     }
845
846                     try {
847                         lock.lock();
848
849                         boolean proceed = true;
850                         if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
851                             proceed = false;
852                         }
853
854                         if (proceed) {
855                             for (Map.Entry<String, JsonElement> entry : entrySet) {
856                                 try {
857                                     TeslaChannelSelector selector = TeslaChannelSelector
858                                             .getValueSelectorFromRESTID(entry.getKey());
859                                     if (!selector.isProperty()) {
860                                         if (!entry.getValue().isJsonNull()) {
861                                             updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
862                                                     entry.getValue().getAsString(), selector, editProperties()));
863                                             if (logger.isTraceEnabled()) {
864                                                 logger.trace(
865                                                         "The variable/value pair '{}':'{}' is successfully processed",
866                                                         entry.getKey(), entry.getValue());
867                                             }
868                                         } else {
869                                             updateState(selector.getChannelID(), UnDefType.UNDEF);
870                                         }
871                                     } else {
872                                         if (!entry.getValue().isJsonNull()) {
873                                             Map<String, String> properties = editProperties();
874                                             properties.put(selector.getChannelID(), entry.getValue().getAsString());
875                                             updateProperties(properties);
876                                             if (logger.isTraceEnabled()) {
877                                                 logger.trace(
878                                                         "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
879                                                         entry.getKey(), entry.getValue(), selector.getChannelID());
880                                             }
881                                         }
882                                     }
883                                 } catch (IllegalArgumentException e) {
884                                     logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
885                                             entry.getKey(), entry.getValue());
886                                 } catch (ClassCastException | IllegalStateException e) {
887                                     logger.trace("An exception occurred while converting the JSON data : '{}'",
888                                             e.getMessage(), e);
889                                 }
890                             }
891                         } else {
892                             logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
893                                     request);
894                         }
895                     } finally {
896                         lock.unlock();
897                     }
898                 }
899             }
900         } catch (Exception p) {
901             logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
902         }
903     }
904
905     @SuppressWarnings("unchecked")
906     protected QuantityType<Temperature> commandToQuantityType(Command command) {
907         if (command instanceof QuantityType) {
908             return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
909         }
910         return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
911     }
912
913     protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
914         return roundBigDecimal(quantity.toBigDecimal()).floatValue();
915     }
916
917     protected BigDecimal roundBigDecimal(BigDecimal value) {
918         return value.setScale(1, RoundingMode.HALF_EVEN);
919     }
920
921     protected Runnable slowStateRunnable = () -> {
922         queryVehicleAndUpdate();
923
924         boolean allowQuery = allowQuery();
925
926         if (allowQuery) {
927             requestData(CHARGE_STATE);
928             requestData(CLIMATE_STATE);
929             requestData(GUI_STATE);
930             queryVehicle(MOBILE_ENABLED_STATE);
931         } else {
932             if (allowWakeUp) {
933                 wakeUp();
934             } else {
935                 if (isAwake()) {
936                     logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
937                 }
938             }
939         }
940     };
941
942     protected Runnable fastStateRunnable = () -> {
943         if (getThing().getStatus() == ThingStatus.ONLINE) {
944             boolean allowQuery = allowQuery();
945
946             if (allowQuery) {
947                 requestData(DRIVE_STATE);
948                 requestData(VEHICLE_STATE);
949             } else {
950                 if (allowWakeUp) {
951                     wakeUp();
952                 } else {
953                     if (isAwake()) {
954                         logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
955                     }
956                 }
957             }
958         }
959     };
960
961     protected Runnable eventRunnable = new Runnable() {
962         Response eventResponse;
963         BufferedReader eventBufferedReader;
964         InputStreamReader eventInputStreamReader;
965         boolean isEstablished = false;
966
967         protected boolean establishEventStream() {
968             try {
969                 if (!isEstablished) {
970                     eventBufferedReader = null;
971
972                     eventClient = clientBuilder.build()
973                             .register(new Authenticator((String) getConfig().get(CONFIG_USERNAME), vehicle.tokens[0]));
974                     eventTarget = eventClient.target(URI_EVENT).path(vehicle.vehicle_id + "/").queryParam("values",
975                             Arrays.asList(EventKeys.values()).stream().skip(1).map(Enum::toString)
976                                     .collect(Collectors.joining(",")));
977                     eventResponse = eventTarget.request(MediaType.TEXT_PLAIN_TYPE).get();
978
979                     logger.debug("Event Stream: Establishing the event stream: Response: {}:{}",
980                             eventResponse.getStatus(), eventResponse.getStatusInfo());
981
982                     if (eventResponse.getStatus() == 200) {
983                         InputStream dummy = (InputStream) eventResponse.getEntity();
984                         eventInputStreamReader = new InputStreamReader(dummy);
985                         eventBufferedReader = new BufferedReader(eventInputStreamReader);
986                         isEstablished = true;
987                     } else if (eventResponse.getStatus() == 401) {
988                         isEstablished = false;
989                     } else {
990                         isEstablished = false;
991                     }
992
993                     if (!isEstablished) {
994                         eventIntervalErrors++;
995                         if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
996                             logger.warn(
997                                     "Reached the maximum number of errors ({}) for the current interval ({} seconds)",
998                                     EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
999                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1000                             eventClient.close();
1001                         }
1002
1003                         if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
1004                                 * EVENT_ERROR_INTERVAL_SECONDS) {
1005                             logger.trace("Resetting the error counter. ({} errors in the last interval)",
1006                                     eventIntervalErrors);
1007                             eventIntervalTimestamp = System.currentTimeMillis();
1008                             eventIntervalErrors = 0;
1009                         }
1010                     }
1011                 }
1012             } catch (Exception e) {
1013                 logger.error(
1014                         "Event stream: An exception occurred while establishing the event stream for the vehicle: '{}'",
1015                         e.getMessage());
1016                 isEstablished = false;
1017             }
1018
1019             return isEstablished;
1020         }
1021
1022         @Override
1023         public void run() {
1024             while (true) {
1025                 try {
1026                     if (getThing().getStatus() == ThingStatus.ONLINE) {
1027                         if (isAwake()) {
1028                             if (establishEventStream()) {
1029                                 String line = eventBufferedReader.readLine();
1030
1031                                 while (line != null) {
1032                                     logger.debug("Event stream: Received an event: '{}'", line);
1033                                     String vals[] = line.split(",");
1034                                     long currentTimeStamp = Long.valueOf(vals[0]);
1035                                     long systemTimeStamp = System.currentTimeMillis();
1036                                     if (logger.isDebugEnabled()) {
1037                                         SimpleDateFormat dateFormatter = new SimpleDateFormat(
1038                                                 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1039                                         logger.debug("STS {} CTS {} Delta {}",
1040                                                 dateFormatter.format(new Date(systemTimeStamp)),
1041                                                 dateFormatter.format(new Date(currentTimeStamp)),
1042                                                 systemTimeStamp - currentTimeStamp);
1043                                     }
1044                                     if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
1045                                         if (currentTimeStamp > lastTimeStamp) {
1046                                             lastTimeStamp = Long.valueOf(vals[0]);
1047                                             if (logger.isDebugEnabled()) {
1048                                                 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1049                                                         "yyyy-MM-dd'T'HH:mm:ss.SSS");
1050                                                 logger.debug("Event Stream: Event stamp is {}",
1051                                                         dateFormatter.format(new Date(lastTimeStamp)));
1052                                             }
1053                                             for (int i = 0; i < EventKeys.values().length; i++) {
1054                                                 TeslaChannelSelector selector = TeslaChannelSelector
1055                                                         .getValueSelectorFromRESTID((EventKeys.values()[i]).toString());
1056                                                 if (!selector.isProperty()) {
1057                                                     State newState = teslaChannelSelectorProxy.getState(vals[i],
1058                                                             selector, editProperties());
1059                                                     if (newState != null && !"".equals(vals[i])) {
1060                                                         updateState(selector.getChannelID(), newState);
1061                                                     } else {
1062                                                         updateState(selector.getChannelID(), UnDefType.UNDEF);
1063                                                     }
1064                                                 } else {
1065                                                     Map<String, String> properties = editProperties();
1066                                                     properties.put(selector.getChannelID(),
1067                                                             (selector.getState(vals[i])).toString());
1068                                                     updateProperties(properties);
1069                                                 }
1070                                             }
1071                                         } else {
1072                                             if (logger.isDebugEnabled()) {
1073                                                 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1074                                                         "yyyy-MM-dd'T'HH:mm:ss.SSS");
1075                                                 logger.debug(
1076                                                         "Event stream: Discarding an event with an out of sync timestamp {} (last is {})",
1077                                                         dateFormatter.format(new Date(currentTimeStamp)),
1078                                                         dateFormatter.format(new Date(lastTimeStamp)));
1079                                             }
1080                                         }
1081                                     } else {
1082                                         if (logger.isDebugEnabled()) {
1083                                             SimpleDateFormat dateFormatter = new SimpleDateFormat(
1084                                                     "yyyy-MM-dd'T'HH:mm:ss.SSS");
1085                                             logger.debug(
1086                                                     "Event Stream: Discarding an event that differs {} ms from the system time: {} (system is {})",
1087                                                     systemTimeStamp - currentTimeStamp,
1088                                                     dateFormatter.format(currentTimeStamp),
1089                                                     dateFormatter.format(systemTimeStamp));
1090                                         }
1091                                         if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
1092                                             logger.trace("Event stream: The event stream will be reset");
1093                                             isEstablished = false;
1094                                         }
1095                                     }
1096                                     line = eventBufferedReader.readLine();
1097                                 }
1098                                 logger.trace("Event stream: The end of stream was reached");
1099                                 isEstablished = false;
1100                             }
1101                         } else {
1102                             logger.debug("Event stream: The vehicle is not awake");
1103                             if (vehicle != null) {
1104                                 if (allowWakeUp) {
1105                                     // wake up the vehicle until streaming token <> 0
1106                                     logger.debug("Event stream: Waking up the vehicle");
1107                                     wakeUp();
1108                                 }
1109                             } else {
1110                                 vehicle = queryVehicle();
1111                             }
1112                             Thread.sleep(EVENT_STREAM_PAUSE);
1113                         }
1114                     }
1115                 } catch (IOException | NumberFormatException e) {
1116                     logger.debug("Event stream: An exception occurred while reading events: '{}'", e.getMessage());
1117                     isEstablished = false;
1118                 } catch (InterruptedException e) {
1119                     isEstablished = false;
1120                 }
1121
1122                 if (Thread.interrupted()) {
1123                     logger.debug("Event stream: the event stream was interrupted");
1124                     return;
1125                 }
1126             }
1127         }
1128     };
1129 }