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.math.BigDecimal;
18 import java.math.RoundingMode;
19 import java.text.SimpleDateFormat;
20 import java.util.Date;
21 import java.util.HashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.locks.ReentrantLock;
28 import javax.measure.quantity.Temperature;
29 import javax.ws.rs.ProcessingException;
30 import javax.ws.rs.client.Client;
31 import javax.ws.rs.client.ClientBuilder;
32 import javax.ws.rs.client.WebTarget;
33 import javax.ws.rs.core.MediaType;
34 import javax.ws.rs.core.Response;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.tesla.internal.TeslaBindingConstants;
38 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy;
39 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy.TeslaChannelSelector;
40 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Request;
41 import org.openhab.binding.tesla.internal.protocol.ChargeState;
42 import org.openhab.binding.tesla.internal.protocol.ClimateState;
43 import org.openhab.binding.tesla.internal.protocol.DriveState;
44 import org.openhab.binding.tesla.internal.protocol.GUIState;
45 import org.openhab.binding.tesla.internal.protocol.Vehicle;
46 import org.openhab.binding.tesla.internal.protocol.VehicleState;
47 import org.openhab.binding.tesla.internal.throttler.QueueChannelThrottler;
48 import org.openhab.binding.tesla.internal.throttler.Rate;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.IncreaseDecreaseType;
51 import org.openhab.core.library.types.OnOffType;
52 import org.openhab.core.library.types.PercentType;
53 import org.openhab.core.library.types.QuantityType;
54 import org.openhab.core.library.types.StringType;
55 import org.openhab.core.library.unit.SIUnits;
56 import org.openhab.core.library.unit.Units;
57 import org.openhab.core.thing.ChannelUID;
58 import org.openhab.core.thing.Thing;
59 import org.openhab.core.thing.ThingStatus;
60 import org.openhab.core.thing.ThingStatusDetail;
61 import org.openhab.core.thing.binding.BaseThingHandler;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.types.UnDefType;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
68 import com.google.gson.Gson;
69 import com.google.gson.JsonElement;
70 import com.google.gson.JsonObject;
71 import com.google.gson.JsonParser;
74 * The {@link TeslaVehicleHandler} is responsible for handling commands, which are sent
75 * to one of the channels of a specific vehicle.
77 * @author Karel Goderis - Initial contribution
78 * @author Kai Kreuzer - Refactored to use separate account handler and improved configuration options
80 public class TeslaVehicleHandler extends BaseThingHandler {
82 private static final int FAST_STATUS_REFRESH_INTERVAL = 15000;
83 private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
84 private static final int API_SLEEP_INTERVAL_MINUTES = 20;
85 private static final int MOVE_THRESHOLD_INTERVAL_MINUTES = 5;
87 private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
89 protected WebTarget eventTarget;
91 // Vehicle state variables
92 protected Vehicle vehicle;
93 protected String vehicleJSON;
94 protected DriveState driveState;
95 protected GUIState guiState;
96 protected VehicleState vehicleState;
97 protected ChargeState chargeState;
98 protected ClimateState climateState;
100 protected boolean allowWakeUp;
101 protected boolean allowWakeUpForCommands;
102 protected long lastTimeStamp;
103 protected long apiIntervalTimestamp;
104 protected int apiIntervalErrors;
105 protected long eventIntervalTimestamp;
106 protected int eventIntervalErrors;
107 protected ReentrantLock lock;
109 protected double lastLongitude;
110 protected double lastLatitude;
111 protected long lastLocationChangeTimestamp;
113 protected long lastStateTimestamp = System.currentTimeMillis();
114 protected String lastState = "";
115 protected boolean isInactive = false;
117 protected TeslaAccountHandler account;
119 protected QueueChannelThrottler stateThrottler;
120 protected ClientBuilder clientBuilder;
121 protected Client eventClient;
122 protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
123 protected Thread eventThread;
124 protected ScheduledFuture<?> fastStateJob;
125 protected ScheduledFuture<?> slowStateJob;
127 private final Gson gson = new Gson();
129 public TeslaVehicleHandler(Thing thing, ClientBuilder clientBuilder) {
131 this.clientBuilder = clientBuilder;
134 @SuppressWarnings("null")
136 public void initialize() {
137 logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
138 updateStatus(ThingStatus.UNKNOWN);
139 allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
140 allowWakeUpForCommands = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUPFORCOMMANDS);
142 // the streaming API seems to be broken - let's keep the code, if it comes back one day
143 // enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
145 account = (TeslaAccountHandler) getBridge().getHandler();
146 lock = new ReentrantLock();
147 scheduler.execute(() -> queryVehicleAndUpdate());
151 Map<Object, Rate> channels = new HashMap<>();
152 channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
153 channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
155 Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
156 Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
157 stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
158 stateThrottler.addRate(secondRate);
160 if (fastStateJob == null || fastStateJob.isCancelled()) {
161 fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
162 TimeUnit.MILLISECONDS);
165 if (slowStateJob == null || slowStateJob.isCancelled()) {
166 slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
167 TimeUnit.MILLISECONDS);
175 public void dispose() {
176 logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
179 if (fastStateJob != null && !fastStateJob.isCancelled()) {
180 fastStateJob.cancel(true);
184 if (slowStateJob != null && !slowStateJob.isCancelled()) {
185 slowStateJob.cancel(true);
189 if (eventThread != null && !eventThread.isInterrupted()) {
190 eventThread.interrupt();
197 if (eventClient != null) {
203 * Retrieves the unique vehicle id this handler is associated with
205 * @return the vehicle id
207 public String getVehicleId() {
208 if (vehicle != null) {
216 public void handleCommand(ChannelUID channelUID, Command command) {
217 logger.debug("handleCommand {} {}", channelUID, command);
218 String channelID = channelUID.getId();
219 TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
221 if (command instanceof RefreshType) {
223 logger.debug("Waking vehicle to refresh all data");
229 // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
230 // throttled so we are safe not to break the Tesla SLA
233 if (selector != null) {
234 if (!isAwake() && allowWakeUpForCommands) {
235 logger.debug("Waking vehicle to send command.");
241 case CHARGE_LIMIT_SOC: {
242 if (command instanceof PercentType) {
243 setChargeLimit(((PercentType) command).intValue());
244 } else if (command instanceof OnOffType && command == OnOffType.ON) {
246 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
248 } else if (command instanceof IncreaseDecreaseType
249 && command == IncreaseDecreaseType.INCREASE) {
250 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
251 } else if (command instanceof IncreaseDecreaseType
252 && command == IncreaseDecreaseType.DECREASE) {
253 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
259 if (command instanceof DecimalType) {
260 amps = ((DecimalType) command).intValue();
262 if (command instanceof QuantityType<?>) {
263 QuantityType<?> qamps = ((QuantityType<?>) command).toUnit(Units.AMPERE);
265 amps = qamps.intValue();
269 if (amps < 5 || amps > 32) {
270 logger.warn("Charging amps can only be set in a range of 5-32A, but not to {}A.",
274 setChargingAmps(amps);
277 case COMBINED_TEMP: {
278 QuantityType<Temperature> quantity = commandToQuantityType(command);
279 if (quantity != null) {
280 setCombinedTemperature(quanityToRoundedFloat(quantity));
285 QuantityType<Temperature> quantity = commandToQuantityType(command);
286 if (quantity != null) {
287 setDriverTemperature(quanityToRoundedFloat(quantity));
291 case PASSENGER_TEMP: {
292 QuantityType<Temperature> quantity = commandToQuantityType(command);
293 if (quantity != null) {
294 setPassengerTemperature(quanityToRoundedFloat(quantity));
299 if (command instanceof OnOffType) {
300 setSentryMode(command == OnOffType.ON);
304 case SUN_ROOF_STATE: {
305 if (command instanceof StringType) {
306 setSunroof(command.toString());
310 case CHARGE_TO_MAX: {
311 if (command instanceof OnOffType) {
312 if (((OnOffType) command) == OnOffType.ON) {
313 setMaxRangeCharging(true);
315 setMaxRangeCharging(false);
321 if (command instanceof OnOffType) {
322 if (((OnOffType) command) == OnOffType.ON) {
331 if (command instanceof OnOffType) {
332 if (((OnOffType) command) == OnOffType.ON) {
339 if (command instanceof OnOffType) {
340 if (((OnOffType) command) == OnOffType.ON) {
347 if (command instanceof OnOffType) {
348 if (((OnOffType) command) == OnOffType.ON) {
355 if (command instanceof OnOffType) {
356 if (((OnOffType) command) == OnOffType.ON) {
365 if (command instanceof OnOffType) {
366 if (((OnOffType) command) == OnOffType.ON) {
367 autoConditioning(true);
369 autoConditioning(false);
375 if (command instanceof OnOffType) {
376 if (((OnOffType) command) == OnOffType.ON) {
383 if (command instanceof OnOffType) {
384 if (((OnOffType) command) == OnOffType.ON) {
391 if (command instanceof OnOffType) {
392 if (((OnOffType) command) == OnOffType.ON) {
393 if (vehicleState.rt == 0) {
397 if (vehicleState.rt == 1) {
405 if (command instanceof OnOffType) {
406 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
407 if (((OnOffType) command) == OnOffType.ON) {
408 setValetMode(true, valetpin);
410 setValetMode(false, valetpin);
415 case RESET_VALET_PIN: {
416 if (command instanceof OnOffType) {
417 if (((OnOffType) command) == OnOffType.ON) {
427 } catch (IllegalArgumentException e) {
429 "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
430 channelID, command.toString());
436 public void sendCommand(String command, String payLoad, WebTarget target) {
437 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
438 Request request = account.newRequest(this, command, payLoad, target, allowWakeUpForCommands);
439 if (stateThrottler != null) {
440 stateThrottler.submit(COMMAND_THROTTLE, request);
445 public void sendCommand(String command) {
446 sendCommand(command, "{}");
449 public void sendCommand(String command, String payLoad) {
450 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
451 Request request = account.newRequest(this, command, payLoad, account.commandTarget, allowWakeUpForCommands);
452 if (stateThrottler != null) {
453 stateThrottler.submit(COMMAND_THROTTLE, request);
458 public void sendCommand(String command, WebTarget target) {
459 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
460 Request request = account.newRequest(this, command, "{}", target, allowWakeUpForCommands);
461 if (stateThrottler != null) {
462 stateThrottler.submit(COMMAND_THROTTLE, request);
467 public void requestData(String command, String payLoad) {
468 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
469 Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget, false);
470 if (stateThrottler != null) {
471 stateThrottler.submit(DATA_THROTTLE, request);
477 protected void updateStatus(ThingStatus status) {
478 super.updateStatus(status);
482 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
483 super.updateStatus(status, statusDetail);
487 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
488 super.updateStatus(status, statusDetail, description);
491 public void requestData(String command) {
492 requestData(command, null);
495 public void queryVehicle(String parameter) {
496 WebTarget target = account.vehicleTarget.path(parameter);
497 sendCommand(parameter, null, target);
500 public void requestAllData() {
501 requestData(DRIVE_STATE);
502 requestData(VEHICLE_STATE);
503 requestData(CHARGE_STATE);
504 requestData(CLIMATE_STATE);
505 requestData(GUI_STATE);
508 protected boolean isAwake() {
509 return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
512 protected boolean isInMotion() {
513 if (driveState != null) {
514 if (driveState.speed != null && driveState.shift_state != null) {
515 return !"Undefined".equals(driveState.speed)
516 && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
522 protected boolean isInactive() {
523 // vehicle is inactive in case
524 // - it does not charge
525 // - it has not moved in the observation period
526 return isInactive && !isCharging() && !hasMovedInSleepInterval();
529 protected boolean isCharging() {
530 return chargeState != null && "Charging".equals(chargeState.charging_state);
533 protected boolean hasMovedInSleepInterval() {
534 return lastLocationChangeTimestamp > (System.currentTimeMillis()
535 - (MOVE_THRESHOLD_INTERVAL_MINUTES * 60 * 1000));
538 protected boolean allowQuery() {
539 return (isAwake() && !isInactive());
542 protected void setActive() {
544 lastLocationChangeTimestamp = System.currentTimeMillis();
549 protected boolean checkResponse(Response response, boolean immediatelyFail) {
550 if (response != null && response.getStatus() == 200) {
552 } else if (response != null && response.getStatus() == 401) {
553 logger.debug("The access token has expired, trying to get a new one.");
554 account.authenticate();
558 if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
559 if (immediatelyFail) {
560 logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
562 logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
563 TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
564 TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
567 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
568 if (eventClient != null) {
571 } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
572 * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
573 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
574 apiIntervalTimestamp = System.currentTimeMillis();
575 apiIntervalErrors = 0;
582 public void setChargeLimit(int percent) {
583 JsonObject payloadObject = new JsonObject();
584 payloadObject.addProperty("percent", percent);
585 sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
586 requestData(CHARGE_STATE);
589 public void setChargingAmps(int amps) {
590 JsonObject payloadObject = new JsonObject();
591 payloadObject.addProperty("charging_amps", amps);
592 sendCommand(COMMAND_SET_CHARGING_AMPS, gson.toJson(payloadObject), account.commandTarget);
593 requestData(CHARGE_STATE);
596 public void setSentryMode(boolean b) {
597 JsonObject payloadObject = new JsonObject();
598 payloadObject.addProperty("on", b);
599 sendCommand(COMMAND_SET_SENTRY_MODE, gson.toJson(payloadObject), account.commandTarget);
600 requestData(VEHICLE_STATE);
603 public void setSunroof(String state) {
604 if (state.equals("vent") || state.equals("close")) {
605 JsonObject payloadObject = new JsonObject();
606 payloadObject.addProperty("state", state);
607 sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
608 requestData(VEHICLE_STATE);
610 logger.warn("Ignoring invalid command '{}' for sunroof.", state);
615 * Sets the driver and passenger temperatures.
617 * While setting different temperature values is supported by the API, in practice this does not always work
618 * reliably, possibly if the the
619 * only reliable method is to set the driver and passenger temperature to the same value
621 * @param driverTemperature in Celsius
622 * @param passenegerTemperature in Celsius
624 public void setTemperature(float driverTemperature, float passenegerTemperature) {
625 JsonObject payloadObject = new JsonObject();
626 payloadObject.addProperty("driver_temp", driverTemperature);
627 payloadObject.addProperty("passenger_temp", passenegerTemperature);
628 sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
629 requestData(CLIMATE_STATE);
632 public void setCombinedTemperature(float temperature) {
633 setTemperature(temperature, temperature);
636 public void setDriverTemperature(float temperature) {
637 setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
640 public void setPassengerTemperature(float temperature) {
641 setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
644 public void openFrunk() {
645 JsonObject payloadObject = new JsonObject();
646 payloadObject.addProperty("which_trunk", "front");
647 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
648 requestData(VEHICLE_STATE);
651 public void openTrunk() {
652 JsonObject payloadObject = new JsonObject();
653 payloadObject.addProperty("which_trunk", "rear");
654 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
655 requestData(VEHICLE_STATE);
658 public void closeTrunk() {
662 public void setValetMode(boolean b, Integer pin) {
663 JsonObject payloadObject = new JsonObject();
664 payloadObject.addProperty("on", b);
666 payloadObject.addProperty("password", String.format("%04d", pin));
668 sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
669 requestData(VEHICLE_STATE);
672 public void resetValetPin() {
673 sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
674 requestData(VEHICLE_STATE);
677 public void setMaxRangeCharging(boolean b) {
678 sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
679 requestData(CHARGE_STATE);
682 public void charge(boolean b) {
683 sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
684 requestData(CHARGE_STATE);
687 public void flashLights() {
688 sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
691 public void honkHorn() {
692 sendCommand(COMMAND_HONK_HORN, account.commandTarget);
695 public void openChargePort() {
696 sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
697 requestData(CHARGE_STATE);
700 public void lockDoors(boolean b) {
701 sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
702 requestData(VEHICLE_STATE);
705 public void autoConditioning(boolean b) {
706 sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
707 requestData(CLIMATE_STATE);
710 public void wakeUp() {
711 sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
714 protected synchronized Vehicle queryVehicle() {
715 String authHeader = account.getAuthHeader();
717 if (authHeader != null) {
719 // get a list of vehicles
720 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
721 .header("Authorization", authHeader).get();
723 logger.debug("Querying the vehicle, response : {}, {}", response.getStatus(),
724 response.getStatusInfo().getReasonPhrase());
726 if (!checkResponse(response, true)) {
727 logger.debug("An error occurred while querying the vehicle");
731 JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
732 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
734 for (Vehicle vehicle : vehicleArray) {
735 logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
736 if (vehicle.vin.equals(getConfig().get(VIN))) {
737 vehicleJSON = gson.toJson(vehicle);
738 parseAndUpdate("queryVehicle", null, vehicleJSON);
739 if (logger.isTraceEnabled()) {
740 logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
746 } catch (ProcessingException e) {
747 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
753 protected void queryVehicleAndUpdate() {
754 vehicle = queryVehicle();
755 if (vehicle != null) {
756 parseAndUpdate("queryVehicle", null, vehicleJSON);
760 public void parseAndUpdate(String request, String payLoad, String result) {
761 final Double LOCATION_THRESHOLD = .0000001;
763 JsonObject jsonObject = null;
766 if (request != null && result != null && !"null".equals(result)) {
767 updateStatus(ThingStatus.ONLINE);
768 // first, update state objects
771 driveState = gson.fromJson(result, DriveState.class);
773 if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
774 || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
775 logger.debug("Vehicle moved, resetting last location timestamp");
777 lastLatitude = driveState.latitude;
778 lastLongitude = driveState.longitude;
779 lastLocationChangeTimestamp = System.currentTimeMillis();
785 guiState = gson.fromJson(result, GUIState.class);
788 case VEHICLE_STATE: {
789 vehicleState = gson.fromJson(result, VehicleState.class);
793 chargeState = gson.fromJson(result, ChargeState.class);
795 updateState(CHANNEL_CHARGE, OnOffType.ON);
797 updateState(CHANNEL_CHARGE, OnOffType.OFF);
802 case CLIMATE_STATE: {
803 climateState = gson.fromJson(result, ClimateState.class);
804 BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
805 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
806 updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
809 case "queryVehicle": {
810 if (vehicle != null && !lastState.equals(vehicle.state)) {
811 lastState = vehicle.state;
813 // in case vehicle changed to awake, refresh all data
815 logger.debug("Vehicle is now awake, updating all data");
816 lastLocationChangeTimestamp = System.currentTimeMillis();
823 // reset timestamp if elapsed and set inactive to false
824 if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
825 .currentTimeMillis()) {
826 logger.debug("Vehicle did not fall asleep within sleep period, checking again");
829 boolean wasInactive = isInactive;
830 isInactive = !isCharging() && !hasMovedInSleepInterval();
832 if (!wasInactive && isInactive) {
833 lastStateTimestamp = System.currentTimeMillis();
834 logger.debug("Vehicle is inactive");
842 // secondly, reformat the response string to a JSON compliant
843 // object for some specific non-JSON compatible requests
845 case MOBILE_ENABLED_STATE: {
846 jsonObject = new JsonObject();
847 jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
851 jsonObject = JsonParser.parseString(result).getAsJsonObject();
857 // process the result
858 if (jsonObject != null && result != null && !"null".equals(result)) {
859 // deal with responses for "set" commands, which get confirmed
860 // positively, or negatively, in which case a reason for failure
862 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
863 boolean requestResult = jsonObject.get("result").getAsBoolean();
864 logger.debug("The request ({}) execution was {}, and reported '{}'", new Object[] { request,
865 requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString() });
867 Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
869 long resultTimeStamp = 0;
870 for (Map.Entry<String, JsonElement> entry : entrySet) {
871 if ("timestamp".equals(entry.getKey())) {
872 resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
873 if (logger.isTraceEnabled()) {
874 Date date = new Date(resultTimeStamp);
875 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
876 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
885 boolean proceed = true;
886 if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
891 for (Map.Entry<String, JsonElement> entry : entrySet) {
893 TeslaChannelSelector selector = TeslaChannelSelector
894 .getValueSelectorFromRESTID(entry.getKey());
895 if (!selector.isProperty()) {
896 if (!entry.getValue().isJsonNull()) {
897 updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
898 entry.getValue().getAsString(), selector, editProperties()));
899 if (logger.isTraceEnabled()) {
901 "The variable/value pair '{}':'{}' is successfully processed",
902 entry.getKey(), entry.getValue());
905 updateState(selector.getChannelID(), UnDefType.UNDEF);
908 if (!entry.getValue().isJsonNull()) {
909 Map<String, String> properties = editProperties();
910 properties.put(selector.getChannelID(), entry.getValue().getAsString());
911 updateProperties(properties);
912 if (logger.isTraceEnabled()) {
914 "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
915 entry.getKey(), entry.getValue(), selector.getChannelID());
919 } catch (IllegalArgumentException e) {
920 logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
921 entry.getKey(), entry.getValue());
922 } catch (ClassCastException | IllegalStateException e) {
923 logger.trace("An exception occurred while converting the JSON data : '{}'",
928 logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
936 } catch (Exception p) {
937 logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
941 @SuppressWarnings("unchecked")
942 protected QuantityType<Temperature> commandToQuantityType(Command command) {
943 if (command instanceof QuantityType) {
944 return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
946 return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
949 protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
950 return roundBigDecimal(quantity.toBigDecimal()).floatValue();
953 protected BigDecimal roundBigDecimal(BigDecimal value) {
954 return value.setScale(1, RoundingMode.HALF_EVEN);
957 protected Runnable slowStateRunnable = () -> {
959 queryVehicleAndUpdate();
961 boolean allowQuery = allowQuery();
964 requestData(CHARGE_STATE);
965 requestData(CLIMATE_STATE);
966 requestData(GUI_STATE);
967 queryVehicle(MOBILE_ENABLED_STATE);
973 logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
977 } catch (Exception e) {
978 logger.warn("Exception occurred in slowStateRunnable", e);
982 protected Runnable fastStateRunnable = () -> {
983 if (getThing().getStatus() == ThingStatus.ONLINE) {
984 boolean allowQuery = allowQuery();
987 requestData(DRIVE_STATE);
988 requestData(VEHICLE_STATE);
994 logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");