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