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