2 * Copyright (c) 2010-2022 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 = 5;
93 private static final int EVENT_MAXIMUM_ERRORS_IN_INTERVAL = 10;
94 private static final int EVENT_ERROR_INTERVAL_SECONDS = 15;
95 private static final int EVENT_STREAM_PAUSE = 3000;
96 private static final int EVENT_TIMESTAMP_AGE_LIMIT = 3000;
97 private static final int EVENT_TIMESTAMP_MAX_DELTA = 10000;
98 private static final int EVENT_PING_INTERVAL = 10000;
100 private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
102 // Vehicle state variables
103 protected Vehicle vehicle;
104 protected String vehicleJSON;
105 protected DriveState driveState;
106 protected GUIState guiState;
107 protected VehicleState vehicleState;
108 protected ChargeState chargeState;
109 protected ClimateState climateState;
111 protected boolean allowWakeUp;
112 protected boolean allowWakeUpForCommands;
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 TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
133 protected Thread eventThread;
134 protected ScheduledFuture<?> fastStateJob;
135 protected ScheduledFuture<?> slowStateJob;
136 protected WebSocketFactory webSocketFactory;
138 private final Gson gson = new Gson();
140 public TeslaVehicleHandler(Thing thing, WebSocketFactory webSocketFactory) {
142 this.webSocketFactory = webSocketFactory;
145 @SuppressWarnings("null")
147 public void initialize() {
148 logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
149 updateStatus(ThingStatus.UNKNOWN);
150 allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
151 allowWakeUpForCommands = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUPFORCOMMANDS);
152 enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
154 account = (TeslaAccountHandler) getBridge().getHandler();
155 lock = new ReentrantLock();
156 scheduler.execute(() -> queryVehicleAndUpdate());
160 Map<Object, Rate> channels = new HashMap<>();
161 channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
162 channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
164 Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
165 Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
166 stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
167 stateThrottler.addRate(secondRate);
169 if (fastStateJob == null || fastStateJob.isCancelled()) {
170 fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
171 TimeUnit.MILLISECONDS);
174 if (slowStateJob == null || slowStateJob.isCancelled()) {
175 slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
176 TimeUnit.MILLISECONDS);
180 if (eventThread == null) {
181 eventThread = new Thread(eventRunnable, "openHAB-Tesla-Events-" + getThing().getUID());
192 public void dispose() {
193 logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
196 if (fastStateJob != null && !fastStateJob.isCancelled()) {
197 fastStateJob.cancel(true);
201 if (slowStateJob != null && !slowStateJob.isCancelled()) {
202 slowStateJob.cancel(true);
206 if (eventThread != null && !eventThread.isInterrupted()) {
207 eventThread.interrupt();
216 * Retrieves the unique vehicle id this handler is associated with
218 * @return the vehicle id
220 public String getVehicleId() {
221 if (vehicle != null) {
229 public void handleCommand(ChannelUID channelUID, Command command) {
230 logger.debug("handleCommand {} {}", channelUID, command);
231 String channelID = channelUID.getId();
232 TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
234 if (command instanceof RefreshType) {
236 logger.debug("Waking vehicle to refresh all data");
242 // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
243 // throttled so we are safe not to break the Tesla SLA
246 if (selector != null) {
247 if (!isAwake() && allowWakeUpForCommands) {
248 logger.debug("Waking vehicle to send command.");
254 case CHARGE_LIMIT_SOC: {
255 if (command instanceof PercentType) {
256 setChargeLimit(((PercentType) command).intValue());
257 } else if (command instanceof OnOffType && command == OnOffType.ON) {
259 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
261 } else if (command instanceof IncreaseDecreaseType
262 && command == IncreaseDecreaseType.INCREASE) {
263 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
264 } else if (command instanceof IncreaseDecreaseType
265 && command == IncreaseDecreaseType.DECREASE) {
266 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
272 if (command instanceof DecimalType) {
273 amps = ((DecimalType) command).intValue();
275 if (command instanceof QuantityType<?>) {
276 QuantityType<?> qamps = ((QuantityType<?>) command).toUnit(Units.AMPERE);
278 amps = qamps.intValue();
282 if (amps < 5 || amps > 32) {
283 logger.warn("Charging amps can only be set in a range of 5-32A, but not to {}A.",
287 setChargingAmps(amps);
290 case COMBINED_TEMP: {
291 QuantityType<Temperature> quantity = commandToQuantityType(command);
292 if (quantity != null) {
293 setCombinedTemperature(quanityToRoundedFloat(quantity));
298 QuantityType<Temperature> quantity = commandToQuantityType(command);
299 if (quantity != null) {
300 setDriverTemperature(quanityToRoundedFloat(quantity));
304 case PASSENGER_TEMP: {
305 QuantityType<Temperature> quantity = commandToQuantityType(command);
306 if (quantity != null) {
307 setPassengerTemperature(quanityToRoundedFloat(quantity));
312 if (command instanceof OnOffType) {
313 setSentryMode(command == OnOffType.ON);
317 case SUN_ROOF_STATE: {
318 if (command instanceof StringType) {
319 setSunroof(command.toString());
323 case CHARGE_TO_MAX: {
324 if (command instanceof OnOffType) {
325 if (((OnOffType) command) == OnOffType.ON) {
326 setMaxRangeCharging(true);
328 setMaxRangeCharging(false);
334 if (command instanceof OnOffType) {
335 if (((OnOffType) command) == OnOffType.ON) {
344 if (command instanceof OnOffType) {
345 if (((OnOffType) command) == OnOffType.ON) {
352 if (command instanceof OnOffType) {
353 if (((OnOffType) command) == OnOffType.ON) {
360 if (command instanceof OnOffType) {
361 if (((OnOffType) command) == OnOffType.ON) {
368 if (command instanceof OnOffType) {
369 if (((OnOffType) command) == OnOffType.ON) {
378 if (command instanceof OnOffType) {
379 if (((OnOffType) command) == OnOffType.ON) {
380 autoConditioning(true);
382 autoConditioning(false);
388 if (command instanceof OnOffType) {
389 if (((OnOffType) command) == OnOffType.ON) {
396 if (command instanceof OnOffType) {
397 if (((OnOffType) command) == OnOffType.ON) {
404 if (command instanceof OnOffType) {
405 if (((OnOffType) command) == OnOffType.ON) {
406 if (vehicleState.rt == 0) {
410 if (vehicleState.rt == 1) {
418 if (command instanceof OnOffType) {
419 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
420 if (((OnOffType) command) == OnOffType.ON) {
421 setValetMode(true, valetpin);
423 setValetMode(false, valetpin);
428 case RESET_VALET_PIN: {
429 if (command instanceof OnOffType) {
430 if (((OnOffType) command) == OnOffType.ON) {
440 } catch (IllegalArgumentException e) {
442 "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
443 channelID, command.toString());
449 public void sendCommand(String command, String payLoad, WebTarget target) {
450 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
451 Request request = account.newRequest(this, command, payLoad, target, allowWakeUpForCommands);
452 if (stateThrottler != null) {
453 stateThrottler.submit(COMMAND_THROTTLE, request);
458 public void sendCommand(String command) {
459 sendCommand(command, "{}");
462 public void sendCommand(String command, String payLoad) {
463 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
464 Request request = account.newRequest(this, command, payLoad, account.commandTarget, allowWakeUpForCommands);
465 if (stateThrottler != null) {
466 stateThrottler.submit(COMMAND_THROTTLE, request);
471 public void sendCommand(String command, WebTarget target) {
472 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
473 Request request = account.newRequest(this, command, "{}", target, allowWakeUpForCommands);
474 if (stateThrottler != null) {
475 stateThrottler.submit(COMMAND_THROTTLE, request);
480 public void requestData(String command, String payLoad) {
481 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
482 Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget, false);
483 if (stateThrottler != null) {
484 stateThrottler.submit(DATA_THROTTLE, request);
490 protected void updateStatus(ThingStatus status) {
491 super.updateStatus(status);
495 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
496 super.updateStatus(status, statusDetail);
500 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
501 super.updateStatus(status, statusDetail, description);
504 public void requestData(String command) {
505 requestData(command, null);
508 public void queryVehicle(String parameter) {
509 WebTarget target = account.vehicleTarget.path(parameter);
510 sendCommand(parameter, null, target);
513 public void requestAllData() {
514 requestData(DRIVE_STATE);
515 requestData(VEHICLE_STATE);
516 requestData(CHARGE_STATE);
517 requestData(CLIMATE_STATE);
518 requestData(GUI_STATE);
521 protected boolean isAwake() {
522 return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
525 protected boolean isInMotion() {
526 if (driveState != null) {
527 if (driveState.speed != null && driveState.shift_state != null) {
528 return !"Undefined".equals(driveState.speed)
529 && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
535 protected boolean isInactive() {
536 // vehicle is inactive in case
537 // - it does not charge
538 // - it has not moved in the observation period
539 return isInactive && !isCharging() && !hasMovedInSleepInterval();
542 protected boolean isCharging() {
543 return chargeState != null && "Charging".equals(chargeState.charging_state);
546 protected boolean hasMovedInSleepInterval() {
547 return lastLocationChangeTimestamp > (System.currentTimeMillis()
548 - (MOVE_THRESHOLD_INTERVAL_MINUTES * 60 * 1000));
551 protected boolean allowQuery() {
552 return (isAwake() && !isInactive());
555 protected void setActive() {
557 lastLocationChangeTimestamp = System.currentTimeMillis();
562 protected boolean checkResponse(Response response, boolean immediatelyFail) {
563 if (response != null && response.getStatus() == 200) {
565 } else if (response != null && response.getStatus() == 401) {
566 logger.debug("The access token has expired, trying to get a new one.");
567 account.authenticate();
571 if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
572 if (immediatelyFail) {
573 logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
575 logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
576 TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
577 TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
580 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
581 } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
582 * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
583 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
584 apiIntervalTimestamp = System.currentTimeMillis();
585 apiIntervalErrors = 0;
592 public void setChargeLimit(int percent) {
593 JsonObject payloadObject = new JsonObject();
594 payloadObject.addProperty("percent", percent);
595 sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
596 requestData(CHARGE_STATE);
599 public void setChargingAmps(int amps) {
600 JsonObject payloadObject = new JsonObject();
601 payloadObject.addProperty("charging_amps", amps);
602 sendCommand(COMMAND_SET_CHARGING_AMPS, gson.toJson(payloadObject), account.commandTarget);
603 requestData(CHARGE_STATE);
606 public void setSentryMode(boolean b) {
607 JsonObject payloadObject = new JsonObject();
608 payloadObject.addProperty("on", b);
609 sendCommand(COMMAND_SET_SENTRY_MODE, gson.toJson(payloadObject), account.commandTarget);
610 requestData(VEHICLE_STATE);
613 public void setSunroof(String state) {
614 if (state.equals("vent") || state.equals("close")) {
615 JsonObject payloadObject = new JsonObject();
616 payloadObject.addProperty("state", state);
617 sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
618 requestData(VEHICLE_STATE);
620 logger.warn("Ignoring invalid command '{}' for sunroof.", state);
625 * Sets the driver and passenger temperatures.
627 * While setting different temperature values is supported by the API, in practice this does not always work
628 * reliably, possibly if the the
629 * only reliable method is to set the driver and passenger temperature to the same value
631 * @param driverTemperature in Celsius
632 * @param passenegerTemperature in Celsius
634 public void setTemperature(float driverTemperature, float passenegerTemperature) {
635 JsonObject payloadObject = new JsonObject();
636 payloadObject.addProperty("driver_temp", driverTemperature);
637 payloadObject.addProperty("passenger_temp", passenegerTemperature);
638 sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
639 requestData(CLIMATE_STATE);
642 public void setCombinedTemperature(float temperature) {
643 setTemperature(temperature, temperature);
646 public void setDriverTemperature(float temperature) {
647 setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
650 public void setPassengerTemperature(float temperature) {
651 setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
654 public void openFrunk() {
655 JsonObject payloadObject = new JsonObject();
656 payloadObject.addProperty("which_trunk", "front");
657 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
658 requestData(VEHICLE_STATE);
661 public void openTrunk() {
662 JsonObject payloadObject = new JsonObject();
663 payloadObject.addProperty("which_trunk", "rear");
664 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
665 requestData(VEHICLE_STATE);
668 public void closeTrunk() {
672 public void setValetMode(boolean b, Integer pin) {
673 JsonObject payloadObject = new JsonObject();
674 payloadObject.addProperty("on", b);
676 payloadObject.addProperty("password", String.format("%04d", pin));
678 sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
679 requestData(VEHICLE_STATE);
682 public void resetValetPin() {
683 sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
684 requestData(VEHICLE_STATE);
687 public void setMaxRangeCharging(boolean b) {
688 sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
689 requestData(CHARGE_STATE);
692 public void charge(boolean b) {
693 sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
694 requestData(CHARGE_STATE);
697 public void flashLights() {
698 sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
701 public void honkHorn() {
702 sendCommand(COMMAND_HONK_HORN, account.commandTarget);
705 public void openChargePort() {
706 sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
707 requestData(CHARGE_STATE);
710 public void lockDoors(boolean b) {
711 sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
712 requestData(VEHICLE_STATE);
715 public void autoConditioning(boolean b) {
716 sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
717 requestData(CLIMATE_STATE);
720 public void wakeUp() {
721 sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
724 protected Vehicle queryVehicle() {
725 String authHeader = account.getAuthHeader();
727 if (authHeader != null) {
729 // get a list of vehicles
730 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
731 .header("Authorization", authHeader).get();
733 logger.debug("Querying the vehicle, response : {}, {}", response.getStatus(),
734 response.getStatusInfo().getReasonPhrase());
736 if (!checkResponse(response, true)) {
737 logger.debug("An error occurred while querying the vehicle");
741 JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
742 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
744 for (Vehicle vehicle : vehicleArray) {
745 logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
746 if (vehicle.vin.equals(getConfig().get(VIN))) {
747 vehicleJSON = gson.toJson(vehicle);
748 parseAndUpdate("queryVehicle", null, vehicleJSON);
749 if (logger.isTraceEnabled()) {
750 logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
756 } catch (ProcessingException e) {
757 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
763 protected void queryVehicleAndUpdate() {
764 vehicle = queryVehicle();
765 if (vehicle != null) {
766 parseAndUpdate("queryVehicle", null, vehicleJSON);
770 public void parseAndUpdate(String request, String payLoad, String result) {
771 final Double LOCATION_THRESHOLD = .0000001;
773 JsonObject jsonObject = null;
776 if (request != null && result != null && !"null".equals(result)) {
777 updateStatus(ThingStatus.ONLINE);
778 // first, update state objects
781 driveState = gson.fromJson(result, DriveState.class);
783 if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
784 || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
785 logger.debug("Vehicle moved, resetting last location timestamp");
787 lastLatitude = driveState.latitude;
788 lastLongitude = driveState.longitude;
789 lastLocationChangeTimestamp = System.currentTimeMillis();
795 guiState = gson.fromJson(result, GUIState.class);
798 case VEHICLE_STATE: {
799 vehicleState = gson.fromJson(result, VehicleState.class);
803 chargeState = gson.fromJson(result, ChargeState.class);
805 updateState(CHANNEL_CHARGE, OnOffType.ON);
807 updateState(CHANNEL_CHARGE, OnOffType.OFF);
812 case CLIMATE_STATE: {
813 climateState = gson.fromJson(result, ClimateState.class);
814 BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
815 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
816 updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
819 case "queryVehicle": {
820 if (vehicle != null && !lastState.equals(vehicle.state)) {
821 lastState = vehicle.state;
823 // in case vehicle changed to awake, refresh all data
825 logger.debug("Vehicle is now awake, updating all data");
826 lastLocationChangeTimestamp = System.currentTimeMillis();
833 // reset timestamp if elapsed and set inactive to false
834 if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
835 .currentTimeMillis()) {
836 logger.debug("Vehicle did not fall asleep within sleep period, checking again");
839 boolean wasInactive = isInactive;
840 isInactive = !isCharging() && !hasMovedInSleepInterval();
842 if (!wasInactive && isInactive) {
843 lastStateTimestamp = System.currentTimeMillis();
844 logger.debug("Vehicle is inactive");
852 // secondly, reformat the response string to a JSON compliant
853 // object for some specific non-JSON compatible requests
855 case MOBILE_ENABLED_STATE: {
856 jsonObject = new JsonObject();
857 jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
861 jsonObject = JsonParser.parseString(result).getAsJsonObject();
867 // process the result
868 if (jsonObject != null && result != null && !"null".equals(result)) {
869 // deal with responses for "set" commands, which get confirmed
870 // positively, or negatively, in which case a reason for failure
872 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
873 boolean requestResult = jsonObject.get("result").getAsBoolean();
874 logger.debug("The request ({}) execution was {}, and reported '{}'", new Object[] { request,
875 requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString() });
877 Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
879 long resultTimeStamp = 0;
880 for (Map.Entry<String, JsonElement> entry : entrySet) {
881 if ("timestamp".equals(entry.getKey())) {
882 resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
883 if (logger.isTraceEnabled()) {
884 Date date = new Date(resultTimeStamp);
885 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
886 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
895 boolean proceed = true;
896 if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
901 for (Map.Entry<String, JsonElement> entry : entrySet) {
903 TeslaChannelSelector selector = TeslaChannelSelector
904 .getValueSelectorFromRESTID(entry.getKey());
905 if (!selector.isProperty()) {
906 if (!entry.getValue().isJsonNull()) {
907 updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
908 entry.getValue().getAsString(), selector, editProperties()));
909 if (logger.isTraceEnabled()) {
911 "The variable/value pair '{}':'{}' is successfully processed",
912 entry.getKey(), entry.getValue());
915 updateState(selector.getChannelID(), UnDefType.UNDEF);
918 if (!entry.getValue().isJsonNull()) {
919 Map<String, String> properties = editProperties();
920 properties.put(selector.getChannelID(), entry.getValue().getAsString());
921 updateProperties(properties);
922 if (logger.isTraceEnabled()) {
924 "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
925 entry.getKey(), entry.getValue(), selector.getChannelID());
929 } catch (IllegalArgumentException e) {
930 logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
931 entry.getKey(), entry.getValue());
932 } catch (ClassCastException | IllegalStateException e) {
933 logger.trace("An exception occurred while converting the JSON data : '{}'",
938 logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
946 } catch (Exception p) {
947 logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
951 @SuppressWarnings("unchecked")
952 protected QuantityType<Temperature> commandToQuantityType(Command command) {
953 if (command instanceof QuantityType) {
954 return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
956 return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
959 protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
960 return roundBigDecimal(quantity.toBigDecimal()).floatValue();
963 protected BigDecimal roundBigDecimal(BigDecimal value) {
964 return value.setScale(1, RoundingMode.HALF_EVEN);
967 protected Runnable slowStateRunnable = () -> {
969 queryVehicleAndUpdate();
971 boolean allowQuery = allowQuery();
974 requestData(CHARGE_STATE);
975 requestData(CLIMATE_STATE);
976 requestData(GUI_STATE);
977 queryVehicle(MOBILE_ENABLED_STATE);
983 logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
987 } catch (Exception e) {
988 logger.warn("Exception occurred in slowStateRunnable", e);
992 protected Runnable fastStateRunnable = () -> {
993 if (getThing().getStatus() == ThingStatus.ONLINE) {
994 boolean allowQuery = allowQuery();
997 requestData(DRIVE_STATE);
998 requestData(VEHICLE_STATE);
1004 logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
1011 protected Runnable eventRunnable = new Runnable() {
1012 TeslaEventEndpoint eventEndpoint;
1013 boolean isAuthenticated = false;
1014 long lastPingTimestamp = 0;
1018 eventEndpoint = new TeslaEventEndpoint(webSocketFactory);
1019 eventEndpoint.addEventHandler(new TeslaEventEndpoint.EventHandler() {
1021 public void handleEvent(Event event) {
1022 if (event != null) {
1023 switch (event.msg_type) {
1024 case "control:hello":
1025 logger.debug("Event : Received hello");
1028 logger.debug("Event : Received an update: '{}'", event.value);
1030 String vals[] = event.value.split(",");
1031 long currentTimeStamp = Long.valueOf(vals[0]);
1032 long systemTimeStamp = System.currentTimeMillis();
1033 if (logger.isDebugEnabled()) {
1034 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
1035 logger.debug("STS {} CTS {} Delta {}",
1036 dateFormatter.format(new Date(systemTimeStamp)),
1037 dateFormatter.format(new Date(currentTimeStamp)),
1038 systemTimeStamp - currentTimeStamp);
1040 if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
1041 if (currentTimeStamp > lastTimeStamp) {
1042 lastTimeStamp = Long.valueOf(vals[0]);
1043 if (logger.isDebugEnabled()) {
1044 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1045 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1046 logger.debug("Event : Event stamp is {}",
1047 dateFormatter.format(new Date(lastTimeStamp)));
1049 for (int i = 0; i < EventKeys.values().length; i++) {
1050 TeslaChannelSelector selector = TeslaChannelSelector
1051 .getValueSelectorFromRESTID((EventKeys.values()[i]).toString());
1053 if (!selector.isProperty()) {
1054 State newState = teslaChannelSelectorProxy.getState(vals[i], selector,
1056 if (newState != null && !"".equals(vals[i])) {
1057 updateState(selector.getChannelID(), newState);
1059 updateState(selector.getChannelID(), UnDefType.UNDEF);
1061 if (logger.isTraceEnabled()) {
1063 "The variable/value pair '{}':'{}' is successfully processed",
1064 EventKeys.values()[i], vals[i]);
1067 Map<String, String> properties = editProperties();
1068 properties.put(selector.getChannelID(),
1069 (selector.getState(vals[i])).toString());
1070 updateProperties(properties);
1071 if (logger.isTraceEnabled()) {
1073 "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
1074 EventKeys.values()[i], vals[i], selector.getChannelID());
1079 if (logger.isDebugEnabled()) {
1080 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1081 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1083 "Event : Discarding an event with an out of sync timestamp {} (last is {})",
1084 dateFormatter.format(new Date(currentTimeStamp)),
1085 dateFormatter.format(new Date(lastTimeStamp)));
1089 if (logger.isDebugEnabled()) {
1090 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1091 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1093 "Event : Discarding an event that differs {} ms from the system time: {} (system is {})",
1094 systemTimeStamp - currentTimeStamp,
1095 dateFormatter.format(currentTimeStamp),
1096 dateFormatter.format(systemTimeStamp));
1098 if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
1099 logger.trace("Event : The event endpoint will be reset");
1100 eventEndpoint.close();
1105 logger.debug("Event : Received an error: '{}'/'{}'", event.value, event.error_type);
1106 eventEndpoint.close();
1115 if (getThing().getStatus() == ThingStatus.ONLINE) {
1117 eventEndpoint.connect(new URI(URI_EVENT));
1119 if (eventEndpoint.isConnected()) {
1120 if (!isAuthenticated) {
1121 logger.debug("Event : Authenticating vehicle {}", vehicle.vehicle_id);
1122 JsonObject payloadObject = new JsonObject();
1123 payloadObject.addProperty("msg_type", "data:subscribe_oauth");
1124 payloadObject.addProperty("token", account.getAccessToken());
1125 payloadObject.addProperty("value", Arrays.asList(EventKeys.values()).stream()
1126 .skip(1).map(Enum::toString).collect(Collectors.joining(",")));
1127 payloadObject.addProperty("tag", vehicle.vehicle_id);
1129 eventEndpoint.sendMessage(gson.toJson(payloadObject));
1130 isAuthenticated = true;
1132 lastPingTimestamp = System.nanoTime();
1135 if (TimeUnit.MILLISECONDS.convert(System.nanoTime() - lastPingTimestamp,
1136 TimeUnit.NANOSECONDS) > EVENT_PING_INTERVAL) {
1137 logger.trace("Event : Pinging the Tesla event stream infrastructure");
1138 eventEndpoint.ping();
1139 lastPingTimestamp = System.nanoTime();
1143 if (!eventEndpoint.isConnected()) {
1144 isAuthenticated = false;
1145 eventIntervalErrors++;
1146 if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
1148 "Event : Reached the maximum number of errors ({}) for the current interval ({} seconds)",
1149 EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
1150 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1151 eventEndpoint.close();
1154 if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
1155 * EVENT_ERROR_INTERVAL_SECONDS) {
1157 "Event : Resetting the error counter. ({} errors in the last interval)",
1158 eventIntervalErrors);
1159 eventIntervalTimestamp = System.currentTimeMillis();
1160 eventIntervalErrors = 0;
1164 logger.debug("Event : The vehicle is not awake");
1165 if (vehicle != null) {
1167 // wake up the vehicle until streaming token <> 0
1168 logger.debug("Event : Waking up the vehicle");
1172 vehicle = queryVehicle();
1176 } catch (URISyntaxException | NumberFormatException | IOException e) {
1177 logger.debug("Event : An exception occurred while processing events: '{}'", e.getMessage());
1181 Thread.sleep(EVENT_STREAM_PAUSE);
1182 } catch (InterruptedException e) {
1183 logger.debug("Event : An exception occurred while putting the event thread to sleep: '{}'",
1187 if (Thread.interrupted()) {
1188 logger.debug("Event : The event thread was interrupted");