]> git.basschouten.com Git - openhab-addons.git/blob
3a21f01b3054cc7badc9860b5356ceae2a89345d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.volvooncall.internal.handler;
14
15 import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.KILO;
17 import static org.openhab.core.library.unit.SIUnits.*;
18 import static org.openhab.core.library.unit.SmartHomeUnits.*;
19
20 import java.time.ZonedDateTime;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Stack;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.stream.Collectors;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
34 import org.openhab.binding.volvooncall.internal.action.VolvoOnCallActions;
35 import org.openhab.binding.volvooncall.internal.api.ActionResultController;
36 import org.openhab.binding.volvooncall.internal.api.VocHttpApi;
37 import org.openhab.binding.volvooncall.internal.config.VehicleConfiguration;
38 import org.openhab.binding.volvooncall.internal.dto.Attributes;
39 import org.openhab.binding.volvooncall.internal.dto.DoorsStatus;
40 import org.openhab.binding.volvooncall.internal.dto.Heater;
41 import org.openhab.binding.volvooncall.internal.dto.HvBattery;
42 import org.openhab.binding.volvooncall.internal.dto.Position;
43 import org.openhab.binding.volvooncall.internal.dto.PostResponse;
44 import org.openhab.binding.volvooncall.internal.dto.Status;
45 import org.openhab.binding.volvooncall.internal.dto.Trip;
46 import org.openhab.binding.volvooncall.internal.dto.TripDetail;
47 import org.openhab.binding.volvooncall.internal.dto.Trips;
48 import org.openhab.binding.volvooncall.internal.dto.TyrePressure;
49 import org.openhab.binding.volvooncall.internal.dto.Vehicles;
50 import org.openhab.binding.volvooncall.internal.dto.WindowsStatus;
51 import org.openhab.binding.volvooncall.internal.wrapper.VehiclePositionWrapper;
52 import org.openhab.core.library.types.DateTimeType;
53 import org.openhab.core.library.types.DecimalType;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.library.types.QuantityType;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.thing.Bridge;
58 import org.openhab.core.thing.Channel;
59 import org.openhab.core.thing.ChannelUID;
60 import org.openhab.core.thing.Thing;
61 import org.openhab.core.thing.ThingStatus;
62 import org.openhab.core.thing.ThingStatusDetail;
63 import org.openhab.core.thing.ThingStatusInfo;
64 import org.openhab.core.thing.binding.BaseThingHandler;
65 import org.openhab.core.thing.binding.ThingHandler;
66 import org.openhab.core.thing.binding.ThingHandlerService;
67 import org.openhab.core.types.Command;
68 import org.openhab.core.types.RefreshType;
69 import org.openhab.core.types.State;
70 import org.openhab.core.types.UnDefType;
71 import org.slf4j.Logger;
72 import org.slf4j.LoggerFactory;
73
74 /**
75  * The {@link VehicleHandler} is responsible for handling commands, which are sent
76  * to one of the channels.
77  *
78  * @author GaĆ«l L'hopital - Initial contribution
79  */
80 @NonNullByDefault
81 public class VehicleHandler extends BaseThingHandler {
82     private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
83     private final Map<String, String> activeOptions = new HashMap<>();
84     private @Nullable ScheduledFuture<?> refreshJob;
85     private final List<ScheduledFuture<?>> pendingActions = new Stack<>();
86
87     private Vehicles vehicle = new Vehicles();
88     private VehiclePositionWrapper vehiclePosition = new VehiclePositionWrapper(new Position());
89     private Status vehicleStatus = new Status();
90     private @NonNullByDefault({}) VehicleConfiguration configuration;
91     private @NonNullByDefault({}) VolvoOnCallBridgeHandler bridgeHandler;
92     private long lastTripId;
93
94     public VehicleHandler(Thing thing, VehicleStateDescriptionProvider stateDescriptionProvider) {
95         super(thing);
96     }
97
98     @Override
99     public void initialize() {
100         logger.trace("Initializing the Volvo On Call handler for {}", getThing().getUID());
101
102         Bridge bridge = getBridge();
103         initializeBridge(bridge == null ? null : bridge.getHandler(), bridge == null ? null : bridge.getStatus());
104     }
105
106     @Override
107     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
108         logger.debug("bridgeStatusChanged {} for thing {}", bridgeStatusInfo, getThing().getUID());
109
110         Bridge bridge = getBridge();
111         initializeBridge(bridge == null ? null : bridge.getHandler(), bridgeStatusInfo.getStatus());
112     }
113
114     private void initializeBridge(@Nullable ThingHandler thingHandler, @Nullable ThingStatus bridgeStatus) {
115         logger.debug("initializeBridge {} for thing {}", bridgeStatus, getThing().getUID());
116
117         if (thingHandler != null && bridgeStatus != null) {
118             bridgeHandler = (VolvoOnCallBridgeHandler) thingHandler;
119             if (bridgeStatus == ThingStatus.ONLINE) {
120                 configuration = getConfigAs(VehicleConfiguration.class);
121                 VocHttpApi api = bridgeHandler.getApi();
122                 if (api != null) {
123                     try {
124                         vehicle = api.getURL("vehicles/" + configuration.vin, Vehicles.class);
125                         if (thing.getProperties().isEmpty()) {
126                             Map<String, String> properties = discoverAttributes(api);
127                             updateProperties(properties);
128                         }
129
130                         activeOptions.putAll(
131                                 thing.getProperties().entrySet().stream().filter(p -> "true".equals(p.getValue()))
132                                         .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
133
134                         String lastTripIdString = thing.getProperties().get(LAST_TRIP_ID);
135                         if (lastTripIdString != null) {
136                             lastTripId = Long.parseLong(lastTripIdString);
137                         }
138
139                         updateStatus(ThingStatus.ONLINE);
140                         startAutomaticRefresh(configuration.refresh, api);
141                     } catch (VolvoOnCallException e) {
142                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage());
143                     }
144
145                 }
146             } else {
147                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
148             }
149         } else {
150             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
151         }
152     }
153
154     private Map<String, String> discoverAttributes(VocHttpApi service) throws VolvoOnCallException {
155         Attributes attributes = service.getURL(vehicle.attributesURL, Attributes.class);
156
157         Map<String, String> properties = new HashMap<>();
158         properties.put(CAR_LOCATOR, attributes.carLocatorSupported.toString());
159         properties.put(HONK_AND_OR_BLINK, Boolean.toString(attributes.honkAndBlinkSupported
160                 && attributes.honkAndBlinkVersionsSupported.contains(HONK_AND_OR_BLINK)));
161         properties.put(HONK_BLINK, Boolean.toString(
162                 attributes.honkAndBlinkSupported && attributes.honkAndBlinkVersionsSupported.contains(HONK_BLINK)));
163         properties.put(REMOTE_HEATER, attributes.remoteHeaterSupported.toString());
164         properties.put(UNLOCK, attributes.unlockSupported.toString());
165         properties.put(LOCK, attributes.lockSupported.toString());
166         properties.put(JOURNAL_LOG, Boolean.toString(attributes.journalLogSupported && attributes.journalLogEnabled));
167         properties.put(PRECLIMATIZATION, attributes.preclimatizationSupported.toString());
168         properties.put(ENGINE_START, attributes.engineStartSupported.toString());
169         properties.put(UNLOCK_TIME, attributes.unlockTimeFrame.toString());
170
171         return properties;
172     }
173
174     /**
175      * Start the job refreshing the vehicle data
176      *
177      * @param refresh : refresh frequency in minutes
178      * @param service
179      */
180     private void startAutomaticRefresh(int refresh, VocHttpApi service) {
181         ScheduledFuture<?> refreshJob = this.refreshJob;
182         if (refreshJob == null || refreshJob.isCancelled()) {
183             this.refreshJob = scheduler.scheduleWithFixedDelay(() -> queryApiAndUpdateChannels(service), 1, refresh,
184                     TimeUnit.MINUTES);
185         }
186     }
187
188     private void queryApiAndUpdateChannels(VocHttpApi service) {
189         try {
190             Status newVehicleStatus = service.getURL(vehicle.statusURL, Status.class);
191             vehiclePosition = new VehiclePositionWrapper(service.getURL(Position.class, configuration.vin));
192             // Update all channels from the updated data
193             getThing().getChannels().stream().map(Channel::getUID)
194                     .filter(channelUID -> isLinked(channelUID) && !LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
195                     .forEach(channelUID -> {
196                         String groupID = channelUID.getGroupId();
197                         if (groupID != null) {
198                             State state = getValue(groupID, channelUID.getIdWithoutGroup(), newVehicleStatus,
199                                     vehiclePosition);
200                             updateState(channelUID, state);
201                         }
202                     });
203             if (newVehicleStatus.odometer != vehicleStatus.odometer) {
204                 triggerChannel(GROUP_OTHER + "#" + CAR_EVENT, EVENT_CAR_MOVED);
205                 // We will update trips only if car position has changed to save server queries
206                 updateTrips(service);
207             }
208             if (!vehicleStatus.getEngineRunning().equals(newVehicleStatus.getEngineRunning())
209                     && newVehicleStatus.getEngineRunning().get() == OnOffType.ON) {
210                 triggerChannel(GROUP_OTHER + "#" + CAR_EVENT, EVENT_CAR_STARTED);
211             }
212             vehicleStatus = newVehicleStatus;
213         } catch (VolvoOnCallException e) {
214             logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
215             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
216             freeRefreshJob();
217             startAutomaticRefresh(configuration.refresh, service);
218         }
219     }
220
221     private void freeRefreshJob() {
222         ScheduledFuture<?> refreshJob = this.refreshJob;
223         if (refreshJob != null) {
224             refreshJob.cancel(true);
225             this.refreshJob = null;
226         }
227         pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));
228     }
229
230     @Override
231     public void dispose() {
232         freeRefreshJob();
233         super.dispose();
234     }
235
236     private void updateTrips(VocHttpApi service) throws VolvoOnCallException {
237         // This seems to rewind 100 days by default, did not find any way to filter it
238         Trips carTrips = service.getURL(Trips.class, configuration.vin);
239         List<Trip> newTrips = carTrips.trips.stream().filter(trip -> trip.id >= lastTripId)
240                 .collect(Collectors.toList());
241         Collections.reverse(newTrips);
242
243         logger.debug("Trips discovered : {}", newTrips.size());
244
245         if (!newTrips.isEmpty()) {
246             Long newTripId = newTrips.get(newTrips.size() - 1).id;
247             if (newTripId > lastTripId) {
248                 updateProperty(LAST_TRIP_ID, newTripId.toString());
249                 triggerChannel(GROUP_OTHER + "#" + CAR_EVENT, EVENT_CAR_STOPPED);
250                 lastTripId = newTripId;
251             }
252
253             newTrips.stream().map(t -> t.tripDetails.get(0)).forEach(catchUpTrip -> {
254                 logger.debug("Trip found {}", catchUpTrip.getStartTime());
255                 getThing().getChannels().stream().map(Channel::getUID)
256                         .filter(channelUID -> isLinked(channelUID) && LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
257                         .forEach(channelUID -> {
258                             State state = getTripValue(channelUID.getIdWithoutGroup(), catchUpTrip);
259                             updateState(channelUID, state);
260                         });
261             });
262         }
263     }
264
265     @Override
266     public void handleCommand(ChannelUID channelUID, Command command) {
267         String channelID = channelUID.getIdWithoutGroup();
268         if (command instanceof RefreshType) {
269             VocHttpApi api = bridgeHandler.getApi();
270             if (api != null) {
271                 queryApiAndUpdateChannels(api);
272             }
273         } else if (command instanceof OnOffType) {
274             OnOffType onOffCommand = (OnOffType) command;
275             if (ENGINE_START.equals(channelID) && onOffCommand == OnOffType.ON) {
276                 actionStart(5);
277             } else if (REMOTE_HEATER.equals(channelID) || PRECLIMATIZATION.equals(channelID)) {
278                 actionHeater(channelID, onOffCommand == OnOffType.ON);
279             } else if (CAR_LOCKED.equals(channelID)) {
280                 actionOpenClose((onOffCommand == OnOffType.ON) ? LOCK : UNLOCK, onOffCommand);
281             }
282         }
283     }
284
285     private State getTripValue(String channelId, TripDetail tripDetails) {
286         switch (channelId) {
287             case TRIP_CONSUMPTION:
288                 return tripDetails.getFuelConsumption()
289                         .map(value -> (State) new QuantityType<>(value.floatValue() / 100, LITRE))
290                         .orElse(UnDefType.UNDEF);
291             case TRIP_DISTANCE:
292                 return new QuantityType<>((double) tripDetails.distance / 1000, KILO(METRE));
293             case TRIP_START_TIME:
294                 return tripDetails.getStartTime();
295             case TRIP_END_TIME:
296                 return tripDetails.getEndTime();
297             case TRIP_DURATION:
298                 return tripDetails.getDurationInMinutes().map(value -> (State) new QuantityType<>(value, MINUTE))
299                         .orElse(UnDefType.UNDEF);
300             case TRIP_START_ODOMETER:
301                 return new QuantityType<>((double) tripDetails.startOdometer / 1000, KILO(METRE));
302             case TRIP_STOP_ODOMETER:
303                 return new QuantityType<>((double) tripDetails.endOdometer / 1000, KILO(METRE));
304             case TRIP_START_POSITION:
305                 return tripDetails.getStartPosition();
306             case TRIP_END_POSITION:
307                 return tripDetails.getEndPosition();
308         }
309         return UnDefType.NULL;
310     }
311
312     private State getDoorsValue(String channelId, DoorsStatus doors) {
313         switch (channelId) {
314             case TAILGATE:
315                 return doors.tailgateOpen;
316             case REAR_RIGHT:
317                 return doors.rearRightDoorOpen;
318             case REAR_LEFT:
319                 return doors.rearLeftDoorOpen;
320             case FRONT_RIGHT:
321                 return doors.frontRightDoorOpen;
322             case FRONT_LEFT:
323                 return doors.frontLeftDoorOpen;
324             case HOOD:
325                 return doors.hoodOpen;
326         }
327         return UnDefType.NULL;
328     }
329
330     private State getWindowsValue(String channelId, WindowsStatus windows) {
331         switch (channelId) {
332             case REAR_RIGHT_WND:
333                 return windows.rearRightWindowOpen;
334             case REAR_LEFT_WND:
335                 return windows.rearLeftWindowOpen;
336             case FRONT_RIGHT_WND:
337                 return windows.frontRightWindowOpen;
338             case FRONT_LEFT_WND:
339                 return windows.frontLeftWindowOpen;
340         }
341         return UnDefType.NULL;
342     }
343
344     private State getTyresValue(String channelId, TyrePressure tyrePressure) {
345         switch (channelId) {
346             case REAR_RIGHT_TYRE:
347                 return tyrePressure.rearRightTyrePressure;
348             case REAR_LEFT_TYRE:
349                 return tyrePressure.rearLeftTyrePressure;
350             case FRONT_RIGHT_TYRE:
351                 return tyrePressure.frontRightTyrePressure;
352             case FRONT_LEFT_TYRE:
353                 return tyrePressure.frontLeftTyrePressure;
354         }
355         return UnDefType.NULL;
356     }
357
358     private State getHeaterValue(String channelId, Heater heater) {
359         switch (channelId) {
360             case REMOTE_HEATER:
361             case PRECLIMATIZATION:
362                 return heater.getStatus();
363         }
364         return UnDefType.NULL;
365     }
366
367     private State getBatteryValue(String channelId, HvBattery hvBattery) {
368         switch (channelId) {
369             case BATTERY_LEVEL:
370                 return hvBattery.hvBatteryLevel != UNDEFINED ? new QuantityType<>(hvBattery.hvBatteryLevel, PERCENT)
371                         : UnDefType.UNDEF;
372             case BATTERY_DISTANCE_TO_EMPTY:
373                 return hvBattery.distanceToHVBatteryEmpty != UNDEFINED
374                         ? new QuantityType<>(hvBattery.distanceToHVBatteryEmpty, KILO(METRE))
375                         : UnDefType.UNDEF;
376             case CHARGE_STATUS:
377                 return hvBattery.hvBatteryChargeStatusDerived != null ? hvBattery.hvBatteryChargeStatusDerived
378                         : UnDefType.UNDEF;
379             case TIME_TO_BATTERY_FULLY_CHARGED:
380                 return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED
381                         ? new QuantityType<>(hvBattery.timeToHVBatteryFullyCharged, MINUTE)
382                         : UnDefType.UNDEF;
383             case CHARGING_END:
384                 return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED && hvBattery.timeToHVBatteryFullyCharged > 0
385                         ? new DateTimeType(ZonedDateTime.now().plusMinutes(hvBattery.timeToHVBatteryFullyCharged))
386                         : UnDefType.UNDEF;
387         }
388         return UnDefType.NULL;
389     }
390
391     private State getValue(String groupId, String channelId, Status status, VehiclePositionWrapper position) {
392         switch (channelId) {
393             case CAR_LOCKED:
394                 // Warning : carLocked is in the Doors group but is part of general status informations.
395                 // Did not change it to avoid breaking change for users
396                 return status.getCarLocked().map(State.class::cast).orElse(UnDefType.UNDEF);
397             case ENGINE_RUNNING:
398                 return status.getEngineRunning().map(State.class::cast).orElse(UnDefType.UNDEF);
399             case BRAKE_FLUID_LEVEL:
400                 return new StringType(status.brakeFluid);
401             case WASHER_FLUID_LEVEL:
402                 return new StringType(status.washerFluidLevel);
403             case AVERAGE_SPEED:
404                 return status.averageSpeed != UNDEFINED ? new QuantityType<>(status.averageSpeed, KILOMETRE_PER_HOUR)
405                         : UnDefType.UNDEF;
406             case SERVICE_WARNING:
407                 return new StringType(status.serviceWarningStatus);
408             case BULB_FAILURE:
409                 return status.aFailedBulb() ? OnOffType.ON : OnOffType.OFF;
410             case REMOTE_HEATER:
411             case PRECLIMATIZATION:
412                 return status.getHeater().map(heater -> getHeaterValue(channelId, heater)).orElse(UnDefType.NULL);
413         }
414         switch (groupId) {
415             case GROUP_TANK:
416                 return getTankValue(channelId, status);
417             case GROUP_ODOMETER:
418                 return getOdometerValue(channelId, status);
419             case GROUP_POSITION:
420                 return getPositionValue(channelId, position);
421             case GROUP_DOORS:
422                 return status.getDoors().map(doors -> getDoorsValue(channelId, doors)).orElse(UnDefType.NULL);
423             case GROUP_WINDOWS:
424                 return status.getWindows().map(windows -> getWindowsValue(channelId, windows)).orElse(UnDefType.NULL);
425             case GROUP_TYRES:
426                 return status.getTyrePressure().map(tyres -> getTyresValue(channelId, tyres)).orElse(UnDefType.NULL);
427             case GROUP_BATTERY:
428                 return status.getHvBattery().map(batteries -> getBatteryValue(channelId, batteries))
429                         .orElse(UnDefType.NULL);
430         }
431         return UnDefType.NULL;
432     }
433
434     private State getTankValue(String channelId, Status status) {
435         switch (channelId) {
436             case DISTANCE_TO_EMPTY:
437                 return status.distanceToEmpty != UNDEFINED ? new QuantityType<>(status.distanceToEmpty, KILO(METRE))
438                         : UnDefType.UNDEF;
439             case FUEL_AMOUNT:
440                 return status.fuelAmount != UNDEFINED ? new QuantityType<>(status.fuelAmount, LITRE) : UnDefType.UNDEF;
441             case FUEL_LEVEL:
442                 return status.fuelAmountLevel != UNDEFINED ? new QuantityType<>(status.fuelAmountLevel, PERCENT)
443                         : UnDefType.UNDEF;
444             case FUEL_CONSUMPTION:
445                 return status.averageFuelConsumption != UNDEFINED ? new DecimalType(status.averageFuelConsumption / 10)
446                         : UnDefType.UNDEF;
447             case FUEL_ALERT:
448                 return status.distanceToEmpty < 100 ? OnOffType.ON : OnOffType.OFF;
449         }
450         return UnDefType.UNDEF;
451     }
452
453     private State getOdometerValue(String channelId, Status status) {
454         switch (channelId) {
455             case ODOMETER:
456                 return status.odometer != UNDEFINED ? new QuantityType<>((double) status.odometer / 1000, KILO(METRE))
457                         : UnDefType.UNDEF;
458             case TRIPMETER1:
459                 return status.tripMeter1 != UNDEFINED
460                         ? new QuantityType<>((double) status.tripMeter1 / 1000, KILO(METRE))
461                         : UnDefType.UNDEF;
462             case TRIPMETER2:
463                 return status.tripMeter2 != UNDEFINED
464                         ? new QuantityType<>((double) status.tripMeter2 / 1000, KILO(METRE))
465                         : UnDefType.UNDEF;
466         }
467         return UnDefType.UNDEF;
468     }
469
470     private State getPositionValue(String channelId, VehiclePositionWrapper position) {
471         switch (channelId) {
472             case ACTUAL_LOCATION:
473                 return position.getPosition();
474             case CALCULATED_LOCATION:
475                 return position.isCalculated();
476             case HEADING:
477                 return position.isHeading();
478             case LOCATION_TIMESTAMP:
479                 return position.getTimestamp();
480         }
481         return UnDefType.UNDEF;
482     }
483
484     public void actionHonkBlink(Boolean honk, Boolean blink) {
485         StringBuilder url = new StringBuilder("vehicles/" + vehicle.vehicleId + "/honk_blink/");
486
487         if (honk && blink && activeOptions.containsKey(HONK_BLINK)) {
488             url.append("both");
489         } else if (honk && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
490             url.append("horn");
491         } else if (blink && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
492             url.append("lights");
493         } else {
494             logger.warn("The vehicle is not capable of this action");
495             return;
496         }
497
498         post(url.toString(), vehiclePosition.getPositionAsJSon());
499     }
500
501     private void post(String url, @Nullable String param) {
502         VocHttpApi api = bridgeHandler.getApi();
503         if (api != null) {
504             try {
505                 PostResponse postResponse = api.postURL(url.toString(), param);
506                 if (postResponse != null) {
507                     pendingActions
508                             .add(scheduler.schedule(new ActionResultController(api, postResponse, scheduler, this),
509                                     1000, TimeUnit.MILLISECONDS));
510                 }
511             } catch (VolvoOnCallException e) {
512                 logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
513                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
514             }
515         }
516         ;
517         pendingActions.removeIf(ScheduledFuture::isDone);
518     }
519
520     public void actionOpenClose(String action, OnOffType controlState) {
521         if (activeOptions.containsKey(action)) {
522             if (!vehicleStatus.getCarLocked().isPresent() || vehicleStatus.getCarLocked().get() != controlState) {
523                 post(String.format("vehicles/%s/%s", configuration.vin, action), "{}");
524             } else {
525                 logger.info("The car {} is already {}ed", configuration.vin, action);
526             }
527         } else {
528             logger.warn("The car {} does not support remote {}ing", configuration.vin, action);
529         }
530     }
531
532     public void actionHeater(String action, Boolean start) {
533         if (activeOptions.containsKey(action)) {
534             String address = String.format("vehicles/%s/%s/%s", configuration.vin,
535                     action.contains(REMOTE_HEATER) ? "heater" : "preclimatization", start ? "start" : "stop");
536             post(address, start ? "{}" : null);
537         } else {
538             logger.warn("The car {} does not support {}", configuration.vin, action);
539         }
540     }
541
542     public void actionStart(Integer runtime) {
543         if (activeOptions.containsKey(ENGINE_START)) {
544             String address = String.format("vehicles/%s/engine/start", vehicle.vehicleId);
545             String json = "{\"runtime\":" + runtime.toString() + "}";
546
547             post(address, json);
548         } else {
549             logger.warn("The car {} does not support remote engine starting", vehicle.vehicleId);
550         }
551     }
552
553     @Override
554     public Collection<Class<? extends ThingHandlerService>> getServices() {
555         return Collections.singletonList(VolvoOnCallActions.class);
556     }
557 }