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.BufferedReader;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.InputStreamReader;
21 import java.math.BigDecimal;
22 import java.math.RoundingMode;
23 import java.text.SimpleDateFormat;
24 import java.util.Arrays;
25 import java.util.Date;
26 import java.util.HashMap;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.locks.ReentrantLock;
32 import java.util.stream.Collectors;
34 import javax.measure.quantity.Temperature;
35 import javax.ws.rs.ProcessingException;
36 import javax.ws.rs.client.Client;
37 import javax.ws.rs.client.ClientBuilder;
38 import javax.ws.rs.client.WebTarget;
39 import javax.ws.rs.core.MediaType;
40 import javax.ws.rs.core.Response;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.openhab.binding.tesla.internal.TeslaBindingConstants;
44 import org.openhab.binding.tesla.internal.TeslaBindingConstants.EventKeys;
45 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy;
46 import org.openhab.binding.tesla.internal.TeslaChannelSelectorProxy.TeslaChannelSelector;
47 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Authenticator;
48 import org.openhab.binding.tesla.internal.handler.TeslaAccountHandler.Request;
49 import org.openhab.binding.tesla.internal.protocol.ChargeState;
50 import org.openhab.binding.tesla.internal.protocol.ClimateState;
51 import org.openhab.binding.tesla.internal.protocol.DriveState;
52 import org.openhab.binding.tesla.internal.protocol.GUIState;
53 import org.openhab.binding.tesla.internal.protocol.Vehicle;
54 import org.openhab.binding.tesla.internal.protocol.VehicleState;
55 import org.openhab.binding.tesla.internal.throttler.QueueChannelThrottler;
56 import org.openhab.binding.tesla.internal.throttler.Rate;
57 import org.openhab.core.library.types.DecimalType;
58 import org.openhab.core.library.types.IncreaseDecreaseType;
59 import org.openhab.core.library.types.OnOffType;
60 import org.openhab.core.library.types.PercentType;
61 import org.openhab.core.library.types.QuantityType;
62 import org.openhab.core.library.types.StringType;
63 import org.openhab.core.library.unit.SIUnits;
64 import org.openhab.core.library.unit.Units;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.thing.Thing;
67 import org.openhab.core.thing.ThingStatus;
68 import org.openhab.core.thing.ThingStatusDetail;
69 import org.openhab.core.thing.binding.BaseThingHandler;
70 import org.openhab.core.types.Command;
71 import org.openhab.core.types.RefreshType;
72 import org.openhab.core.types.State;
73 import org.openhab.core.types.UnDefType;
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
77 import com.google.gson.Gson;
78 import com.google.gson.JsonElement;
79 import com.google.gson.JsonObject;
80 import com.google.gson.JsonParser;
83 * The {@link TeslaVehicleHandler} is responsible for handling commands, which are sent
84 * to one of the channels of a specific vehicle.
86 * @author Karel Goderis - Initial contribution
87 * @author Kai Kreuzer - Refactored to use separate account handler and improved configuration options
89 public class TeslaVehicleHandler extends BaseThingHandler {
91 private static final int EVENT_STREAM_PAUSE = 5000;
92 private static final int EVENT_TIMESTAMP_AGE_LIMIT = 3000;
93 private static final int EVENT_TIMESTAMP_MAX_DELTA = 10000;
94 private static final int FAST_STATUS_REFRESH_INTERVAL = 15000;
95 private static final int SLOW_STATUS_REFRESH_INTERVAL = 60000;
96 private static final int EVENT_MAXIMUM_ERRORS_IN_INTERVAL = 10;
97 private static final int EVENT_ERROR_INTERVAL_SECONDS = 15;
98 private static final int API_SLEEP_INTERVAL_MINUTES = 20;
99 private static final int MOVE_THRESHOLD_INTERVAL_MINUTES = 5;
101 private final Logger logger = LoggerFactory.getLogger(TeslaVehicleHandler.class);
103 protected WebTarget eventTarget;
105 // Vehicle state variables
106 protected Vehicle vehicle;
107 protected String vehicleJSON;
108 protected DriveState driveState;
109 protected GUIState guiState;
110 protected VehicleState vehicleState;
111 protected ChargeState chargeState;
112 protected ClimateState climateState;
114 protected boolean allowWakeUp;
115 protected boolean allowWakeUpForCommands;
116 protected boolean enableEvents = false;
117 protected long lastTimeStamp;
118 protected long apiIntervalTimestamp;
119 protected int apiIntervalErrors;
120 protected long eventIntervalTimestamp;
121 protected int eventIntervalErrors;
122 protected ReentrantLock lock;
124 protected double lastLongitude;
125 protected double lastLatitude;
126 protected long lastLocationChangeTimestamp;
128 protected long lastStateTimestamp = System.currentTimeMillis();
129 protected String lastState = "";
130 protected boolean isInactive = false;
132 protected TeslaAccountHandler account;
134 protected QueueChannelThrottler stateThrottler;
135 protected ClientBuilder clientBuilder;
136 protected Client eventClient;
137 protected TeslaChannelSelectorProxy teslaChannelSelectorProxy = new TeslaChannelSelectorProxy();
138 protected Thread eventThread;
139 protected ScheduledFuture<?> fastStateJob;
140 protected ScheduledFuture<?> slowStateJob;
142 private final Gson gson = new Gson();
144 public TeslaVehicleHandler(Thing thing, ClientBuilder clientBuilder) {
146 this.clientBuilder = clientBuilder;
149 @SuppressWarnings("null")
151 public void initialize() {
152 logger.trace("Initializing the Tesla handler for {}", getThing().getUID());
153 updateStatus(ThingStatus.UNKNOWN);
154 allowWakeUp = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUP);
155 allowWakeUpForCommands = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ALLOWWAKEUPFORCOMMANDS);
157 // the streaming API seems to be broken - let's keep the code, if it comes back one day
158 // enableEvents = (boolean) getConfig().get(TeslaBindingConstants.CONFIG_ENABLEEVENTS);
160 account = (TeslaAccountHandler) getBridge().getHandler();
161 lock = new ReentrantLock();
162 scheduler.execute(() -> queryVehicleAndUpdate());
166 Map<Object, Rate> channels = new HashMap<>();
167 channels.put(DATA_THROTTLE, new Rate(1, 1, TimeUnit.SECONDS));
168 channels.put(COMMAND_THROTTLE, new Rate(20, 1, TimeUnit.MINUTES));
170 Rate firstRate = new Rate(20, 1, TimeUnit.MINUTES);
171 Rate secondRate = new Rate(200, 10, TimeUnit.MINUTES);
172 stateThrottler = new QueueChannelThrottler(firstRate, scheduler, channels);
173 stateThrottler.addRate(secondRate);
175 if (fastStateJob == null || fastStateJob.isCancelled()) {
176 fastStateJob = scheduler.scheduleWithFixedDelay(fastStateRunnable, 0, FAST_STATUS_REFRESH_INTERVAL,
177 TimeUnit.MILLISECONDS);
180 if (slowStateJob == null || slowStateJob.isCancelled()) {
181 slowStateJob = scheduler.scheduleWithFixedDelay(slowStateRunnable, 0, SLOW_STATUS_REFRESH_INTERVAL,
182 TimeUnit.MILLISECONDS);
189 if (eventThread == null) {
190 eventThread = new Thread(eventRunnable, "openHAB-Tesla-Events-" + getThing().getUID());
197 public void dispose() {
198 logger.trace("Disposing the Tesla handler for {}", getThing().getUID());
201 if (fastStateJob != null && !fastStateJob.isCancelled()) {
202 fastStateJob.cancel(true);
206 if (slowStateJob != null && !slowStateJob.isCancelled()) {
207 slowStateJob.cancel(true);
211 if (eventThread != null && !eventThread.isInterrupted()) {
212 eventThread.interrupt();
219 if (eventClient != null) {
225 * Retrieves the unique vehicle id this handler is associated with
227 * @return the vehicle id
229 public String getVehicleId() {
230 if (vehicle != null) {
238 public void handleCommand(ChannelUID channelUID, Command command) {
239 logger.debug("handleCommand {} {}", channelUID, command);
240 String channelID = channelUID.getId();
241 TeslaChannelSelector selector = TeslaChannelSelector.getValueSelectorFromChannelID(channelID);
243 if (command instanceof RefreshType) {
245 logger.debug("Waking vehicle to refresh all data");
251 // Request the state of all known variables. This is sub-optimal, but the requests get scheduled and
252 // throttled so we are safe not to break the Tesla SLA
255 if (selector != null) {
256 if (!isAwake() && allowWakeUpForCommands) {
257 logger.debug("Waking vehicle to send command.");
263 case CHARGE_LIMIT_SOC: {
264 if (command instanceof PercentType) {
265 setChargeLimit(((PercentType) command).intValue());
266 } else if (command instanceof OnOffType && command == OnOffType.ON) {
268 } else if (command instanceof OnOffType && command == OnOffType.OFF) {
270 } else if (command instanceof IncreaseDecreaseType
271 && command == IncreaseDecreaseType.INCREASE) {
272 setChargeLimit(Math.min(chargeState.charge_limit_soc + 1, 100));
273 } else if (command instanceof IncreaseDecreaseType
274 && command == IncreaseDecreaseType.DECREASE) {
275 setChargeLimit(Math.max(chargeState.charge_limit_soc - 1, 0));
281 if (command instanceof DecimalType) {
282 amps = ((DecimalType) command).intValue();
284 if (command instanceof QuantityType<?>) {
285 QuantityType<?> qamps = ((QuantityType<?>) command).toUnit(Units.AMPERE);
287 amps = qamps.intValue();
291 if (amps < 5 || amps > 32) {
292 logger.warn("Charging amps can only be set in a range of 5-32A, but not to {}A.",
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) {
334 if (((OnOffType) command) == OnOffType.ON) {
335 setMaxRangeCharging(true);
337 setMaxRangeCharging(false);
343 if (command instanceof OnOffType) {
344 if (((OnOffType) command) == OnOffType.ON) {
353 if (command instanceof OnOffType) {
354 if (((OnOffType) command) == OnOffType.ON) {
361 if (command instanceof OnOffType) {
362 if (((OnOffType) command) == OnOffType.ON) {
369 if (command instanceof OnOffType) {
370 if (((OnOffType) command) == OnOffType.ON) {
377 if (command instanceof OnOffType) {
378 if (((OnOffType) command) == OnOffType.ON) {
387 if (command instanceof OnOffType) {
388 if (((OnOffType) command) == OnOffType.ON) {
389 autoConditioning(true);
391 autoConditioning(false);
397 if (command instanceof OnOffType) {
398 if (((OnOffType) command) == OnOffType.ON) {
405 if (command instanceof OnOffType) {
406 if (((OnOffType) command) == OnOffType.ON) {
413 if (command instanceof OnOffType) {
414 if (((OnOffType) command) == OnOffType.ON) {
415 if (vehicleState.rt == 0) {
419 if (vehicleState.rt == 1) {
427 if (command instanceof OnOffType) {
428 int valetpin = ((BigDecimal) getConfig().get(VALETPIN)).intValue();
429 if (((OnOffType) command) == OnOffType.ON) {
430 setValetMode(true, valetpin);
432 setValetMode(false, valetpin);
437 case RESET_VALET_PIN: {
438 if (command instanceof OnOffType) {
439 if (((OnOffType) command) == OnOffType.ON) {
449 } catch (IllegalArgumentException e) {
451 "An error occurred while trying to set the read-only variable associated with channel '{}' to '{}'",
452 channelID, command.toString());
458 public void sendCommand(String command, String payLoad, WebTarget target) {
459 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
460 Request request = account.newRequest(this, command, payLoad, target, allowWakeUpForCommands);
461 if (stateThrottler != null) {
462 stateThrottler.submit(COMMAND_THROTTLE, request);
467 public void sendCommand(String command) {
468 sendCommand(command, "{}");
471 public void sendCommand(String command, String payLoad) {
472 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
473 Request request = account.newRequest(this, command, payLoad, account.commandTarget, allowWakeUpForCommands);
474 if (stateThrottler != null) {
475 stateThrottler.submit(COMMAND_THROTTLE, request);
480 public void sendCommand(String command, WebTarget target) {
481 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
482 Request request = account.newRequest(this, command, "{}", target, allowWakeUpForCommands);
483 if (stateThrottler != null) {
484 stateThrottler.submit(COMMAND_THROTTLE, request);
489 public void requestData(String command, String payLoad) {
490 if (command.equals(COMMAND_WAKE_UP) || isAwake() || allowWakeUpForCommands) {
491 Request request = account.newRequest(this, command, payLoad, account.dataRequestTarget, false);
492 if (stateThrottler != null) {
493 stateThrottler.submit(DATA_THROTTLE, request);
499 protected void updateStatus(ThingStatus status) {
500 super.updateStatus(status);
504 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail) {
505 super.updateStatus(status, statusDetail);
509 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
510 super.updateStatus(status, statusDetail, description);
513 public void requestData(String command) {
514 requestData(command, null);
517 public void queryVehicle(String parameter) {
518 WebTarget target = account.vehicleTarget.path(parameter);
519 sendCommand(parameter, null, target);
522 public void requestAllData() {
523 requestData(DRIVE_STATE);
524 requestData(VEHICLE_STATE);
525 requestData(CHARGE_STATE);
526 requestData(CLIMATE_STATE);
527 requestData(GUI_STATE);
530 protected boolean isAwake() {
531 return vehicle != null && "online".equals(vehicle.state) && vehicle.vehicle_id != null;
534 protected boolean isInMotion() {
535 if (driveState != null) {
536 if (driveState.speed != null && driveState.shift_state != null) {
537 return !"Undefined".equals(driveState.speed)
538 && (!"P".equals(driveState.shift_state) || !"Undefined".equals(driveState.shift_state));
544 protected boolean isInactive() {
545 // vehicle is inactive in case
546 // - it does not charge
547 // - it has not moved in the observation period
548 return isInactive && !isCharging() && !hasMovedInSleepInterval();
551 protected boolean isCharging() {
552 return chargeState != null && "Charging".equals(chargeState.charging_state);
555 protected boolean hasMovedInSleepInterval() {
556 return lastLocationChangeTimestamp > (System.currentTimeMillis()
557 - (MOVE_THRESHOLD_INTERVAL_MINUTES * 60 * 1000));
560 protected boolean allowQuery() {
561 return (isAwake() && !isInactive());
564 protected void setActive() {
566 lastLocationChangeTimestamp = System.currentTimeMillis();
571 protected boolean checkResponse(Response response, boolean immediatelyFail) {
572 if (response != null && response.getStatus() == 200) {
576 if (immediatelyFail || apiIntervalErrors >= TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL) {
577 if (immediatelyFail) {
578 logger.warn("Got an unsuccessful result, setting vehicle to offline and will try again");
580 logger.warn("Reached the maximum number of errors ({}) for the current interval ({} seconds)",
581 TeslaAccountHandler.API_MAXIMUM_ERRORS_IN_INTERVAL,
582 TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS);
585 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
586 if (eventClient != null) {
589 } else if ((System.currentTimeMillis() - apiIntervalTimestamp) > 1000
590 * TeslaAccountHandler.API_ERROR_INTERVAL_SECONDS) {
591 logger.trace("Resetting the error counter. ({} errors in the last interval)", apiIntervalErrors);
592 apiIntervalTimestamp = System.currentTimeMillis();
593 apiIntervalErrors = 0;
600 public void setChargeLimit(int percent) {
601 JsonObject payloadObject = new JsonObject();
602 payloadObject.addProperty("percent", percent);
603 sendCommand(COMMAND_SET_CHARGE_LIMIT, gson.toJson(payloadObject), account.commandTarget);
604 requestData(CHARGE_STATE);
607 public void setChargingAmps(int amps) {
608 JsonObject payloadObject = new JsonObject();
609 payloadObject.addProperty("charging_amps", amps);
610 sendCommand(COMMAND_SET_CHARGING_AMPS, gson.toJson(payloadObject), account.commandTarget);
611 requestData(CHARGE_STATE);
614 public void setSentryMode(boolean b) {
615 JsonObject payloadObject = new JsonObject();
616 payloadObject.addProperty("on", b);
617 sendCommand(COMMAND_SET_SENTRY_MODE, gson.toJson(payloadObject), account.commandTarget);
618 requestData(VEHICLE_STATE);
621 public void setSunroof(String state) {
622 if (state.equals("vent") || state.equals("close")) {
623 JsonObject payloadObject = new JsonObject();
624 payloadObject.addProperty("state", state);
625 sendCommand(COMMAND_SUN_ROOF, gson.toJson(payloadObject), account.commandTarget);
626 requestData(VEHICLE_STATE);
628 logger.warn("Ignoring invalid command '{}' for sunroof.", state);
633 * Sets the driver and passenger temperatures.
635 * While setting different temperature values is supported by the API, in practice this does not always work
636 * reliably, possibly if the the
637 * only reliable method is to set the driver and passenger temperature to the same value
639 * @param driverTemperature in Celsius
640 * @param passenegerTemperature in Celsius
642 public void setTemperature(float driverTemperature, float passenegerTemperature) {
643 JsonObject payloadObject = new JsonObject();
644 payloadObject.addProperty("driver_temp", driverTemperature);
645 payloadObject.addProperty("passenger_temp", passenegerTemperature);
646 sendCommand(COMMAND_SET_TEMP, gson.toJson(payloadObject), account.commandTarget);
647 requestData(CLIMATE_STATE);
650 public void setCombinedTemperature(float temperature) {
651 setTemperature(temperature, temperature);
654 public void setDriverTemperature(float temperature) {
655 setTemperature(temperature, climateState != null ? climateState.passenger_temp_setting : temperature);
658 public void setPassengerTemperature(float temperature) {
659 setTemperature(climateState != null ? climateState.driver_temp_setting : temperature, temperature);
662 public void openFrunk() {
663 JsonObject payloadObject = new JsonObject();
664 payloadObject.addProperty("which_trunk", "front");
665 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
666 requestData(VEHICLE_STATE);
669 public void openTrunk() {
670 JsonObject payloadObject = new JsonObject();
671 payloadObject.addProperty("which_trunk", "rear");
672 sendCommand(COMMAND_ACTUATE_TRUNK, gson.toJson(payloadObject), account.commandTarget);
673 requestData(VEHICLE_STATE);
676 public void closeTrunk() {
680 public void setValetMode(boolean b, Integer pin) {
681 JsonObject payloadObject = new JsonObject();
682 payloadObject.addProperty("on", b);
684 payloadObject.addProperty("password", String.format("%04d", pin));
686 sendCommand(COMMAND_SET_VALET_MODE, gson.toJson(payloadObject), account.commandTarget);
687 requestData(VEHICLE_STATE);
690 public void resetValetPin() {
691 sendCommand(COMMAND_RESET_VALET_PIN, account.commandTarget);
692 requestData(VEHICLE_STATE);
695 public void setMaxRangeCharging(boolean b) {
696 sendCommand(b ? COMMAND_CHARGE_MAX : COMMAND_CHARGE_STD, account.commandTarget);
697 requestData(CHARGE_STATE);
700 public void charge(boolean b) {
701 sendCommand(b ? COMMAND_CHARGE_START : COMMAND_CHARGE_STOP, account.commandTarget);
702 requestData(CHARGE_STATE);
705 public void flashLights() {
706 sendCommand(COMMAND_FLASH_LIGHTS, account.commandTarget);
709 public void honkHorn() {
710 sendCommand(COMMAND_HONK_HORN, account.commandTarget);
713 public void openChargePort() {
714 sendCommand(COMMAND_OPEN_CHARGE_PORT, account.commandTarget);
715 requestData(CHARGE_STATE);
718 public void lockDoors(boolean b) {
719 sendCommand(b ? COMMAND_DOOR_LOCK : COMMAND_DOOR_UNLOCK, account.commandTarget);
720 requestData(VEHICLE_STATE);
723 public void autoConditioning(boolean b) {
724 sendCommand(b ? COMMAND_AUTO_COND_START : COMMAND_AUTO_COND_STOP, account.commandTarget);
725 requestData(CLIMATE_STATE);
728 public void wakeUp() {
729 sendCommand(COMMAND_WAKE_UP, account.wakeUpTarget);
732 protected Vehicle queryVehicle() {
733 String authHeader = account.getAuthHeader();
735 if (authHeader != null) {
737 // get a list of vehicles
738 Response response = account.vehiclesTarget.request(MediaType.APPLICATION_JSON_TYPE)
739 .header("Authorization", authHeader).get();
741 logger.debug("Querying the vehicle, response : {}, {}", response.getStatus(),
742 response.getStatusInfo().getReasonPhrase());
744 if (!checkResponse(response, true)) {
745 logger.error("An error occurred while querying the vehicle");
749 JsonObject jsonObject = JsonParser.parseString(response.readEntity(String.class)).getAsJsonObject();
750 Vehicle[] vehicleArray = gson.fromJson(jsonObject.getAsJsonArray("response"), Vehicle[].class);
752 for (Vehicle vehicle : vehicleArray) {
753 logger.debug("Querying the vehicle: VIN {}", vehicle.vin);
754 if (vehicle.vin.equals(getConfig().get(VIN))) {
755 vehicleJSON = gson.toJson(vehicle);
756 parseAndUpdate("queryVehicle", null, vehicleJSON);
757 if (logger.isTraceEnabled()) {
758 logger.trace("Vehicle is id {}/vehicle_id {}/tokens {}", vehicle.id, vehicle.vehicle_id,
764 } catch (ProcessingException e) {
765 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
771 protected void queryVehicleAndUpdate() {
772 vehicle = queryVehicle();
773 if (vehicle != null) {
774 parseAndUpdate("queryVehicle", null, vehicleJSON);
778 public void parseAndUpdate(String request, String payLoad, String result) {
779 final Double LOCATION_THRESHOLD = .0000001;
781 JsonObject jsonObject = null;
784 if (request != null && result != null && !"null".equals(result)) {
785 updateStatus(ThingStatus.ONLINE);
786 // first, update state objects
789 driveState = gson.fromJson(result, DriveState.class);
791 if (Math.abs(lastLatitude - driveState.latitude) > LOCATION_THRESHOLD
792 || Math.abs(lastLongitude - driveState.longitude) > LOCATION_THRESHOLD) {
793 logger.debug("Vehicle moved, resetting last location timestamp");
795 lastLatitude = driveState.latitude;
796 lastLongitude = driveState.longitude;
797 lastLocationChangeTimestamp = System.currentTimeMillis();
803 guiState = gson.fromJson(result, GUIState.class);
806 case VEHICLE_STATE: {
807 vehicleState = gson.fromJson(result, VehicleState.class);
811 chargeState = gson.fromJson(result, ChargeState.class);
813 updateState(CHANNEL_CHARGE, OnOffType.ON);
815 updateState(CHANNEL_CHARGE, OnOffType.OFF);
820 case CLIMATE_STATE: {
821 climateState = gson.fromJson(result, ClimateState.class);
822 BigDecimal avgtemp = roundBigDecimal(new BigDecimal(
823 (climateState.driver_temp_setting + climateState.passenger_temp_setting) / 2.0f));
824 updateState(CHANNEL_COMBINED_TEMP, new QuantityType<>(avgtemp, SIUnits.CELSIUS));
827 case "queryVehicle": {
828 if (vehicle != null && !lastState.equals(vehicle.state)) {
829 lastState = vehicle.state;
831 // in case vehicle changed to awake, refresh all data
833 logger.debug("Vehicle is now awake, updating all data");
834 lastLocationChangeTimestamp = System.currentTimeMillis();
841 // reset timestamp if elapsed and set inactive to false
842 if (isInactive && lastStateTimestamp + (API_SLEEP_INTERVAL_MINUTES * 60 * 1000) < System
843 .currentTimeMillis()) {
844 logger.debug("Vehicle did not fall asleep within sleep period, checking again");
847 boolean wasInactive = isInactive;
848 isInactive = !isCharging() && !hasMovedInSleepInterval();
850 if (!wasInactive && isInactive) {
851 lastStateTimestamp = System.currentTimeMillis();
852 logger.debug("Vehicle is inactive");
860 // secondly, reformat the response string to a JSON compliant
861 // object for some specific non-JSON compatible requests
863 case MOBILE_ENABLED_STATE: {
864 jsonObject = new JsonObject();
865 jsonObject.addProperty(MOBILE_ENABLED_STATE, result);
869 jsonObject = JsonParser.parseString(result).getAsJsonObject();
875 // process the result
876 if (jsonObject != null && result != null && !"null".equals(result)) {
877 // deal with responses for "set" commands, which get confirmed
878 // positively, or negatively, in which case a reason for failure
880 if (jsonObject.get("reason") != null && jsonObject.get("reason").getAsString() != null) {
881 boolean requestResult = jsonObject.get("result").getAsBoolean();
882 logger.debug("The request ({}) execution was {}, and reported '{}'", new Object[] { request,
883 requestResult ? "successful" : "not successful", jsonObject.get("reason").getAsString() });
885 Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
887 long resultTimeStamp = 0;
888 for (Map.Entry<String, JsonElement> entry : entrySet) {
889 if ("timestamp".equals(entry.getKey())) {
890 resultTimeStamp = Long.valueOf(entry.getValue().getAsString());
891 if (logger.isTraceEnabled()) {
892 Date date = new Date(resultTimeStamp);
893 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
894 logger.trace("The request result timestamp is {}", dateFormatter.format(date));
903 boolean proceed = true;
904 if (resultTimeStamp < lastTimeStamp && request == DRIVE_STATE) {
909 for (Map.Entry<String, JsonElement> entry : entrySet) {
911 TeslaChannelSelector selector = TeslaChannelSelector
912 .getValueSelectorFromRESTID(entry.getKey());
913 if (!selector.isProperty()) {
914 if (!entry.getValue().isJsonNull()) {
915 updateState(selector.getChannelID(), teslaChannelSelectorProxy.getState(
916 entry.getValue().getAsString(), selector, editProperties()));
917 if (logger.isTraceEnabled()) {
919 "The variable/value pair '{}':'{}' is successfully processed",
920 entry.getKey(), entry.getValue());
923 updateState(selector.getChannelID(), UnDefType.UNDEF);
926 if (!entry.getValue().isJsonNull()) {
927 Map<String, String> properties = editProperties();
928 properties.put(selector.getChannelID(), entry.getValue().getAsString());
929 updateProperties(properties);
930 if (logger.isTraceEnabled()) {
932 "The variable/value pair '{}':'{}' is successfully used to set property '{}'",
933 entry.getKey(), entry.getValue(), selector.getChannelID());
937 } catch (IllegalArgumentException e) {
938 logger.trace("The variable/value pair '{}':'{}' is not (yet) supported",
939 entry.getKey(), entry.getValue());
940 } catch (ClassCastException | IllegalStateException e) {
941 logger.trace("An exception occurred while converting the JSON data : '{}'",
946 logger.warn("The result for request '{}' is discarded due to an out of sync timestamp",
954 } catch (Exception p) {
955 logger.error("An exception occurred while parsing data received from the vehicle: '{}'", p.getMessage());
959 @SuppressWarnings("unchecked")
960 protected QuantityType<Temperature> commandToQuantityType(Command command) {
961 if (command instanceof QuantityType) {
962 return ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
964 return new QuantityType<>(new BigDecimal(command.toString()), SIUnits.CELSIUS);
967 protected float quanityToRoundedFloat(QuantityType<Temperature> quantity) {
968 return roundBigDecimal(quantity.toBigDecimal()).floatValue();
971 protected BigDecimal roundBigDecimal(BigDecimal value) {
972 return value.setScale(1, RoundingMode.HALF_EVEN);
975 protected Runnable slowStateRunnable = () -> {
977 queryVehicleAndUpdate();
979 boolean allowQuery = allowQuery();
982 requestData(CHARGE_STATE);
983 requestData(CLIMATE_STATE);
984 requestData(GUI_STATE);
985 queryVehicle(MOBILE_ENABLED_STATE);
991 logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
995 } catch (Exception e) {
996 logger.warn("Exception occurred in slowStateRunnable", e);
1000 protected Runnable fastStateRunnable = () -> {
1001 if (getThing().getStatus() == ThingStatus.ONLINE) {
1002 boolean allowQuery = allowQuery();
1005 requestData(DRIVE_STATE);
1006 requestData(VEHICLE_STATE);
1012 logger.debug("Vehicle is neither charging nor moving, skipping updates to allow it to sleep");
1019 protected Runnable eventRunnable = new Runnable() {
1020 Response eventResponse;
1021 BufferedReader eventBufferedReader;
1022 InputStreamReader eventInputStreamReader;
1023 boolean isEstablished = false;
1025 protected boolean establishEventStream() {
1027 if (!isEstablished) {
1028 eventBufferedReader = null;
1030 eventClient = clientBuilder.build()
1031 .register(new Authenticator((String) getConfig().get(CONFIG_USERNAME), vehicle.tokens[0]));
1032 eventTarget = eventClient.target(URI_EVENT).path(vehicle.vehicle_id + "/").queryParam("values",
1033 Arrays.asList(EventKeys.values()).stream().skip(1).map(Enum::toString)
1034 .collect(Collectors.joining(",")));
1035 eventResponse = eventTarget.request(MediaType.TEXT_PLAIN_TYPE).get();
1037 logger.debug("Event Stream: Establishing the event stream: Response: {}:{}",
1038 eventResponse.getStatus(), eventResponse.getStatusInfo());
1040 if (eventResponse.getStatus() == 200) {
1041 InputStream dummy = (InputStream) eventResponse.getEntity();
1042 eventInputStreamReader = new InputStreamReader(dummy);
1043 eventBufferedReader = new BufferedReader(eventInputStreamReader);
1044 isEstablished = true;
1045 } else if (eventResponse.getStatus() == 401) {
1046 isEstablished = false;
1048 isEstablished = false;
1051 if (!isEstablished) {
1052 eventIntervalErrors++;
1053 if (eventIntervalErrors >= EVENT_MAXIMUM_ERRORS_IN_INTERVAL) {
1055 "Reached the maximum number of errors ({}) for the current interval ({} seconds)",
1056 EVENT_MAXIMUM_ERRORS_IN_INTERVAL, EVENT_ERROR_INTERVAL_SECONDS);
1057 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
1058 eventClient.close();
1061 if ((System.currentTimeMillis() - eventIntervalTimestamp) > 1000
1062 * EVENT_ERROR_INTERVAL_SECONDS) {
1063 logger.trace("Resetting the error counter. ({} errors in the last interval)",
1064 eventIntervalErrors);
1065 eventIntervalTimestamp = System.currentTimeMillis();
1066 eventIntervalErrors = 0;
1070 } catch (Exception e) {
1072 "Event stream: An exception occurred while establishing the event stream for the vehicle: '{}'",
1074 isEstablished = false;
1077 return isEstablished;
1084 if (getThing().getStatus() == ThingStatus.ONLINE) {
1086 if (establishEventStream()) {
1087 String line = eventBufferedReader.readLine();
1089 while (line != null) {
1090 logger.debug("Event stream: Received an event: '{}'", line);
1091 String vals[] = line.split(",");
1092 long currentTimeStamp = Long.valueOf(vals[0]);
1093 long systemTimeStamp = System.currentTimeMillis();
1094 if (logger.isDebugEnabled()) {
1095 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1096 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1097 logger.debug("STS {} CTS {} Delta {}",
1098 dateFormatter.format(new Date(systemTimeStamp)),
1099 dateFormatter.format(new Date(currentTimeStamp)),
1100 systemTimeStamp - currentTimeStamp);
1102 if (systemTimeStamp - currentTimeStamp < EVENT_TIMESTAMP_AGE_LIMIT) {
1103 if (currentTimeStamp > lastTimeStamp) {
1104 lastTimeStamp = Long.valueOf(vals[0]);
1105 if (logger.isDebugEnabled()) {
1106 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1107 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1108 logger.debug("Event Stream: Event stamp is {}",
1109 dateFormatter.format(new Date(lastTimeStamp)));
1111 for (int i = 0; i < EventKeys.values().length; i++) {
1112 TeslaChannelSelector selector = TeslaChannelSelector
1113 .getValueSelectorFromRESTID((EventKeys.values()[i]).toString());
1114 if (!selector.isProperty()) {
1115 State newState = teslaChannelSelectorProxy.getState(vals[i],
1116 selector, editProperties());
1117 if (newState != null && !"".equals(vals[i])) {
1118 updateState(selector.getChannelID(), newState);
1120 updateState(selector.getChannelID(), UnDefType.UNDEF);
1123 Map<String, String> properties = editProperties();
1124 properties.put(selector.getChannelID(),
1125 (selector.getState(vals[i])).toString());
1126 updateProperties(properties);
1130 if (logger.isDebugEnabled()) {
1131 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1132 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1134 "Event stream: Discarding an event with an out of sync timestamp {} (last is {})",
1135 dateFormatter.format(new Date(currentTimeStamp)),
1136 dateFormatter.format(new Date(lastTimeStamp)));
1140 if (logger.isDebugEnabled()) {
1141 SimpleDateFormat dateFormatter = new SimpleDateFormat(
1142 "yyyy-MM-dd'T'HH:mm:ss.SSS");
1144 "Event Stream: Discarding an event that differs {} ms from the system time: {} (system is {})",
1145 systemTimeStamp - currentTimeStamp,
1146 dateFormatter.format(currentTimeStamp),
1147 dateFormatter.format(systemTimeStamp));
1149 if (systemTimeStamp - currentTimeStamp > EVENT_TIMESTAMP_MAX_DELTA) {
1150 logger.trace("Event stream: The event stream will be reset");
1151 isEstablished = false;
1154 line = eventBufferedReader.readLine();
1156 logger.trace("Event stream: The end of stream was reached");
1157 isEstablished = false;
1160 logger.debug("Event stream: The vehicle is not awake");
1161 if (vehicle != null) {
1163 // wake up the vehicle until streaming token <> 0
1164 logger.debug("Event stream: Waking up the vehicle");
1168 vehicle = queryVehicle();
1170 Thread.sleep(EVENT_STREAM_PAUSE);
1173 } catch (IOException | NumberFormatException e) {
1174 logger.debug("Event stream: An exception occurred while reading events: '{}'", e.getMessage());
1175 isEstablished = false;
1176 } catch (InterruptedException e) {
1177 isEstablished = false;
1180 if (Thread.interrupted()) {
1181 logger.debug("Event stream: the event stream was interrupted");