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