2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.tesla.internal.handler;
15 import static org.openhab.binding.tesla.internal.TeslaBindingConstants.*;
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;
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;
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;
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;
75 import com.google.gson.Gson;
76 import com.google.gson.JsonElement;
77 import com.google.gson.JsonObject;
78 import com.google.gson.JsonParser;
81 * The {@link TeslaVehicleHandler} is responsible for handling commands, which are sent
82 * to one of the channels of a specific vehicle.
84 * @author Karel Goderis - Initial contribution
85 * @author Kai Kreuzer - Refactored to use separate account handler and improved configuration options
87 public class TeslaVehicleHandler extends BaseThingHandler {
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;
99 private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
101 protected WebTarget eventTarget;
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;
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;
121 protected double lastLongitude;
122 protected double lastLatitude;
123 protected long lastLocationChangeTimestamp;
125 protected long lastStateTimestamp = System.currentTimeMillis();
126 protected String lastState = "";
127 protected boolean isInactive = false;
129 protected TeslaAccountHandler account;
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;
139 private final Gson gson = new Gson();
140 private final JsonParser parser = new JsonParser();
142 public TeslaVehicleHandler(Thing thing, ClientBuilder clientBuilder) {
144 this.clientBuilder = clientBuilder;
147 @SuppressWarnings("null")
149 public void initialize() {
150 logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
151 updateStatus(ThingStatus.UNKNOWN);
152 allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
154 // the streaming API seems to be broken - let's keep the code, if it comes back one day
155 // enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
157 account = (TeslaAccountHandler) getBridge().getHandler();
158 lock = new ReentrantLock();
159 scheduler.execute(() -> queryVehicleAndUpdate());
163 Map<Object, Rate> channels = new HashMap<>();
164 channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
165 channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
167 Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
168 Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
169 stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
170 stateThrottler.addRate(secondRate);
172 if (fastStateJob == null || fastStateJob.isCancelled()) {
173 fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
174 TimeUnit.MILLISECONDS);
177 if (slowStateJob == null || slowStateJob.isCancelled()) {
178 slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
179 TimeUnit.MILLISECONDS);
186 if (eventThread == null) {
187 eventThread = new Thread(eventRunnable, "openHAB-Tesla-Events-" + getThing().getUID());
194 public void dispose() {
195 logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
198 if (fastStateJob != null && !fastStateJob.isCancelled()) {
199 fastStateJob.cancel(true);
203 if (slowStateJob != null && !slowStateJob.isCancelled()) {
204 slowStateJob.cancel(true);
208 if (eventThread != null && !eventThread.isInterrupted()) {
209 eventThread.interrupt();
216 if (eventClient != null) {
222 * Retrieves the unique vehicle id this handler is associated with
224 * @return the vehicle id
226 public String getVehicleId() {
231 public void handleCommand(ChannelUID channelUID, Command command) {
232 logger.debug("handleCommand {} {}", channelUID, command);
233 String channelID = channelUID.getId();
234 TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
236 if (command instanceof RefreshType) {
238 logger.debug("Waking vehicle to refresh all data");
244 // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
245 // throttled so we are safe not to break the Tesla SLA
248 if (selector != null) {
251 case CHARGE_LIMIT_SOC: {
252 if (command instanceof PercentType) {
253 setChargeLimit(((PercentType) command).intValue());
254 } else if (command instanceof OnOffType && command == OnOffType.ON) {
256 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
258 } else if (command instanceof IncreaseDecreaseType
259 && command == IncreaseDecreaseType.INCREASE) {
260 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
261 } else if (command instanceof IncreaseDecreaseType
262 && command == IncreaseDecreaseType.DECREASE) {
263 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
267 case COMBINED_TEMP: {
268 QuantityType<Temperature> quantity = commandToQuantityType(command);
269 if (quantity != null) {
270 setCombinedTemperature(quanityToRoundedFloat(quantity));
275 QuantityType<Temperature> quantity = commandToQuantityType(command);
276 if (quantity != null) {
277 setDriverTemperature(quanityToRoundedFloat(quantity));
281 case PASSENGER_TEMP: {
282 QuantityType<Temperature> quantity = commandToQuantityType(command);
283 if (quantity != null) {
284 setPassengerTemperature(quanityToRoundedFloat(quantity));
288 case SUN_ROOF_STATE: {
289 if (command instanceof StringType) {
290 setSunroof(command.toString());
295 if (command instanceof PercentType) {
296 moveSunroof(((PercentType) command).intValue());
297 } else if (command instanceof OnOffType && command == OnOffType.ON) {
299 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
301 } else if (command instanceof IncreaseDecreaseType
302 && command == IncreaseDecreaseType.INCREASE) {
303 moveSunroof(Math.min(vehicleState.sun_roof_percent_open + 1, 100));
304 } else if (command instanceof IncreaseDecreaseType
305 && command == IncreaseDecreaseType.DECREASE) {
306 moveSunroof(Math.max(vehicleState.sun_roof_percent_open - 1, 0));
310 case CHARGE_TO_MAX: {
311 if (command instanceof OnOffType) {
312 if (((OnOffType) command) == OnOffType.ON) {
313 setMaxRangeCharging(true);
315 setMaxRangeCharging(false);
321 if (command instanceof OnOffType) {
322 if (((OnOffType) command) == OnOffType.ON) {
331 if (command instanceof OnOffType) {
332 if (((OnOffType) command) == OnOffType.ON) {
339 if (command instanceof OnOffType) {
340 if (((OnOffType) command) == OnOffType.ON) {
347 if (command instanceof OnOffType) {
348 if (((OnOffType) command) == OnOffType.ON) {
355 if (command instanceof OnOffType) {
356 if (((OnOffType) command) == OnOffType.ON) {
365 if (command instanceof OnOffType) {
366 if (((OnOffType) command) == OnOffType.ON) {
367 autoConditioning(true);
369 autoConditioning(false);
375 if (command instanceof OnOffType) {
376 if (((OnOffType) command) == OnOffType.ON) {
383 if (command instanceof OnOffType) {
384 if (((OnOffType) command) == OnOffType.ON) {
391 if (command instanceof OnOffType) {
392 if (((OnOffType) command) == OnOffType.ON) {
393 if (vehicleState.rt == 0) {
397 if (vehicleState.rt == 1) {
405 if (command instanceof OnOffType) {
406 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
407 if (((OnOffType) command) == OnOffType.ON) {
408 setValetMode(true, valetpin);
410 setValetMode(false, valetpin);
415 case RESET_VALET_PIN: {
416 if (command instanceof OnOffType) {
417 if (((OnOffType) command) == OnOffType.ON) {
427 } catch (IllegalArgumentException e) {
429 "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
430 channelID, command.toString());
436 public void sendCommand(String command, String payLoad, WebTarget target) {
437 if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
438 Request request = account.newRequest(this, command, payLoad, target);
439 if (stateThrottler != null) {
440 stateThrottler.submit(COMMAND_THROTTLE, request);
445 public void sendCommand(String command) {
446 sendCommand(command, "{}");
449 public void sendCommand(String command, String payLoad) {
450 if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
451 Request request = account.newRequest(this, command, payLoad, account.commandTarget);
452 if (stateThrottler != null) {
453 stateThrottler.submit(COMMAND_THROTTLE, request);
458 public void sendCommand(String command, WebTarget target) {
459 if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
460 Request request = account.newRequest(this, command, "{}", target);
461 if (stateThrottler != null) {
462 stateThrottler.submit(COMMAND_THROTTLE, request);
467 public void requestData(String command, String payLoad) {
468 if (command.equals(COMMAND_WAKE_UP) || isAwake()) {
469 Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget);
470 if (stateThrottler != null) {
471 stateThrottler.submit(DATA_THROTTLE, request);
477 protected void updateStatus(ThingStatus status) {
478 super.updateStatus(status);
482 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
483 super.updateStatus(status, statusDetail);
487 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
488 super.updateStatus(status, statusDetail, description);
491 public void requestData(String command) {
492 requestData(command, null);
495 public void queryVehicle(String parameter) {
496 WebTarget target = account.vehicleTarget.path(parameter);
497 sendCommand(parameter, null, target);
500 public void requestAllData() {
501 requestData(DRIVE_STATE);
502 requestData(VEHICLE_STATE);
503 requestData(CHARGE_STATE);
504 requestData(CLIMATE_STATE);
505 requestData(GUI_STATE);
508 protected boolean isAwake() {
509 return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
512 protected boolean isInMotion() {
513 if (driveState != null) {
514 if (driveState.speed != null && driveState.shift_state != null) {
515 return !"Undefined".equals(driveState.speed)
516 && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
522 protected boolean isInactive() {
523 // vehicle is inactive in case
524 // - it does not charge
525 // - it has not moved in the observation period
526 return isInactive && !isCharging() && !hasMovedInSleepInterval();
529 protected boolean isCharging() {
530 return chargeState != null && "Charging".equals(chargeState.charging_state);
533 protected boolean hasMovedInSleepInterval() {
534 return lastLocationChangeTimestamp > (System.currentTimeMillis()
535 - (MOVE_THRESHOLD_INTERVAL_MINUTES * 60 * 1000));
538 protected boolean allowQuery() {
539 return (isAwake() && !isInactive());
542 protected void setActive() {
544 lastLocationChangeTimestamp = System.currentTimeMillis();
549 protected boolean checkResponse(Response response, boolean immediatelyFail) {
550 if (response != null && response.getStatus() == 200) {
554 if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
555 if (immediatelyFail) {
556 logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
558 logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
559 TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
560 TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
563 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
565 } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
566 * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
567 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
568 apiIntervalTimestamp = System.currentTimeMillis();
569 apiIntervalErrors = 0;
576 public void setChargeLimit(int percent) {
577 JsonObject payloadObject = new JsonObject();
578 payloadObject.addProperty("percent", percent);
579 sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
580 requestData(CHARGE_STATE);
583 public void setSunroof(String state) {
584 JsonObject payloadObject = new JsonObject();
585 payloadObject.addProperty("state", state);
586 sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
587 requestData(VEHICLE_STATE);
590 public void moveSunroof(int percent) {
591 JsonObject payloadObject = new JsonObject();
592 payloadObject.addProperty("state", "move");
593 payloadObject.addProperty("percent", percent);
594 sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
595 requestData(VEHICLE_STATE);
599 * Sets the driver and passenger temperatures.
601 * While setting different temperature values is supported by the API, in practice this does not always work
602 * reliably, possibly if the the
603 * only reliable method is to set the driver and passenger temperature to the same value
605 * @param driverTemperature in Celsius
606 * @param passenegerTemperature in Celsius
608 public void setTemperature(float driverTemperature, float passenegerTemperature) {
609 JsonObject payloadObject = new JsonObject();
610 payloadObject.addProperty("driver_temp", driverTemperature);
611 payloadObject.addProperty("passenger_temp", passenegerTemperature);
612 sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
613 requestData(CLIMATE_STATE);
616 public void setCombinedTemperature(float temperature) {
617 setTemperature(temperature, temperature);
620 public void setDriverTemperature(float temperature) {
621 setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
624 public void setPassengerTemperature(float temperature) {
625 setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
628 public void openFrunk() {
629 JsonObject payloadObject = new JsonObject();
630 payloadObject.addProperty("which_trunk", "front");
631 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
632 requestData(VEHICLE_STATE);
635 public void openTrunk() {
636 JsonObject payloadObject = new JsonObject();
637 payloadObject.addProperty("which_trunk", "rear");
638 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
639 requestData(VEHICLE_STATE);
642 public void closeTrunk() {
646 public void setValetMode(boolean b, Integer pin) {
647 JsonObject payloadObject = new JsonObject();
648 payloadObject.addProperty("on", b);
650 payloadObject.addProperty("password", String.format("%04d", pin));
652 sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
653 requestData(VEHICLE_STATE);
656 public void resetValetPin() {
657 sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
658 requestData(VEHICLE_STATE);
661 public void setMaxRangeCharging(boolean b) {
662 sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
663 requestData(CHARGE_STATE);
666 public void charge(boolean b) {
667 sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
668 requestData(CHARGE_STATE);
671 public void flashLights() {
672 sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
675 public void honkHorn() {
676 sendCommand(COMMAND_HONK_HORN, account.commandTarget);
679 public void openChargePort() {
680 sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
681 requestData(CHARGE_STATE);
684 public void lockDoors(boolean b) {
685 sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
686 requestData(VEHICLE_STATE);
689 public void autoConditioning(boolean b) {
690 sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
691 requestData(CLIMATE_STATE);
694 public void wakeUp() {
695 sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
698 protected Vehicle queryVehicle() {
699 String authHeader = account.getAuthHeader();
701 if (authHeader != null) {
703 // get a list of vehicles
704 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
705 .header("Authorization", authHeader).get();
707 logger.debug("Querying the vehicle : Response : {}:{}", response.getStatus(), response.getStatusInfo());
709 if (!checkResponse(response, true)) {
710 logger.error("An error occurred while querying the vehicle");
714 JsonObject jsonObject = parser.parse(response.readEntity(String.class)).getAsJsonObject();
715 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
717 for (Vehicle vehicle : vehicleArray) {
718 logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
719 if (vehicle.vin.equals(getConfig().get(VIN))) {
720 vehicleJSON = gson.toJson(vehicle);
721 parseAndUpdate("queryVehicle", null, vehicleJSON);
722 if (logger.isTraceEnabled()) {
723 logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
729 } catch (ProcessingException e) {
730 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
736 protected void queryVehicleAndUpdate() {
737 vehicle = queryVehicle();
738 if (vehicle != null) {
739 parseAndUpdate("queryVehicle", null, vehicleJSON);
743 public void parseAndUpdate(String request, String payLoad, String result) {
744 final Double LOCATION_THRESHOLD = .0000001;
746 JsonObject jsonObject = null;
749 if (request != null && result != null && !"null".equals(result)) {
750 updateStatus(ThingStatus.ONLINE);
751 // first, update state objects
754 driveState = gson.fromJson(result, DriveState.class);
756 if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
757 || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
758 logger.debug("Vehicle moved, resetting last location timestamp");
760 lastLatitude = driveState.latitude;
761 lastLongitude = driveState.longitude;
762 lastLocationChangeTimestamp = System.currentTimeMillis();
768 guiState = gson.fromJson(result, GUIState.class);
771 case VEHICLE_STATE: {
772 vehicleState = gson.fromJson(result, VehicleState.class);
776 chargeState = gson.fromJson(result, ChargeState.class);
778 updateState(CHANNEL_CHARGE, OnOffType.ON);
780 updateState(CHANNEL_CHARGE, OnOffType.OFF);
785 case CLIMATE_STATE: {
786 climateState = gson.fromJson(result, ClimateState.class);
787 BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
788 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
789 updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
792 case "queryVehicle": {
793 if (vehicle != null && !lastState.equals(vehicle.state)) {
794 lastState = vehicle.state;
796 // in case vehicle changed to awake, refresh all data
798 logger.debug("Vehicle is now awake, updating all data");
799 lastLocationChangeTimestamp = System.currentTimeMillis();
806 // reset timestamp if elapsed and set inactive to false
807 if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
808 .currentTimeMillis()) {
809 logger.debug("Vehicle did not fall asleep within sleep period, checking again");
812 boolean wasInactive = isInactive;
813 isInactive = !isCharging() && !hasMovedInSleepInterval();
815 if (!wasInactive && isInactive) {
816 lastStateTimestamp = System.currentTimeMillis();
817 logger.debug("Vehicle is inactive");
825 // secondly, reformat the response string to a JSON compliant
826 // object for some specific non-JSON compatible requests
828 case MOBILE_ENABLED_STATE: {
829 jsonObject = new JsonObject();
830 jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
834 jsonObject = parser.parse(result).getAsJsonObject();
840 // process the result
841 if (jsonObject != null && result != null && !"null".equals(result)) {
842 // deal with responses for "set" commands, which get confirmed
843 // positively, or negatively, in which case a reason for failure
845 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
846 boolean requestResult = jsonObject.get("result").getAsBoolean();
847 logger.debug("The request ({}) execution was {}, and reported '{}'", new Object[] { request,
848 requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString() });
850 Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
852 long resultTimeStamp = 0;
853 for (Map.Entry<String, JsonElement> entry : entrySet) {
854 if ("timestamp".equals(entry.getKey())) {
855 resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
856 if (logger.isTraceEnabled()) {
857 Date date = new Date(resultTimeStamp);
858 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
859 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
868 boolean proceed = true;
869 if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
874 for (Map.Entry<String, JsonElement> entry : entrySet) {
876 TeslaChannelSelector selector = TeslaChannelSelector
877 .getValueSelectorFromRESTID(entry.getKey());
878 if (!selector.isProperty()) {
879 if (!entry.getValue().isJsonNull()) {
880 updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
881 entry.getValue().getAsString(), selector, editProperties()));
882 if (logger.isTraceEnabled()) {
884 "The variable/value pair '{}':'{}' is successfully processed",
885 entry.getKey(), entry.getValue());
888 updateState(selector.getChannelID(), UnDefType.UNDEF);
891 if (!entry.getValue().isJsonNull()) {
892 Map<String, String> properties = editProperties();
893 properties.put(selector.getChannelID(), entry.getValue().getAsString());
894 updateProperties(properties);
895 if (logger.isTraceEnabled()) {
897 "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
898 entry.getKey(), entry.getValue(), selector.getChannelID());
902 } catch (IllegalArgumentException e) {
903 logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
904 entry.getKey(), entry.getValue());
905 } catch (ClassCastException | IllegalStateException e) {
906 logger.trace("An exception occurred while converting the JSON data : '{}'",
911 logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
919 } catch (Exception p) {
920 logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
924 @SuppressWarnings("unchecked")
925 protected QuantityType<Temperature> commandToQuantityType(Command command) {
926 if (command instanceof QuantityType) {
927 return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
929 return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
932 protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
933 return roundBigDecimal(quantity.toBigDecimal()).floatValue();
936 protected BigDecimal roundBigDecimal(BigDecimal value) {
937 return value.setScale(1, RoundingMode.HALF_EVEN);
940 protected Runnable slowStateRunnable = () -> {
941 queryVehicleAndUpdate();
943 boolean allowQuery = allowQuery();
946 requestData(CHARGE_STATE);
947 requestData(CLIMATE_STATE);
948 requestData(GUI_STATE);
949 queryVehicle(MOBILE_ENABLED_STATE);
955 logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
961 protected Runnable fastStateRunnable = () -> {
962 if (getThing().getStatus() == ThingStatus.ONLINE) {
963 boolean allowQuery = allowQuery();
966 requestData(DRIVE_STATE);
967 requestData(VEHICLE_STATE);
973 logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
980 protected Runnable eventRunnable = new Runnable() {
981 Response eventResponse;
982 BufferedReader eventBufferedReader;
983 InputStreamReader eventInputStreamReader;
984 boolean isEstablished = false;
986 protected boolean establishEventStream() {
988 if (!isEstablished) {
989 eventBufferedReader = null;
991 eventClient = clientBuilder.build()
992 .register(new Authenticator((String) getConfig().get(CONFIG_USERNAME), vehicle.tokens[0]));
993 eventTarget = eventClient.target(URI_EVENT).path(vehicle.vehicle_id + "/").queryParam("values",
994 Arrays.asList(EventKeys.values()).stream().skip(1).map(Enum::toString)
995 .collect(Collectors.joining(",")));
996 eventResponse = eventTarget.request(MediaType.TEXT_PLAIN_TYPE).get();
998 logger.debug("Event Stream: Establishing the event stream: Response: {}:{}",
999 eventResponse.getStatus(), eventResponse.getStatusInfo());
1001 if (eventResponse.getStatus() == 200) {
1002 InputStream dummy = (InputStream) eventResponse.getEntity();
1003 eventInputStreamReader = new InputStreamReader(dummy);
1004 eventBufferedReader = new BufferedReader(eventInputStreamReader);
1005 isEstablished = true;
1006 } else if (eventResponse.getStatus() == 401) {
1007 isEstablished = false;
1009 isEstablished = false;
1012 if (!isEstablished) {
1013 eventIntervalErrors++;
1014 if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
1016 "Reached the maximum number of errors ({}) for the current interval ({} seconds)",
1017 EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
1018 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1019 eventClient.close();
1022 if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
1023 * EVENT_ERROR_INTERVAL_SECONDS) {
1024 logger.trace("Resetting the error counter. ({} errors in the last interval)",
1025 eventIntervalErrors);
1026 eventIntervalTimestamp = System.currentTimeMillis();
1027 eventIntervalErrors = 0;
1031 } catch (Exception e) {
1033 "Event stream: An exception occurred while establishing the event stream for the vehicle: '{}'",
1035 isEstablished = false;
1038 return isEstablished;
1045 if (getThing().getStatus() == ThingStatus.ONLINE) {
1047 if (establishEventStream()) {
1048 String line = eventBufferedReader.readLine();
1050 while (line != null) {
1051 logger.debug("Event stream: Received an event: '{}'", line);
1052 String vals[] = line.split(",");
1053 long currentTimeStamp = Long.valueOf(vals[0]);
1054 long systemTimeStamp = System.currentTimeMillis();
1055 if (logger.isDebugEnabled()) {
1056 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1057 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1058 logger.debug("STS {} CTS {} Delta {}",
1059 dateFormatter.format(new Date(systemTimeStamp)),
1060 dateFormatter.format(new Date(currentTimeStamp)),
1061 systemTimeStamp - currentTimeStamp);
1063 if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
1064 if (currentTimeStamp > lastTimeStamp) {
1065 lastTimeStamp = Long.valueOf(vals[0]);
1066 if (logger.isDebugEnabled()) {
1067 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1068 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1069 logger.debug("Event Stream: Event stamp is {}",
1070 dateFormatter.format(new Date(lastTimeStamp)));
1072 for (int i = 0; i < EventKeys.values().length; i++) {
1073 TeslaChannelSelector selector = TeslaChannelSelector
1074 .getValueSelectorFromRESTID((EventKeys.values()[i]).toString());
1075 if (!selector.isProperty()) {
1076 State newState = teslaChannelSelectorProxy.getState(vals[i],
1077 selector, editProperties());
1078 if (newState != null && !"".equals(vals[i])) {
1079 updateState(selector.getChannelID(), newState);
1081 updateState(selector.getChannelID(), UnDefType.UNDEF);
1084 Map<String, String> properties = editProperties();
1085 properties.put(selector.getChannelID(),
1086 (selector.getState(vals[i])).toString());
1087 updateProperties(properties);
1091 if (logger.isDebugEnabled()) {
1092 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1093 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1095 "Event stream: Discarding an event with an out of sync timestamp {} (last is {})",
1096 dateFormatter.format(new Date(currentTimeStamp)),
1097 dateFormatter.format(new Date(lastTimeStamp)));
1101 if (logger.isDebugEnabled()) {
1102 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1103 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1105 "Event Stream: Discarding an event that differs {} ms from the system time: {} (system is {})",
1106 systemTimeStamp - currentTimeStamp,
1107 dateFormatter.format(currentTimeStamp),
1108 dateFormatter.format(systemTimeStamp));
1110 if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
1111 logger.trace("Event stream: The event stream will be reset");
1112 isEstablished = false;
1115 line = eventBufferedReader.readLine();
1117 logger.trace("Event stream: The end of stream was reached");
1118 isEstablished = false;
1121 logger.debug("Event stream: The vehicle is not awake");
1122 if (vehicle != null) {
1124 // wake up the vehicle until streaming token <> 0
1125 logger.debug("Event stream: Waking up the vehicle");
1129 vehicle = queryVehicle();
1131 Thread.sleep(EVENT_STREAM_PAUSE);
1134 } catch (IOException | NumberFormatException e) {
1135 logger.debug("Event stream: An exception occurred while reading events: '{}'", e.getMessage());
1136 isEstablished = false;
1137 } catch (InterruptedException e) {
1138 isEstablished = false;
1141 if (Thread.interrupted()) {
1142 logger.debug("Event stream: the event stream was interrupted");