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;
26 import java.util.HashSet;
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.WebTarget;
37 import javax.ws.rs.core.MediaType;
38 import javax.ws.rs.core.Response;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.openhab.binding.tesla.internal.TeslaBindingConstants;
42 import org.openhab.binding.tesla.internal.TeslaBindingConstants.EventKeys;
43 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy;
44 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy.TeslaChannelSelector;
45 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Request;
46 import org.openhab.binding.tesla.internal.protocol.ChargeState;
47 import org.openhab.binding.tesla.internal.protocol.ClimateState;
48 import org.openhab.binding.tesla.internal.protocol.DriveState;
49 import org.openhab.binding.tesla.internal.protocol.Event;
50 import org.openhab.binding.tesla.internal.protocol.GUIState;
51 import org.openhab.binding.tesla.internal.protocol.SoftwareUpdate;
52 import org.openhab.binding.tesla.internal.protocol.Vehicle;
53 import org.openhab.binding.tesla.internal.protocol.VehicleData;
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.io.net.http.WebSocketFactory;
58 import org.openhab.core.library.types.DateTimeType;
59 import org.openhab.core.library.types.DecimalType;
60 import org.openhab.core.library.types.IncreaseDecreaseType;
61 import org.openhab.core.library.types.OnOffType;
62 import org.openhab.core.library.types.PercentType;
63 import org.openhab.core.library.types.QuantityType;
64 import org.openhab.core.library.types.StringType;
65 import org.openhab.core.library.unit.SIUnits;
66 import org.openhab.core.library.unit.Units;
67 import org.openhab.core.thing.ChannelUID;
68 import org.openhab.core.thing.Thing;
69 import org.openhab.core.thing.ThingStatus;
70 import org.openhab.core.thing.ThingStatusDetail;
71 import org.openhab.core.thing.binding.BaseThingHandler;
72 import org.openhab.core.types.Command;
73 import org.openhab.core.types.RefreshType;
74 import org.openhab.core.types.State;
75 import org.openhab.core.types.UnDefType;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
79 import com.google.gson.Gson;
80 import com.google.gson.JsonElement;
81 import com.google.gson.JsonObject;
82 import com.google.gson.JsonParser;
85 * The {@link TeslaVehicleHandler} is responsible for handling commands, which are sent
86 * to one of the channels of a specific vehicle.
88 * @author Karel Goderis - Initial contribution
89 * @author Kai Kreuzer - Refactored to use separate account handler and improved configuration options
91 public class TeslaVehicleHandler extends BaseThingHandler {
93 private static final int FAST_STATUS_REFRESH_INTERVAL = 15000;
94 private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
95 private static final int API_SLEEP_INTERVAL_MINUTES = 20;
96 private static final int MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT = 5;
97 private static final int THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES = 60;
98 private static final int EVENT_MAXIMUM_ERRORS_IN_INTERVAL = 10;
99 private static final int EVENT_ERROR_INTERVAL_SECONDS = 15;
100 private static final int EVENT_STREAM_PAUSE = 3000;
101 private static final int EVENT_TIMESTAMP_AGE_LIMIT = 3000;
102 private static final int EVENT_TIMESTAMP_MAX_DELTA = 10000;
103 private static final int EVENT_PING_INTERVAL = 10000;
105 private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
107 // Vehicle state variables
108 protected Vehicle vehicle;
109 protected String vehicleJSON;
110 protected DriveState driveState;
111 protected GUIState guiState;
112 protected VehicleState vehicleState;
113 protected ChargeState chargeState;
114 protected ClimateState climateState;
115 protected SoftwareUpdate softwareUpdate;
117 protected boolean allowWakeUp;
118 protected boolean allowWakeUpForCommands;
119 protected boolean enableEvents = false;
120 protected boolean useDriveState = false;
121 protected boolean useAdvancedStates = false;
122 protected boolean lastValidDriveStateNotNull = true;
124 protected long lastTimeStamp;
125 protected long apiIntervalTimestamp;
126 protected int apiIntervalErrors;
127 protected long eventIntervalTimestamp;
128 protected int eventIntervalErrors;
129 protected int inactivity = MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT;
130 protected ReentrantLock lock;
132 protected double lastLongitude;
133 protected double lastLatitude;
134 protected long lastLocationChangeTimestamp;
135 protected long lastDriveStateChangeToNullTimestamp;
136 protected long lastAdvModesTimestamp = System.currentTimeMillis();
137 protected long lastStateTimestamp = System.currentTimeMillis();
138 protected int backOffCounter = 0;
140 protected String lastState = "";
141 protected boolean isInactive = false;
143 protected TeslaAccountHandler account;
145 protected QueueChannelThrottler stateThrottler;
146 protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
147 protected Thread eventThread;
148 protected ScheduledFuture<?> stateJob;
149 protected WebSocketFactory webSocketFactory;
151 private final Gson gson = new Gson();
153 public TeslaVehicleHandler(Thing thing, WebSocketFactory webSocketFactory) {
155 this.webSocketFactory = webSocketFactory;
158 @SuppressWarnings("null")
160 public void initialize() {
161 logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
162 updateStatus(ThingStatus.UNKNOWN);
163 allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
164 allowWakeUpForCommands = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUPFORCOMMANDS);
165 enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
166 Number inactivityParam = (Number) getConfig().get(TeslaBindingConstants.CONFIG_INACTIVITY);
167 inactivity = inactivityParam == null ? MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT : inactivityParam.intValue();
168 Boolean useDriveStateParam = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDRIVESTATE);
169 useDriveState = useDriveStateParam == null ? false : useDriveStateParam;
170 Boolean useAdvancedStatesParam = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_USEDADVANCEDSTATES);
171 useAdvancedStates = useAdvancedStatesParam == null ? false : useAdvancedStatesParam;
173 account = (TeslaAccountHandler) getBridge().getHandler();
174 lock = new ReentrantLock();
175 scheduler.execute(this::queryVehicleAndUpdate);
179 Map<Object, Rate> channels = new HashMap<>();
180 channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
181 channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
183 Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
184 Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
185 stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
186 stateThrottler.addRate(secondRate);
188 if (stateJob == null || stateJob.isCancelled()) {
189 stateJob = scheduler.scheduleWithFixedDelay(stateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
190 TimeUnit.MILLISECONDS);
194 if (eventThread == null) {
195 eventThread = new Thread(eventRunnable, "OH-binding-" + getThing().getUID() + "-events");
205 public void dispose() {
206 logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
209 if (stateJob != null && !stateJob.isCancelled()) {
210 stateJob.cancel(true);
214 if (eventThread != null && !eventThread.isInterrupted()) {
215 eventThread.interrupt();
224 * Retrieves the unique vehicle id this handler is associated with
226 * @return the vehicle id
228 public String getVehicleId() {
229 if (vehicle != null) {
237 public void handleCommand(ChannelUID channelUID, Command command) {
238 logger.debug("handleCommand {} {}", channelUID, command);
239 String channelID = channelUID.getId();
240 TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
242 if (command instanceof RefreshType) {
244 logger.debug("Waking vehicle to refresh all data");
250 // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
251 // throttled so we are safe not to break the Tesla SLA
253 } else if (selector != null) {
254 if (!isAwake() && allowWakeUpForCommands) {
255 logger.debug("Waking vehicle to send command.");
261 case CHARGE_LIMIT_SOC: {
262 if (command instanceof PercentType percentCommand) {
263 setChargeLimit(percentCommand.intValue());
264 } else if (command instanceof OnOffType && command == OnOffType.ON) {
266 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
268 } else if (command instanceof IncreaseDecreaseType
269 && command == IncreaseDecreaseType.INCREASE) {
270 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
271 } else if (command instanceof IncreaseDecreaseType
272 && command == IncreaseDecreaseType.DECREASE) {
273 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
279 if (command instanceof DecimalType decimalCommand) {
280 amps = decimalCommand.intValue();
282 if (command instanceof QuantityType<?> quantityCommand) {
283 QuantityType<?> qamps = quantityCommand.toUnit(Units.AMPERE);
285 amps = qamps.intValue();
290 logger.warn("Charging amps cannot be set higher than 32A, {}A was requested", amps);
294 logger.info("Charging amps should be set higher than 5A to avoid excessive losses.");
296 setChargingAmps(amps);
299 case COMBINED_TEMP: {
300 QuantityType<Temperature> quantity = commandToQuantityType(command);
301 if (quantity != null) {
302 setCombinedTemperature(quanityToRoundedFloat(quantity));
307 QuantityType<Temperature> quantity = commandToQuantityType(command);
308 if (quantity != null) {
309 setDriverTemperature(quanityToRoundedFloat(quantity));
313 case PASSENGER_TEMP: {
314 QuantityType<Temperature> quantity = commandToQuantityType(command);
315 if (quantity != null) {
316 setPassengerTemperature(quanityToRoundedFloat(quantity));
321 if (command instanceof OnOffType) {
322 setSentryMode(command == OnOffType.ON);
326 case SUN_ROOF_STATE: {
327 if (command instanceof StringType) {
328 setSunroof(command.toString());
332 case CHARGE_TO_MAX: {
333 if (command instanceof OnOffType onOffCommand) {
334 if (onOffCommand == OnOffType.ON) {
335 setMaxRangeCharging(true);
337 setMaxRangeCharging(false);
343 if (command instanceof OnOffType onOffCommand) {
344 if (onOffCommand == OnOffType.ON) {
353 if (command instanceof OnOffType onOffCommand) {
354 if (onOffCommand == OnOffType.ON) {
361 if (command instanceof OnOffType onOffCommand) {
362 if (onOffCommand == OnOffType.ON) {
369 if (command instanceof OnOffType onOffCommand) {
370 if (onOffCommand == OnOffType.ON) {
377 if (command instanceof OnOffType onOffCommand) {
378 if (onOffCommand == OnOffType.ON) {
387 if (command instanceof OnOffType onOffCommand) {
388 if (onOffCommand == OnOffType.ON) {
389 autoConditioning(true);
391 autoConditioning(false);
397 if (command instanceof OnOffType onOffCommand) {
398 if (onOffCommand == OnOffType.ON) {
405 if (command instanceof OnOffType onOffCommand) {
406 if (onOffCommand == OnOffType.ON) {
413 if (command instanceof OnOffType onOffCommand) {
414 if (onOffCommand == OnOffType.ON) {
415 if (vehicleState.rt == 0) {
418 } else if (vehicleState.rt == 1) {
425 if (command instanceof OnOffType onOffCommand) {
426 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
427 if (onOffCommand == OnOffType.ON) {
428 setValetMode(true, valetpin);
430 setValetMode(false, valetpin);
435 case RESET_VALET_PIN: {
436 if (command instanceof OnOffType onOffCommand) {
437 if (onOffCommand == OnOffType.ON) {
443 case STEERINGWHEEL_HEATER: {
444 if (command instanceof OnOffType onOffCommand) {
445 boolean commandBooleanValue = onOffCommand == OnOffType.ON ? true : false;
446 setSteeringWheelHeater(commandBooleanValue);
454 } catch (IllegalArgumentException e) {
456 "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
457 channelID, command.toString());
462 public void sendCommand(String command, String payLoad, WebTarget target) {
463 if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
464 Request request = account.newRequest(this, command, payLoad, target, allowWakeUpForCommands);
465 if (stateThrottler != null) {
466 stateThrottler.submit(COMMAND_THROTTLE, request);
471 public void sendCommand(String command) {
472 sendCommand(command, "{}");
475 public void sendCommand(String command, String payLoad) {
476 if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
477 Request request = account.newRequest(this, command, payLoad, account.commandTarget, allowWakeUpForCommands);
478 if (stateThrottler != null) {
479 stateThrottler.submit(COMMAND_THROTTLE, request);
484 public void sendCommand(String command, WebTarget target) {
485 if (COMMAND_WAKE_UP.equals(command) || isAwake() || allowWakeUpForCommands) {
486 Request request = account.newRequest(this, command, "{}", target, allowWakeUpForCommands);
487 if (stateThrottler != null) {
488 stateThrottler.submit(COMMAND_THROTTLE, request);
493 public void requestData(String command, String payLoad) {
494 if (COMMAND_WAKE_UP.equals(command) || isAwake()
495 || (!"vehicleData".equals(command) && allowWakeUpForCommands)) {
496 Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget, false);
497 if (stateThrottler != null) {
498 stateThrottler.submit(DATA_THROTTLE, request);
504 protected void updateStatus(ThingStatus status) {
505 super.updateStatus(status);
509 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
510 super.updateStatus(status, statusDetail);
514 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
515 super.updateStatus(status, statusDetail, description);
518 public void requestData(String command) {
519 requestData(command, null);
522 public void queryVehicle(String parameter) {
523 WebTarget target = account.vehicleTarget.path(parameter);
524 sendCommand(parameter, null, target);
527 public void requestAllData() {
528 requestData("vehicleData", null);
531 protected boolean isAwake() {
532 return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
535 protected boolean isInMotion() {
536 if (driveState != null) {
537 if (driveState.speed != null && driveState.shift_state != null) {
538 return !"Undefined".equals(driveState.speed)
539 && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
545 protected boolean isInactive() {
546 // vehicle is inactive in case
547 // - it does not charge
548 // - it has not moved or optionally stopped reporting drive state, in the observation period
549 // - it is not in dog, camp, keep, sentry or any other mode that keeps it online
550 return isInactive && !isCharging() && !notReadyForSleep();
553 protected boolean isCharging() {
554 return chargeState != null && "Charging".equals(chargeState.charging_state);
557 protected boolean notReadyForSleep() {
559 int computedInactivityPeriod = inactivity;
561 if (useAdvancedStates) {
562 if (vehicleState.is_user_present && !isInMotion()) {
563 logger.debug("Car is occupied but stationary.");
564 if (lastAdvModesTimestamp < (System.currentTimeMillis()
565 - (THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES * 60 * 1000))) {
566 logger.debug("Ignoring after {} minutes.", THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES);
568 return (backOffCounter++ % 6 == 0); // using 6 should make sure 1 out of 5 pollers get serviced,
571 } else if (vehicleState.sentry_mode) {
572 logger.debug("Car is in sentry mode.");
573 if (lastAdvModesTimestamp < (System.currentTimeMillis()
574 - (THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES * 60 * 1000))) {
575 logger.debug("Ignoring after {} minutes.", THRESHOLD_INTERVAL_FOR_ADVANCED_MINUTES);
577 return (backOffCounter++ % 6 == 0);
579 } else if ((vehicleState.center_display_state != 0) && (!isInMotion())) {
580 logger.debug("Car is in camp, climate keep, dog, or other mode preventing sleep. Mode {}",
581 vehicleState.center_display_state);
582 return (backOffCounter++ % 6 == 0);
584 lastAdvModesTimestamp = System.currentTimeMillis();
588 if (vehicleState != null && vehicleState.homelink_nearby) {
589 computedInactivityPeriod = MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT;
590 logger.debug("Car is at home. Movement or drive state threshold is {} min.",
591 MOVE_THRESHOLD_INTERVAL_MINUTES_DEFAULT);
595 if (driveState.shift_state != null) {
596 logger.debug("Car drive state not null and not ready to sleep.");
599 status = lastDriveStateChangeToNullTimestamp > (System.currentTimeMillis()
600 - (computedInactivityPeriod * 60 * 1000));
602 logger.debug("Drivestate is null but has changed recently, therefore continuing to poll.");
605 logger.debug("Drivestate has changed to null after interval {} min and can now be put to sleep.",
606 computedInactivityPeriod);
611 status = lastLocationChangeTimestamp > (System.currentTimeMillis()
612 - (computedInactivityPeriod * 60 * 1000));
614 logger.debug("Car has moved recently and can not sleep");
617 logger.debug("Car has not moved in {} min, and can sleep", computedInactivityPeriod);
623 protected boolean allowQuery() {
624 return (isAwake() && !isInactive());
627 protected void setActive() {
629 lastLocationChangeTimestamp = System.currentTimeMillis();
630 lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
635 protected boolean checkResponse(Response response, boolean immediatelyFail) {
636 if (response != null && response.getStatus() == 200) {
638 } else if (response != null && response.getStatus() == 401) {
639 logger.debug("The access token has expired, trying to get a new one.");
640 account.authenticate();
643 if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
644 if (immediatelyFail) {
645 logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
647 logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
648 TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
649 TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
652 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
653 } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
654 * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
655 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
656 apiIntervalTimestamp = System.currentTimeMillis();
657 apiIntervalErrors = 0;
664 public void setChargeLimit(int percent) {
665 JsonObject payloadObject = new JsonObject();
666 payloadObject.addProperty("percent", percent);
667 sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
670 public void setChargingAmps(int amps) {
671 JsonObject payloadObject = new JsonObject();
672 payloadObject.addProperty("charging_amps", amps);
673 sendCommand(COMMAND_SET_CHARGING_AMPS, gson.toJson(payloadObject), account.commandTarget);
676 public void setSentryMode(boolean b) {
677 JsonObject payloadObject = new JsonObject();
678 payloadObject.addProperty("on", b);
679 sendCommand(COMMAND_SET_SENTRY_MODE, gson.toJson(payloadObject), account.commandTarget);
682 public void setSunroof(String state) {
683 if ("vent".equals(state) || "close".equals(state)) {
684 JsonObject payloadObject = new JsonObject();
685 payloadObject.addProperty("state", state);
686 sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
688 logger.warn("Ignoring invalid command '{}' for sunroof.", state);
693 * Sets the driver and passenger temperatures.
695 * While setting different temperature values is supported by the API, in practice this does not always work
696 * reliably, possibly if the the
697 * only reliable method is to set the driver and passenger temperature to the same value
699 * @param driverTemperature in Celsius
700 * @param passenegerTemperature in Celsius
702 public void setTemperature(float driverTemperature, float passenegerTemperature) {
703 JsonObject payloadObject = new JsonObject();
704 payloadObject.addProperty("driver_temp", driverTemperature);
705 payloadObject.addProperty("passenger_temp", passenegerTemperature);
706 sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
709 public void setCombinedTemperature(float temperature) {
710 setTemperature(temperature, temperature);
713 public void setDriverTemperature(float temperature) {
714 setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
717 public void setPassengerTemperature(float temperature) {
718 setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
721 public void openFrunk() {
722 JsonObject payloadObject = new JsonObject();
723 payloadObject.addProperty("which_trunk", "front");
724 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
727 public void openTrunk() {
728 JsonObject payloadObject = new JsonObject();
729 payloadObject.addProperty("which_trunk", "rear");
730 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
733 public void closeTrunk() {
737 public void setValetMode(boolean b, Integer pin) {
738 JsonObject payloadObject = new JsonObject();
739 payloadObject.addProperty("on", b);
741 payloadObject.addProperty("password", String.format("%04d", pin));
743 sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
746 public void resetValetPin() {
747 sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
750 public void setMaxRangeCharging(boolean b) {
751 sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
754 public void charge(boolean b) {
755 sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
758 public void flashLights() {
759 sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
762 public void honkHorn() {
763 sendCommand(COMMAND_HONK_HORN, account.commandTarget);
766 public void openChargePort() {
767 sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
770 public void lockDoors(boolean b) {
771 sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
774 public void autoConditioning(boolean b) {
775 sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
778 public void wakeUp() {
779 sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
782 public void setSteeringWheelHeater(boolean isOn) {
783 JsonObject payloadObject = new JsonObject();
784 payloadObject.addProperty("on", isOn);
785 sendCommand(COMMAND_STEERING_WHEEL_HEATER, gson.toJson(payloadObject), account.commandTarget);
788 protected Vehicle queryVehicle() {
789 String authHeader = account.getAuthHeader();
791 if (authHeader != null) {
793 // get a list of vehicles
794 synchronized (account.vehiclesTarget) {
795 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
796 .header("Authorization", authHeader).get();
798 logger.debug("Querying the vehicle, response : {}, {}", response.getStatus(),
799 response.getStatusInfo().getReasonPhrase());
801 if (!checkResponse(response, true)) {
802 logger.debug("An error occurred while querying the vehicle");
806 JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
807 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
809 for (Vehicle vehicle : vehicleArray) {
810 logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
811 if (vehicle.vin.equals(getConfig().get(VIN))) {
812 vehicleJSON = gson.toJson(vehicle);
813 parseAndUpdate("queryVehicle", null, vehicleJSON);
814 if (logger.isTraceEnabled()) {
815 logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
823 } catch (ProcessingException e) {
824 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
830 protected void queryVehicleAndUpdate() {
831 vehicle = queryVehicle();
834 public void parseAndUpdate(String request, String payLoad, String result) {
835 final double locationThreshold = .0000001;
838 if (request != null && result != null && !"null".equals(result)) {
839 updateStatus(ThingStatus.ONLINE);
840 updateState(CHANNEL_EVENTSTAMP, new DateTimeType());
841 // first, update state objects
842 if ("queryVehicle".equals(request)) {
843 if (vehicle != null) {
844 logger.debug("Vehicle state is {}", vehicle.state);
845 updateState(TeslaChannelSelector.STATE.getChannelID(), new StringType(vehicle.state));
847 logger.debug("Vehicle state is initializing or unknown");
851 if (vehicle != null && ("asleep".equals(vehicle.state) || "offline".equals(vehicle.state))) {
852 logger.debug("Vehicle is {}", vehicle.state);
856 if (vehicle != null && !lastState.equals(vehicle.state)) {
857 lastState = vehicle.state;
859 // in case vehicle changed to awake, refresh all data
861 logger.debug("Vehicle is now awake, updating all data");
862 lastLocationChangeTimestamp = System.currentTimeMillis();
863 lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
870 // reset timestamp if elapsed and set inactive to false
871 if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
872 .currentTimeMillis()) {
873 logger.debug("Vehicle did not fall asleep within sleep period, checking again");
876 boolean wasInactive = isInactive;
877 isInactive = !isCharging() && !notReadyForSleep();
879 if (!wasInactive && isInactive) {
880 lastStateTimestamp = System.currentTimeMillis();
881 logger.debug("Vehicle is inactive");
884 } else if ("vehicleData".equals(request)) {
885 VehicleData vehicleData = gson.fromJson(result, VehicleData.class);
886 if (vehicleData == null) {
887 logger.error("Not able to parse response '{}'", result);
891 driveState = vehicleData.drive_state;
892 if (Math.abs(lastLatitude - driveState.latitude) > locationThreshold
893 || Math.abs(lastLongitude - driveState.longitude) > locationThreshold) {
894 logger.debug("Vehicle moved, resetting last location timestamp");
896 lastLatitude = driveState.latitude;
897 lastLongitude = driveState.longitude;
898 lastLocationChangeTimestamp = System.currentTimeMillis();
900 logger.trace("Drive state: {}", driveState.shift_state);
902 if ((driveState.shift_state == null) && (lastValidDriveStateNotNull)) {
903 logger.debug("Set NULL shiftstate time");
904 lastValidDriveStateNotNull = false;
905 lastDriveStateChangeToNullTimestamp = System.currentTimeMillis();
906 } else if (driveState.shift_state != null) {
907 logger.trace("Clear NULL shiftstate time");
908 lastValidDriveStateNotNull = true;
911 guiState = vehicleData.gui_settings;
913 vehicleState = vehicleData.vehicle_state;
915 chargeState = vehicleData.charge_state;
917 updateState(CHANNEL_CHARGE, OnOffType.ON);
919 updateState(CHANNEL_CHARGE, OnOffType.OFF);
922 climateState = vehicleData.climate_state;
923 BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
924 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
925 updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
927 softwareUpdate = vehicleState.software_update;
932 Set<Map.Entry<String, JsonElement>> entrySet = new HashSet<>();
934 entrySet.addAll(gson.toJsonTree(driveState, DriveState.class).getAsJsonObject().entrySet());
935 entrySet.addAll(gson.toJsonTree(guiState, GUIState.class).getAsJsonObject().entrySet());
936 entrySet.addAll(gson.toJsonTree(vehicleState, VehicleState.class).getAsJsonObject().entrySet());
937 entrySet.addAll(gson.toJsonTree(chargeState, ChargeState.class).getAsJsonObject().entrySet());
938 entrySet.addAll(gson.toJsonTree(climateState, ClimateState.class).getAsJsonObject().entrySet());
940 gson.toJsonTree(softwareUpdate, SoftwareUpdate.class).getAsJsonObject().entrySet());
942 for (Map.Entry<String, JsonElement> entry : entrySet) {
944 TeslaChannelSelector selector = TeslaChannelSelector
945 .getValueSelectorFromRESTID(entry.getKey());
946 if (!selector.isProperty()) {
947 if (!entry.getValue().isJsonNull()) {
948 updateState(selector.getChannelID(), teslaChannelSelectorProxy
949 .getState(entry.getValue().getAsString(), selector, editProperties()));
950 if (logger.isTraceEnabled()) {
951 logger.trace("The variable/value pair '{}':'{}' is successfully processed",
952 entry.getKey(), entry.getValue());
955 updateState(selector.getChannelID(), UnDefType.UNDEF);
957 } else if (!entry.getValue().isJsonNull()) {
958 Map<String, String> properties = editProperties();
959 properties.put(selector.getChannelID(), entry.getValue().getAsString());
960 updateProperties(properties);
961 if (logger.isTraceEnabled()) {
963 "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
964 entry.getKey(), entry.getValue(), selector.getChannelID());
967 } catch (IllegalArgumentException e) {
968 logger.trace("The variable/value pair '{}':'{}' is not (yet) supported", entry.getKey(),
970 } catch (ClassCastException | IllegalStateException e) {
971 logger.trace("An exception occurred while converting the JSON data : '{}'",
976 if (softwareUpdate.version == null || softwareUpdate.version.isBlank()) {
977 updateState(CHANNEL_SOFTWARE_UPDATE_AVAILABLE, OnOffType.OFF);
979 updateState(CHANNEL_SOFTWARE_UPDATE_AVAILABLE, OnOffType.ON);
986 } catch (Exception p) {
987 logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
991 @SuppressWarnings("unchecked")
992 protected QuantityType<Temperature> commandToQuantityType(Command command) {
993 if (command instanceof QuantityType) {
994 return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
996 return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
999 protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
1000 return roundBigDecimal(quantity.toBigDecimal()).floatValue();
1003 protected BigDecimal roundBigDecimal(BigDecimal value) {
1004 return value.setScale(1, RoundingMode.HALF_EVEN);
1007 protected Runnable stateRunnable = () -> {
1009 queryVehicleAndUpdate();
1010 boolean allowQuery = allowQuery();
1014 } else if (allowWakeUp) {
1016 } else if (isAwake()) {
1017 logger.debug("Throttled state polling to allow sleep, occupied/idle, or in a console mode");
1019 lastAdvModesTimestamp = System.currentTimeMillis();
1021 } catch (Exception e) {
1022 logger.warn("Exception occurred in stateRunnable", e);
1026 protected Runnable eventRunnable = new Runnable() {
1027 TeslaEventEndpoint eventEndpoint;
1028 boolean isAuthenticated = false;
1029 long lastPingTimestamp = 0;
1033 eventEndpoint = new TeslaEventEndpoint(getThing().getUID(), webSocketFactory);
1034 eventEndpoint.addEventHandler(new TeslaEventEndpoint.EventHandler() {
1036 public void handleEvent(Event event) {
1037 if (event != null) {
1038 switch (event.msg_type) {
1039 case "control:hello":
1040 logger.debug("Event : Received hello");
1043 logger.debug("Event : Received an update: '{}'", event.value);
1045 String[] vals = event.value.split(",");
1046 long currentTimeStamp = Long.parseLong(vals[0]);
1047 long systemTimeStamp = System.currentTimeMillis();
1048 if (logger.isDebugEnabled()) {
1049 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
1050 logger.debug("STS {} CTS {} Delta {}",
1051 dateFormatter.format(new Date(systemTimeStamp)),
1052 dateFormatter.format(new Date(currentTimeStamp)),
1053 systemTimeStamp - currentTimeStamp);
1055 if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
1056 if (currentTimeStamp > lastTimeStamp) {
1057 lastTimeStamp = Long.parseLong(vals[0]);
1058 if (logger.isDebugEnabled()) {
1059 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1060 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1061 logger.debug("Event : Event stamp is {}",
1062 dateFormatter.format(new Date(lastTimeStamp)));
1064 for (int i = 0; i < EventKeys.values().length; i++) {
1065 TeslaChannelSelector selector = TeslaChannelSelector
1066 .getValueSelectorFromRESTID((EventKeys.values()[i]).toString());
1068 if (!selector.isProperty()) {
1069 State newState = teslaChannelSelectorProxy.getState(vals[i], selector,
1071 if (newState != null && !"".equals(vals[i])) {
1072 updateState(selector.getChannelID(), newState);
1074 updateState(selector.getChannelID(), UnDefType.UNDEF);
1076 if (logger.isTraceEnabled()) {
1078 "The variable/value pair '{}':'{}' is successfully processed",
1079 EventKeys.values()[i], vals[i]);
1082 Map<String, String> properties = editProperties();
1083 properties.put(selector.getChannelID(),
1084 (selector.getState(vals[i])).toString());
1085 updateProperties(properties);
1086 if (logger.isTraceEnabled()) {
1088 "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
1089 EventKeys.values()[i], vals[i], selector.getChannelID());
1093 } else if (logger.isDebugEnabled()) {
1094 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1095 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1097 "Event : Discarding an event with an out of sync timestamp {} (last is {})",
1098 dateFormatter.format(new Date(currentTimeStamp)),
1099 dateFormatter.format(new Date(lastTimeStamp)));
1102 if (logger.isDebugEnabled()) {
1103 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1104 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1106 "Event : Discarding an event that differs {} ms from the system time: {} (system is {})",
1107 systemTimeStamp - currentTimeStamp,
1108 dateFormatter.format(currentTimeStamp),
1109 dateFormatter.format(systemTimeStamp));
1111 if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
1112 logger.trace("Event : The event endpoint will be reset");
1113 eventEndpoint.closeConnection();
1118 logger.debug("Event : Received an error: '{}'/'{}'", event.value, event.error_type);
1119 eventEndpoint.closeConnection();
1128 if (getThing().getStatus() == ThingStatus.ONLINE) {
1130 eventEndpoint.connect(new URI(URI_EVENT));
1132 if (eventEndpoint.isConnected()) {
1133 if (!isAuthenticated) {
1134 logger.debug("Event : Authenticating vehicle {}", vehicle.vehicle_id);
1135 JsonObject payloadObject = new JsonObject();
1136 payloadObject.addProperty("msg_type", "data:subscribe_oauth");
1137 payloadObject.addProperty("token", account.getAccessToken());
1138 payloadObject.addProperty("value", Arrays.asList(EventKeys.values()).stream()
1139 .skip(1).map(Enum::toString).collect(Collectors.joining(",")));
1140 payloadObject.addProperty("tag", vehicle.vehicle_id);
1142 eventEndpoint.sendMessage(gson.toJson(payloadObject));
1143 isAuthenticated = true;
1145 lastPingTimestamp = System.nanoTime();
1148 if (TimeUnit.MILLISECONDS.convert(System.nanoTime() - lastPingTimestamp,
1149 TimeUnit.NANOSECONDS) > EVENT_PING_INTERVAL) {
1150 logger.trace("Event : Pinging the Tesla event stream infrastructure");
1151 eventEndpoint.ping();
1152 lastPingTimestamp = System.nanoTime();
1156 if (!eventEndpoint.isConnected()) {
1157 isAuthenticated = false;
1158 eventIntervalErrors++;
1159 if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
1161 "Event : Reached the maximum number of errors ({}) for the current interval ({} seconds)",
1162 EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
1163 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1164 eventEndpoint.closeConnection();
1167 if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
1168 * EVENT_ERROR_INTERVAL_SECONDS) {
1170 "Event : Resetting the error counter. ({} errors in the last interval)",
1171 eventIntervalErrors);
1172 eventIntervalTimestamp = System.currentTimeMillis();
1173 eventIntervalErrors = 0;
1177 logger.debug("Event : The vehicle is not awake");
1178 if (vehicle != null) {
1180 // wake up the vehicle until streaming token <> 0
1181 logger.debug("Event : Waking up the vehicle");
1185 vehicle = queryVehicle();
1189 } catch (URISyntaxException | NumberFormatException | IOException e) {
1190 logger.debug("Event : An exception occurred while processing events: '{}'", e.getMessage());
1194 Thread.sleep(EVENT_STREAM_PAUSE);
1195 } catch (InterruptedException e) {
1196 logger.debug("Event : An exception occurred while putting the event thread to sleep: '{}'",
1200 if (Thread.interrupted()) {
1201 logger.debug("Event : The event thread was interrupted");
1202 eventEndpoint.close();