2 * Copyright (c) 2010-2023 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.IOException;
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
21 import java.net.URISyntaxException;
22 import java.text.SimpleDateFormat;
23 import java.util.Arrays;
24 import java.util.Date;
25 import java.util.HashMap;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.locks.ReentrantLock;
31 import java.util.stream.Collectors;
33 import javax.measure.quantity.Temperature;
34 import javax.ws.rs.ProcessingException;
35 import javax.ws.rs.client.WebTarget;
36 import javax.ws.rs.core.MediaType;
37 import javax.ws.rs.core.Response;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.tesla.internal.TeslaBindingConstants;
41 import org.openhab.binding.tesla.internal.TeslaBindingConstants.EventKeys;
42 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy;
43 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy.TeslaChannelSelector;
44 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Request;
45 import org.openhab.binding.tesla.internal.protocol.ChargeState;
46 import org.openhab.binding.tesla.internal.protocol.ClimateState;
47 import org.openhab.binding.tesla.internal.protocol.DriveState;
48 import org.openhab.binding.tesla.internal.protocol.Event;
49 import org.openhab.binding.tesla.internal.protocol.GUIState;
50 import org.openhab.binding.tesla.internal.protocol.Vehicle;
51 import org.openhab.binding.tesla.internal.protocol.VehicleState;
52 import org.openhab.binding.tesla.internal.throttler.QueueChannelThrottler;
53 import org.openhab.binding.tesla.internal.throttler.Rate;
54 import org.openhab.core.io.net.http.WebSocketFactory;
55 import org.openhab.core.library.types.DecimalType;
56 import org.openhab.core.library.types.IncreaseDecreaseType;
57 import org.openhab.core.library.types.OnOffType;
58 import org.openhab.core.library.types.PercentType;
59 import org.openhab.core.library.types.QuantityType;
60 import org.openhab.core.library.types.StringType;
61 import org.openhab.core.library.unit.SIUnits;
62 import org.openhab.core.library.unit.Units;
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 FAST_STATUS_REFRESH_INTERVAL = 15000;
90 private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
91 private static final int API_SLEEP_INTERVAL_MINUTES = 20;
92 private static final int MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT = 5;
93 private static final int THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES = 60;
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 EVENT_STREAM_PAUSE = 3000;
97 private static final int EVENT_TIMESTAMP_AGE_LIMIT = 3000;
98 private static final int EVENT_TIMESTAMP_MAX_DELTA = 10000;
99 private static final int EVENT_PING_INTERVAL = 10000;
101 private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
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 allowWakeUpForCommands;
114 protected boolean enableEvents = false;
115 protected boolean useDriveState = false;
116 protected boolean useAdvancedStates = false;
117 protected boolean lastValidDriveStateNotNull = true;
119 protected long lastTimeStamp;
120 protected long apiIntervalTimestamp;
121 protected int apiIntervalErrors;
122 protected long eventIntervalTimestamp;
123 protected int eventIntervalErrors;
124 protected int inactivity = MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT;
125 protected ReentrantLock lock;
127 protected double lastLongitude;
128 protected double lastLatitude;
129 protected long lastLocationChangeTimestamp;
130 protected long lastDriveStateChangeToNullTimestamp;
131 protected long lastAdvModesTimestamp = System.currentTimeMillis();
132 protected long lastStateTimestamp = System.currentTimeMillis();
133 protected int backOffCounter = 0;
135 protected String lastState = "";
136 protected boolean isInactive = false;
138 protected TeslaAccountHandler account;
140 protected QueueChannelThrottler stateThrottler;
141 protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
142 protected Thread eventThread;
143 protected ScheduledFuture<?> fastStateJob;
144 protected ScheduledFuture<?> slowStateJob;
145 protected WebSocketFactory webSocketFactory;
147 private final Gson gson = new Gson();
149 public TeslaVehicleHandler(Thing thing, WebSocketFactory webSocketFactory) {
151 this.webSocketFactory = webSocketFactory;
154 @SuppressWarnings("null")
156 public void initialize() {
157 logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
158 updateStatus(ThingStatus.UNKNOWN);
159 allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
160 allowWakeUpForCommands = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUPFORCOMMANDS);
161 enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
162 Number inactivityParam = (Number) getConfig().get(TeslaBindingConstants.CONFIG_INACTIVITY);
163 inactivity = inactivityParam == null ? MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT : inactivityParam.intValue();
164 Boolean useDriveStateParam = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDRIVESTATE);
165 useDriveState = useDriveStateParam == null ? false : useDriveStateParam;
166 Boolean useAdvancedStatesParam = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDADVANCEDSTATES);
167 useAdvancedStates = useAdvancedStatesParam == null ? false : useAdvancedStatesParam;
169 account = (TeslaAccountHandler) getBridge().getHandler();
170 lock = new ReentrantLock();
171 scheduler.execute(this::queryVehicleAndUpdate);
175 Map<Object, Rate> channels = new HashMap<>();
176 channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
177 channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
179 Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
180 Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
181 stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
182 stateThrottler.addRate(secondRate);
184 if (fastStateJob == null || fastStateJob.isCancelled()) {
185 fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
186 TimeUnit.MILLISECONDS);
189 if (slowStateJob == null || slowStateJob.isCancelled()) {
190 slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
191 TimeUnit.MILLISECONDS);
195 if (eventThread == null) {
196 eventThread = new Thread(eventRunnable, "OH-binding-" + getThing().getUID() + "-events");
206 public void dispose() {
207 logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
210 if (fastStateJob != null && !fastStateJob.isCancelled()) {
211 fastStateJob.cancel(true);
215 if (slowStateJob != null && !slowStateJob.isCancelled()) {
216 slowStateJob.cancel(true);
220 if (eventThread != null && !eventThread.isInterrupted()) {
221 eventThread.interrupt();
230 * Retrieves the unique vehicle id this handler is associated with
232 * @return the vehicle id
234 public String getVehicleId() {
235 if (vehicle != null) {
243 public void handleCommand(ChannelUID channelUID, Command command) {
244 logger.debug("handleCommand {} {}", channelUID, command);
245 String channelID = channelUID.getId();
246 TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
248 if (command instanceof RefreshType) {
250 logger.debug("Waking vehicle to refresh all data");
256 // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
257 // throttled so we are safe not to break the Tesla SLA
259 } else if (selector != null) {
260 if (!isAwake() && allowWakeUpForCommands) {
261 logger.debug("Waking vehicle to send command.");
267 case CHARGE_LIMIT_SOC: {
268 if (command instanceof PercentType) {
269 setChargeLimit(((PercentType) command).intValue());
270 } else if (command instanceof OnOffType && command == OnOffType.ON) {
272 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
274 } else if (command instanceof IncreaseDecreaseType
275 && command == IncreaseDecreaseType.INCREASE) {
276 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
277 } else if (command instanceof IncreaseDecreaseType
278 && command == IncreaseDecreaseType.DECREASE) {
279 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
285 if (command instanceof DecimalType) {
286 amps = ((DecimalType) command).intValue();
288 if (command instanceof QuantityType<?>) {
289 QuantityType<?> qamps = ((QuantityType<?>) command).toUnit(Units.AMPERE);
291 amps = qamps.intValue();
295 if (amps < 5 || amps > 32) {
296 logger.warn("Charging amps can only be set in a range of 5-32A, but not to {}A.", amps);
299 setChargingAmps(amps);
302 case COMBINED_TEMP: {
303 QuantityType<Temperature> quantity = commandToQuantityType(command);
304 if (quantity != null) {
305 setCombinedTemperature(quanityToRoundedFloat(quantity));
310 QuantityType<Temperature> quantity = commandToQuantityType(command);
311 if (quantity != null) {
312 setDriverTemperature(quanityToRoundedFloat(quantity));
316 case PASSENGER_TEMP: {
317 QuantityType<Temperature> quantity = commandToQuantityType(command);
318 if (quantity != null) {
319 setPassengerTemperature(quanityToRoundedFloat(quantity));
324 if (command instanceof OnOffType) {
325 setSentryMode(command == OnOffType.ON);
329 case SUN_ROOF_STATE: {
330 if (command instanceof StringType) {
331 setSunroof(command.toString());
335 case CHARGE_TO_MAX: {
336 if (command instanceof OnOffType) {
337 if (((OnOffType) command) == OnOffType.ON) {
338 setMaxRangeCharging(true);
340 setMaxRangeCharging(false);
346 if (command instanceof OnOffType) {
347 if (((OnOffType) command) == OnOffType.ON) {
356 if (command instanceof OnOffType) {
357 if (((OnOffType) command) == OnOffType.ON) {
364 if (command instanceof OnOffType) {
365 if (((OnOffType) command) == OnOffType.ON) {
372 if (command instanceof OnOffType) {
373 if (((OnOffType) command) == OnOffType.ON) {
380 if (command instanceof OnOffType) {
381 if (((OnOffType) command) == OnOffType.ON) {
390 if (command instanceof OnOffType) {
391 if (((OnOffType) command) == OnOffType.ON) {
392 autoConditioning(true);
394 autoConditioning(false);
400 if (command instanceof OnOffType) {
401 if (((OnOffType) command) == OnOffType.ON) {
408 if (command instanceof OnOffType) {
409 if (((OnOffType) command) == OnOffType.ON) {
416 if (command instanceof OnOffType) {
417 if (((OnOffType) command) == OnOffType.ON) {
418 if (vehicleState.rt == 0) {
421 } else if (vehicleState.rt == 1) {
428 if (command instanceof OnOffType) {
429 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
430 if (((OnOffType) command) == OnOffType.ON) {
431 setValetMode(true, valetpin);
433 setValetMode(false, valetpin);
438 case RESET_VALET_PIN: {
439 if (command instanceof OnOffType) {
440 if (((OnOffType) command) == OnOffType.ON) {
446 case STEERINGWHEEL_HEATER: {
447 if (command instanceof OnOffType) {
448 boolean commandBooleanValue = ((OnOffType) command) == OnOffType.ON ? true : false;
449 setSteeringWheelHeater(commandBooleanValue);
457 } catch (IllegalArgumentException e) {
459 "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
460 channelID, command.toString());
465 public void sendCommand(String command, String payLoad, WebTarget target) {
466 if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
467 Request request = account.newRequest(this, command, payLoad, target, allowWakeUpForCommands);
468 if (stateThrottler != null) {
469 stateThrottler.submit(COMMAND_THROTTLE, request);
474 public void sendCommand(String command) {
475 sendCommand(command, "{}");
478 public void sendCommand(String command, String payLoad) {
479 if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
480 Request request = account.newRequest(this, command, payLoad, account.commandTarget, allowWakeUpForCommands);
481 if (stateThrottler != null) {
482 stateThrottler.submit(COMMAND_THROTTLE, request);
487 public void sendCommand(String command, WebTarget target) {
488 if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
489 Request request = account.newRequest(this, command, "{}", target, allowWakeUpForCommands);
490 if (stateThrottler != null) {
491 stateThrottler.submit(COMMAND_THROTTLE, request);
496 public void requestData(String command, String payLoad) {
497 if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
498 Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget, false);
499 if (stateThrottler != null) {
500 stateThrottler.submit(DATA_THROTTLE, request);
506 protected void updateStatus(ThingStatus status) {
507 super.updateStatus(status);
511 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
512 super.updateStatus(status, statusDetail);
516 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
517 super.updateStatus(status, statusDetail, description);
520 public void requestData(String command) {
521 requestData(command, null);
524 public void queryVehicle(String parameter) {
525 WebTarget target = account.vehicleTarget.path(parameter);
526 sendCommand(parameter, null, target);
529 public void requestAllData() {
530 requestData(DRIVE_STATE);
531 requestData(VEHICLE_STATE);
532 requestData(CHARGE_STATE);
533 requestData(CLIMATE_STATE);
534 requestData(GUI_STATE);
537 protected boolean isAwake() {
538 return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
541 protected boolean isInMotion() {
542 if (driveState != null) {
543 if (driveState.speed != null && driveState.shift_state != null) {
544 return !"Undefined".equals(driveState.speed)
545 && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
551 protected boolean isInactive() {
552 // vehicle is inactive in case
553 // - it does not charge
554 // - it has not moved or optionally stopped reporting drive state, in the observation period
555 // - it is not in dog, camp, keep, sentry or any other mode that keeps it online
556 return isInactive && !isCharging() && !notReadyForSleep();
559 protected boolean isCharging() {
560 return chargeState != null && "Charging".equals(chargeState.charging_state);
563 protected boolean notReadyForSleep() {
565 int computedInactivityPeriod = inactivity;
567 if (useAdvancedStates) {
568 if (vehicleState.is_user_present && !isInMotion()) {
569 logger.debug("Car is occupied but stationary.");
570 if (lastAdvModesTimestamp < (System.currentTimeMillis()
571 - (THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES * 60 * 1000))) {
572 logger.debug("Ignoring after {} minutes.", THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES);
574 return (backOffCounter++ % 6 == 0); // using 6 should make sure 1 out of 5 pollers get serviced,
577 } else if (vehicleState.sentry_mode) {
578 logger.debug("Car is in sentry mode.");
579 if (lastAdvModesTimestamp < (System.currentTimeMillis()
580 - (THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES * 60 * 1000))) {
581 logger.debug("Ignoring after {} minutes.", THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES);
583 return (backOffCounter++ % 6 == 0);
585 } else if ((vehicleState.center_display_state != 0) && (!isInMotion())) {
586 logger.debug("Car is in camp, climate keep, dog, or other mode preventing sleep. Mode {}",
587 vehicleState.center_display_state);
588 return (backOffCounter++ % 6 == 0);
590 lastAdvModesTimestamp = System.currentTimeMillis();
594 if (vehicleState.homelink_nearby) {
595 computedInactivityPeriod = MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT;
596 logger.debug("Car is at home. Movement or drive state threshold is {} min.",
597 MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT);
601 if (driveState.shift_state != null) {
602 logger.debug("Car drive state not null and not ready to sleep.");
605 status = lastDriveStateChangeToNullTimestamp > (System.currentTimeMillis()
606 - (computedInactivityPeriod * 60 * 1000));
608 logger.debug("Drivestate is null but has changed recently, therefore continuing to poll.");
611 logger.debug("Drivestate has changed to null after interval {} min and can now be put to sleep.",
612 computedInactivityPeriod);
617 status = lastLocationChangeTimestamp > (System.currentTimeMillis()
618 - (computedInactivityPeriod * 60 * 1000));
620 logger.debug("Car has moved recently and can not sleep");
623 logger.debug("Car has not moved in {} min, and can sleep", computedInactivityPeriod);
629 protected boolean allowQuery() {
630 return (isAwake() && !isInactive());
633 protected void setActive() {
635 lastLocationChangeTimestamp = System.currentTimeMillis();
636 lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
641 protected boolean checkResponse(Response response, boolean immediatelyFail) {
642 if (response != null && response.getStatus() == 200) {
644 } else if (response != null && response.getStatus() == 401) {
645 logger.debug("The access token has expired, trying to get a new one.");
646 account.authenticate();
649 if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
650 if (immediatelyFail) {
651 logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
653 logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
654 TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
655 TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
658 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
659 } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
660 * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
661 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
662 apiIntervalTimestamp = System.currentTimeMillis();
663 apiIntervalErrors = 0;
670 public void setChargeLimit(int percent) {
671 JsonObject payloadObject = new JsonObject();
672 payloadObject.addProperty("percent", percent);
673 sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
674 requestData(CHARGE_STATE);
677 public void setChargingAmps(int amps) {
678 JsonObject payloadObject = new JsonObject();
679 payloadObject.addProperty("charging_amps", amps);
680 sendCommand(COMMAND_SET_CHARGING_AMPS, gson.toJson(payloadObject), account.commandTarget);
681 requestData(CHARGE_STATE);
684 public void setSentryMode(boolean b) {
685 JsonObject payloadObject = new JsonObject();
686 payloadObject.addProperty("on", b);
687 sendCommand(COMMAND_SET_SENTRY_MODE, gson.toJson(payloadObject), account.commandTarget);
688 requestData(VEHICLE_STATE);
691 public void setSunroof(String state) {
692 if ("vent".equals(state) || "close".equals(state)) {
693 JsonObject payloadObject = new JsonObject();
694 payloadObject.addProperty("state", state);
695 sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
696 requestData(VEHICLE_STATE);
698 logger.warn("Ignoring invalid command '{}' for sunroof.", state);
703 * Sets the driver and passenger temperatures.
705 * While setting different temperature values is supported by the API, in practice this does not always work
706 * reliably, possibly if the the
707 * only reliable method is to set the driver and passenger temperature to the same value
709 * @param driverTemperature in Celsius
710 * @param passenegerTemperature in Celsius
712 public void setTemperature(float driverTemperature, float passenegerTemperature) {
713 JsonObject payloadObject = new JsonObject();
714 payloadObject.addProperty("driver_temp", driverTemperature);
715 payloadObject.addProperty("passenger_temp", passenegerTemperature);
716 sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
717 requestData(CLIMATE_STATE);
720 public void setCombinedTemperature(float temperature) {
721 setTemperature(temperature, temperature);
724 public void setDriverTemperature(float temperature) {
725 setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
728 public void setPassengerTemperature(float temperature) {
729 setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
732 public void openFrunk() {
733 JsonObject payloadObject = new JsonObject();
734 payloadObject.addProperty("which_trunk", "front");
735 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
736 requestData(VEHICLE_STATE);
739 public void openTrunk() {
740 JsonObject payloadObject = new JsonObject();
741 payloadObject.addProperty("which_trunk", "rear");
742 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
743 requestData(VEHICLE_STATE);
746 public void closeTrunk() {
750 public void setValetMode(boolean b, Integer pin) {
751 JsonObject payloadObject = new JsonObject();
752 payloadObject.addProperty("on", b);
754 payloadObject.addProperty("password", String.format("%04d", pin));
756 sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
757 requestData(VEHICLE_STATE);
760 public void resetValetPin() {
761 sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
762 requestData(VEHICLE_STATE);
765 public void setMaxRangeCharging(boolean b) {
766 sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
767 requestData(CHARGE_STATE);
770 public void charge(boolean b) {
771 sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
772 requestData(CHARGE_STATE);
775 public void flashLights() {
776 sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
779 public void honkHorn() {
780 sendCommand(COMMAND_HONK_HORN, account.commandTarget);
783 public void openChargePort() {
784 sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
785 requestData(CHARGE_STATE);
788 public void lockDoors(boolean b) {
789 sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
790 requestData(VEHICLE_STATE);
793 public void autoConditioning(boolean b) {
794 sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
795 requestData(CLIMATE_STATE);
798 public void wakeUp() {
799 sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
802 public void setSteeringWheelHeater(boolean isOn) {
803 JsonObject payloadObject = new JsonObject();
804 payloadObject.addProperty("on", isOn);
805 sendCommand(COMMAND_STEERING_WHEEL_HEATER, gson.toJson(payloadObject), account.commandTarget);
808 protected Vehicle queryVehicle() {
809 String authHeader = account.getAuthHeader();
811 if (authHeader != null) {
813 // get a list of vehicles
814 synchronized (account.vehiclesTarget) {
815 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
816 .header("Authorization", authHeader).get();
818 logger.debug("Querying the vehicle, response : {}, {}", response.getStatus(),
819 response.getStatusInfo().getReasonPhrase());
821 if (!checkResponse(response, true)) {
822 logger.debug("An error occurred while querying the vehicle");
826 JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
827 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
829 for (Vehicle vehicle : vehicleArray) {
830 logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
831 if (vehicle.vin.equals(getConfig().get(VIN))) {
832 vehicleJSON = gson.toJson(vehicle);
833 parseAndUpdate("queryVehicle", null, vehicleJSON);
834 if (logger.isTraceEnabled()) {
835 logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
843 } catch (ProcessingException e) {
844 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
850 protected void queryVehicleAndUpdate() {
851 vehicle = queryVehicle();
854 public void parseAndUpdate(String request, String payLoad, String result) {
855 final double locationThreshold = .0000001;
857 JsonObject jsonObject = null;
860 if (request != null && result != null && !"null".equals(result)) {
861 updateStatus(ThingStatus.ONLINE);
862 // first, update state objects
865 driveState = gson.fromJson(result, DriveState.class);
867 if (Math.abs(lastLatitude - driveState.latitude) > locationThreshold
868 || Math.abs(lastLongitude - driveState.longitude) > locationThreshold) {
869 logger.debug("Vehicle moved, resetting last location timestamp");
871 lastLatitude = driveState.latitude;
872 lastLongitude = driveState.longitude;
873 lastLocationChangeTimestamp = System.currentTimeMillis();
875 logger.trace("Drive state: {}", driveState.shift_state);
877 if ((driveState.shift_state == null) && (lastValidDriveStateNotNull)) {
878 logger.debug("Set NULL shiftstate time");
879 lastValidDriveStateNotNull = false;
880 lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
881 } else if (driveState.shift_state != null) {
882 logger.trace("Clear NULL shiftstate time");
883 lastValidDriveStateNotNull = true;
889 guiState = gson.fromJson(result, GUIState.class);
892 case VEHICLE_STATE: {
893 vehicleState = gson.fromJson(result, VehicleState.class);
897 chargeState = gson.fromJson(result, ChargeState.class);
899 updateState(CHANNEL_CHARGE, OnOffType.ON);
901 updateState(CHANNEL_CHARGE, OnOffType.OFF);
906 case CLIMATE_STATE: {
907 climateState = gson.fromJson(result, ClimateState.class);
908 BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
909 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
910 updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
913 case "queryVehicle": {
914 if (vehicle != null) {
915 logger.debug("Vehicle state is {}", vehicle.state);
917 logger.debug("Vehicle state is initializing or unknown");
921 if (vehicle != null && "asleep".equals(vehicle.state)) {
922 logger.debug("Vehicle is asleep.");
926 if (vehicle != null && !lastState.equals(vehicle.state)) {
927 lastState = vehicle.state;
929 // in case vehicle changed to awake, refresh all data
931 logger.debug("Vehicle is now awake, updating all data");
932 lastLocationChangeTimestamp = System.currentTimeMillis();
933 lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
940 // reset timestamp if elapsed and set inactive to false
941 if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
942 .currentTimeMillis()) {
943 logger.debug("Vehicle did not fall asleep within sleep period, checking again");
946 boolean wasInactive = isInactive;
947 isInactive = !isCharging() && !notReadyForSleep();
949 if (!wasInactive && isInactive) {
950 lastStateTimestamp = System.currentTimeMillis();
951 logger.debug("Vehicle is inactive");
959 // secondly, reformat the response string to a JSON compliant
960 // object for some specific non-JSON compatible requests
962 case MOBILE_ENABLED_STATE: {
963 jsonObject = new JsonObject();
964 jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
968 jsonObject = JsonParser.parseString(result).getAsJsonObject();
974 // process the result
975 if (jsonObject != null && result != null && !"null".equals(result)) {
976 // deal with responses for "set" commands, which get confirmed
977 // positively, or negatively, in which case a reason for failure
979 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
980 boolean requestResult = jsonObject.get("result").getAsBoolean();
981 logger.debug("The request ({}) execution was {}, and reported '{}'", request,
982 requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString());
984 Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
986 long resultTimeStamp = 0;
987 for (Map.Entry<String, JsonElement> entry : entrySet) {
988 if ("timestamp".equals(entry.getKey())) {
989 resultTimeStamp = Long.parseLong(entry.getValue().getAsString());
990 if (logger.isTraceEnabled()) {
991 Date date = new Date(resultTimeStamp);
992 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
993 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
1002 boolean proceed = true;
1003 if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
1008 for (Map.Entry<String, JsonElement> entry : entrySet) {
1010 TeslaChannelSelector selector = TeslaChannelSelector
1011 .getValueSelectorFromRESTID(entry.getKey());
1012 if (!selector.isProperty()) {
1013 if (!entry.getValue().isJsonNull()) {
1014 updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
1015 entry.getValue().getAsString(), selector, editProperties()));
1016 if (logger.isTraceEnabled()) {
1018 "The variable/value pair '{}':'{}' is successfully processed",
1019 entry.getKey(), entry.getValue());
1022 updateState(selector.getChannelID(), UnDefType.UNDEF);
1024 } else if (!entry.getValue().isJsonNull()) {
1025 Map<String, String> properties = editProperties();
1026 properties.put(selector.getChannelID(), entry.getValue().getAsString());
1027 updateProperties(properties);
1028 if (logger.isTraceEnabled()) {
1030 "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
1031 entry.getKey(), entry.getValue(), selector.getChannelID());
1034 } catch (IllegalArgumentException e) {
1035 logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
1036 entry.getKey(), entry.getValue());
1037 } catch (ClassCastException | IllegalStateException e) {
1038 logger.trace("An exception occurred while converting the JSON data : '{}'",
1043 logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
1051 } catch (Exception p) {
1052 logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
1056 @SuppressWarnings("unchecked")
1057 protected QuantityType<Temperature> commandToQuantityType(Command command) {
1058 if (command instanceof QuantityType) {
1059 return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
1061 return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
1064 protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
1065 return roundBigDecimal(quantity.toBigDecimal()).floatValue();
1068 protected BigDecimal roundBigDecimal(BigDecimal value) {
1069 return value.setScale(1, RoundingMode.HALF_EVEN);
1072 protected Runnable slowStateRunnable = () -> {
1074 queryVehicleAndUpdate();
1075 boolean allowQuery = allowQuery();
1078 requestData(CHARGE_STATE);
1079 requestData(CLIMATE_STATE);
1080 requestData(GUI_STATE);
1081 queryVehicle(MOBILE_ENABLED_STATE);
1082 } else if (allowWakeUp) {
1084 } else if (isAwake()) {
1085 logger.debug("slowpoll: Throttled to allow sleep, occupied/idle, or in a console mode");
1087 lastAdvModesTimestamp = System.currentTimeMillis();
1089 } catch (Exception e) {
1090 logger.warn("Exception occurred in slowStateRunnable", e);
1094 protected Runnable fastStateRunnable = () -> {
1095 if (getThing().getStatus() == ThingStatus.ONLINE) {
1096 boolean allowQuery = allowQuery();
1099 requestData(DRIVE_STATE);
1100 requestData(VEHICLE_STATE);
1101 } else if (allowWakeUp) {
1103 } else if (isAwake()) {
1104 logger.debug("fastpoll: Throttled to allow sleep, occupied/idle, or in a console mode");
1109 protected Runnable eventRunnable = new Runnable() {
1110 TeslaEventEndpoint eventEndpoint;
1111 boolean isAuthenticated = false;
1112 long lastPingTimestamp = 0;
1116 eventEndpoint = new TeslaEventEndpoint(getThing().getUID(), webSocketFactory);
1117 eventEndpoint.addEventHandler(new TeslaEventEndpoint.EventHandler() {
1119 public void handleEvent(Event event) {
1120 if (event != null) {
1121 switch (event.msg_type) {
1122 case "control:hello":
1123 logger.debug("Event : Received hello");
1126 logger.debug("Event : Received an update: '{}'", event.value);
1128 String vals[] = event.value.split(",");
1129 long currentTimeStamp = Long.parseLong(vals[0]);
1130 long systemTimeStamp = System.currentTimeMillis();
1131 if (logger.isDebugEnabled()) {
1132 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
1133 logger.debug("STS {} CTS {} Delta {}",
1134 dateFormatter.format(new Date(systemTimeStamp)),
1135 dateFormatter.format(new Date(currentTimeStamp)),
1136 systemTimeStamp - currentTimeStamp);
1138 if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
1139 if (currentTimeStamp > lastTimeStamp) {
1140 lastTimeStamp = Long.parseLong(vals[0]);
1141 if (logger.isDebugEnabled()) {
1142 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1143 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1144 logger.debug("Event : Event stamp is {}",
1145 dateFormatter.format(new Date(lastTimeStamp)));
1147 for (int i = 0; i < EventKeys.values().length; i++) {
1148 TeslaChannelSelector selector = TeslaChannelSelector
1149 .getValueSelectorFromRESTID((EventKeys.values()[i]).toString());
1151 if (!selector.isProperty()) {
1152 State newState = teslaChannelSelectorProxy.getState(vals[i], selector,
1154 if (newState != null && !"".equals(vals[i])) {
1155 updateState(selector.getChannelID(), newState);
1157 updateState(selector.getChannelID(), UnDefType.UNDEF);
1159 if (logger.isTraceEnabled()) {
1161 "The variable/value pair '{}':'{}' is successfully processed",
1162 EventKeys.values()[i], vals[i]);
1165 Map<String, String> properties = editProperties();
1166 properties.put(selector.getChannelID(),
1167 (selector.getState(vals[i])).toString());
1168 updateProperties(properties);
1169 if (logger.isTraceEnabled()) {
1171 "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
1172 EventKeys.values()[i], vals[i], selector.getChannelID());
1176 } else if (logger.isDebugEnabled()) {
1177 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1178 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1180 "Event : Discarding an event with an out of sync timestamp {} (last is {})",
1181 dateFormatter.format(new Date(currentTimeStamp)),
1182 dateFormatter.format(new Date(lastTimeStamp)));
1185 if (logger.isDebugEnabled()) {
1186 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1187 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1189 "Event : Discarding an event that differs {} ms from the system time: {} (system is {})",
1190 systemTimeStamp - currentTimeStamp,
1191 dateFormatter.format(currentTimeStamp),
1192 dateFormatter.format(systemTimeStamp));
1194 if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
1195 logger.trace("Event : The event endpoint will be reset");
1196 eventEndpoint.closeConnection();
1201 logger.debug("Event : Received an error: '{}'/'{}'", event.value, event.error_type);
1202 eventEndpoint.closeConnection();
1211 if (getThing().getStatus() == ThingStatus.ONLINE) {
1213 eventEndpoint.connect(new URI(URI_EVENT));
1215 if (eventEndpoint.isConnected()) {
1216 if (!isAuthenticated) {
1217 logger.debug("Event : Authenticating vehicle {}", vehicle.vehicle_id);
1218 JsonObject payloadObject = new JsonObject();
1219 payloadObject.addProperty("msg_type", "data:subscribe_oauth");
1220 payloadObject.addProperty("token", account.getAccessToken());
1221 payloadObject.addProperty("value", Arrays.asList(EventKeys.values()).stream()
1222 .skip(1).map(Enum::toString).collect(Collectors.joining(",")));
1223 payloadObject.addProperty("tag", vehicle.vehicle_id);
1225 eventEndpoint.sendMessage(gson.toJson(payloadObject));
1226 isAuthenticated = true;
1228 lastPingTimestamp = System.nanoTime();
1231 if (TimeUnit.MILLISECONDS.convert(System.nanoTime() - lastPingTimestamp,
1232 TimeUnit.NANOSECONDS) > EVENT_PING_INTERVAL) {
1233 logger.trace("Event : Pinging the Tesla event stream infrastructure");
1234 eventEndpoint.ping();
1235 lastPingTimestamp = System.nanoTime();
1239 if (!eventEndpoint.isConnected()) {
1240 isAuthenticated = false;
1241 eventIntervalErrors++;
1242 if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
1244 "Event : Reached the maximum number of errors ({}) for the current interval ({} seconds)",
1245 EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
1246 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1247 eventEndpoint.closeConnection();
1250 if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
1251 * EVENT_ERROR_INTERVAL_SECONDS) {
1253 "Event : Resetting the error counter. ({} errors in the last interval)",
1254 eventIntervalErrors);
1255 eventIntervalTimestamp = System.currentTimeMillis();
1256 eventIntervalErrors = 0;
1260 logger.debug("Event : The vehicle is not awake");
1261 if (vehicle != null) {
1263 // wake up the vehicle until streaming token <> 0
1264 logger.debug("Event : Waking up the vehicle");
1268 vehicle = queryVehicle();
1272 } catch (URISyntaxException | NumberFormatException | IOException e) {
1273 logger.debug("Event : An exception occurred while processing events: '{}'", e.getMessage());
1277 Thread.sleep(EVENT_STREAM_PAUSE);
1278 } catch (InterruptedException e) {
1279 logger.debug("Event : An exception occurred while putting the event thread to sleep: '{}'",
1283 if (Thread.interrupted()) {
1284 logger.debug("Event : The event thread was interrupted");
1285 eventEndpoint.close();