]> git.basschouten.com Git - openhab-addons.git/blob
8bac40024a9f6e36b668069520c4eaf064528c63
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.mybmw.internal.handler;
14
15 import static org.openhab.binding.mybmw.internal.MyBMWConstants.ADDRESS;
16 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHARGE_PROFILE;
17 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHARGE_SESSION;
18 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHARGE_STATISTICS;
19 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_CHECK_CONTROL;
20 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_DOORS;
21 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_LOCATION;
22 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_RANGE;
23 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_REMOTE;
24 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_SERVICE;
25 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_STATUS;
26 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_TIRES;
27 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHANNEL_GROUP_VEHICLE_IMAGE;
28 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_ENABLED;
29 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_CLIMATE;
30 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_CONTROL;
31 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_LIMIT;
32 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_MODE;
33 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_PREFERENCE;
34 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_PROFILE_TARGET;
35 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_REMAINING;
36 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHARGE_STATUS;
37 import static org.openhab.binding.mybmw.internal.MyBMWConstants.CHECK_CONTROL;
38 import static org.openhab.binding.mybmw.internal.MyBMWConstants.DATE;
39 import static org.openhab.binding.mybmw.internal.MyBMWConstants.DETAILS;
40 import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOORS;
41 import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_DRIVER_FRONT;
42 import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_DRIVER_REAR;
43 import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_PASSENGER_FRONT;
44 import static org.openhab.binding.mybmw.internal.MyBMWConstants.DOOR_PASSENGER_REAR;
45 import static org.openhab.binding.mybmw.internal.MyBMWConstants.ENERGY;
46 import static org.openhab.binding.mybmw.internal.MyBMWConstants.ESTIMATED_FUEL_L_100KM;
47 import static org.openhab.binding.mybmw.internal.MyBMWConstants.ESTIMATED_FUEL_MPG;
48 import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_LEFT_CURRENT;
49 import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_LEFT_TARGET;
50 import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_RIGHT_CURRENT;
51 import static org.openhab.binding.mybmw.internal.MyBMWConstants.FRONT_RIGHT_TARGET;
52 import static org.openhab.binding.mybmw.internal.MyBMWConstants.GPS;
53 import static org.openhab.binding.mybmw.internal.MyBMWConstants.HEADING;
54 import static org.openhab.binding.mybmw.internal.MyBMWConstants.HOME_DISTANCE;
55 import static org.openhab.binding.mybmw.internal.MyBMWConstants.HOOD;
56 import static org.openhab.binding.mybmw.internal.MyBMWConstants.IMAGE_FORMAT;
57 import static org.openhab.binding.mybmw.internal.MyBMWConstants.IMAGE_VIEWPORT;
58 import static org.openhab.binding.mybmw.internal.MyBMWConstants.ISSUE;
59 import static org.openhab.binding.mybmw.internal.MyBMWConstants.LAST_FETCHED;
60 import static org.openhab.binding.mybmw.internal.MyBMWConstants.LAST_UPDATE;
61 import static org.openhab.binding.mybmw.internal.MyBMWConstants.LOCK;
62 import static org.openhab.binding.mybmw.internal.MyBMWConstants.MILEAGE;
63 import static org.openhab.binding.mybmw.internal.MyBMWConstants.NAME;
64 import static org.openhab.binding.mybmw.internal.MyBMWConstants.PLUG_CONNECTION;
65 import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_ELECTRIC;
66 import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_FUEL;
67 import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_HYBRID;
68 import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_RADIUS_ELECTRIC;
69 import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_RADIUS_FUEL;
70 import static org.openhab.binding.mybmw.internal.MyBMWConstants.RANGE_RADIUS_HYBRID;
71 import static org.openhab.binding.mybmw.internal.MyBMWConstants.RAW;
72 import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_LEFT_CURRENT;
73 import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_LEFT_TARGET;
74 import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_RIGHT_CURRENT;
75 import static org.openhab.binding.mybmw.internal.MyBMWConstants.REAR_RIGHT_TARGET;
76 import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMAINING_FUEL;
77 import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_SERVICE_COMMAND;
78 import static org.openhab.binding.mybmw.internal.MyBMWConstants.REMOTE_STATE;
79 import static org.openhab.binding.mybmw.internal.MyBMWConstants.SERVICE_DATE;
80 import static org.openhab.binding.mybmw.internal.MyBMWConstants.SERVICE_MILEAGE;
81 import static org.openhab.binding.mybmw.internal.MyBMWConstants.SESSIONS;
82 import static org.openhab.binding.mybmw.internal.MyBMWConstants.SEVERITY;
83 import static org.openhab.binding.mybmw.internal.MyBMWConstants.SOC;
84 import static org.openhab.binding.mybmw.internal.MyBMWConstants.STATUS;
85 import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUBTITLE;
86 import static org.openhab.binding.mybmw.internal.MyBMWConstants.SUNROOF;
87 import static org.openhab.binding.mybmw.internal.MyBMWConstants.TITLE;
88 import static org.openhab.binding.mybmw.internal.MyBMWConstants.TRUNK;
89 import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOWS;
90 import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_DRIVER_FRONT;
91 import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_DRIVER_REAR;
92 import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_PASSENGER_FRONT;
93 import static org.openhab.binding.mybmw.internal.MyBMWConstants.WINDOW_DOOR_PASSENGER_REAR;
94
95 import java.time.DayOfWeek;
96 import java.time.LocalTime;
97 import java.time.ZoneId;
98 import java.time.ZonedDateTime;
99 import java.util.ArrayList;
100 import java.util.EnumSet;
101 import java.util.List;
102 import java.util.Optional;
103 import java.util.Set;
104 import java.util.concurrent.ScheduledExecutorService;
105 import java.util.concurrent.ScheduledFuture;
106 import java.util.concurrent.TimeUnit;
107
108 import javax.measure.Unit;
109 import javax.measure.quantity.Length;
110
111 import org.eclipse.jdt.annotation.NonNullByDefault;
112 import org.eclipse.jdt.annotation.Nullable;
113 import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType;
114 import org.openhab.binding.mybmw.internal.MyBMWVehicleConfiguration;
115 import org.openhab.binding.mybmw.internal.dto.charge.ChargingProfile;
116 import org.openhab.binding.mybmw.internal.dto.charge.ChargingSession;
117 import org.openhab.binding.mybmw.internal.dto.charge.ChargingSessionsContainer;
118 import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings;
119 import org.openhab.binding.mybmw.internal.dto.charge.ChargingStatisticsContainer;
120 import org.openhab.binding.mybmw.internal.dto.vehicle.CheckControlMessage;
121 import org.openhab.binding.mybmw.internal.dto.vehicle.RequiredService;
122 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleDoorsState;
123 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleLocation;
124 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleRoofState;
125 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleState;
126 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer;
127 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleTireStates;
128 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleWindowsState;
129 import org.openhab.binding.mybmw.internal.handler.backend.MyBMWProxy;
130 import org.openhab.binding.mybmw.internal.handler.backend.NetworkException;
131 import org.openhab.binding.mybmw.internal.utils.ChargingProfileUtils;
132 import org.openhab.binding.mybmw.internal.utils.ChargingProfileUtils.TimedChannel;
133 import org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper;
134 import org.openhab.binding.mybmw.internal.utils.ChargingProfileWrapper.ProfileKey;
135 import org.openhab.binding.mybmw.internal.utils.Constants;
136 import org.openhab.binding.mybmw.internal.utils.Converter;
137 import org.openhab.binding.mybmw.internal.utils.ImageProperties;
138 import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
139 import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
140 import org.openhab.core.i18n.LocationProvider;
141 import org.openhab.core.i18n.TimeZoneProvider;
142 import org.openhab.core.io.net.http.HttpUtil;
143 import org.openhab.core.library.types.DateTimeType;
144 import org.openhab.core.library.types.DecimalType;
145 import org.openhab.core.library.types.OnOffType;
146 import org.openhab.core.library.types.PointType;
147 import org.openhab.core.library.types.QuantityType;
148 import org.openhab.core.library.types.RawType;
149 import org.openhab.core.library.types.StringType;
150 import org.openhab.core.library.unit.SIUnits;
151 import org.openhab.core.library.unit.Units;
152 import org.openhab.core.thing.Bridge;
153 import org.openhab.core.thing.ChannelUID;
154 import org.openhab.core.thing.Thing;
155 import org.openhab.core.thing.ThingStatus;
156 import org.openhab.core.thing.ThingStatusDetail;
157 import org.openhab.core.thing.binding.BaseThingHandler;
158 import org.openhab.core.thing.binding.BridgeHandler;
159 import org.openhab.core.types.Command;
160 import org.openhab.core.types.CommandOption;
161 import org.openhab.core.types.RefreshType;
162 import org.openhab.core.types.State;
163 import org.openhab.core.types.UnDefType;
164 import org.slf4j.Logger;
165 import org.slf4j.LoggerFactory;
166
167 /**
168  * The {@link VehicleHandler} handles responses from BMW API
169  *
170  * the introduction of channelToBeUpdated is ugly, but if there is a refresh of one channel, always all channels were
171  * updated
172  *
173  * @author Bernd Weymann - Initial contribution
174  * @author Norbert Truchsess - edit and send charge profile
175  * @author Martin Grassl - refactoring, merge with VehicleChannelHandler
176  * @author Mark Herwege - refactoring, V2 API charging
177  */
178 @NonNullByDefault
179 public class VehicleHandler extends BaseThingHandler {
180     private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
181
182     private boolean hasFuel = false;
183     private boolean isElectric = false;
184     private boolean isHybrid = false;
185
186     // List Interfaces
187     private volatile List<RequiredService> serviceList = List.of();
188     private volatile String selectedService = Constants.UNDEF;
189     private volatile List<CheckControlMessage> checkControlList = List.of();
190     private volatile String selectedCC = Constants.UNDEF;
191     private volatile List<ChargingSession> sessionList = List.of();
192     private volatile String selectedSession = Constants.UNDEF;
193
194     private MyBMWCommandOptionProvider commandOptionProvider;
195     private LocationProvider locationProvider;
196     private TimeZoneProvider timeZoneProvider;
197
198     // Data Caches
199     private Optional<VehicleStateContainer> vehicleStatusCache = Optional.empty();
200     private Optional<byte[]> imageCache = Optional.empty();
201
202     private Optional<MyBMWProxy> proxy = Optional.empty();
203     private Optional<RemoteServiceExecutor> remote = Optional.empty();
204     private Optional<MyBMWVehicleConfiguration> vehicleConfiguration = Optional.empty();
205     private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
206     private Optional<ScheduledFuture<?>> editTimeout = Optional.empty();
207
208     private ImageProperties imageProperties = new ImageProperties();
209
210     public VehicleHandler(Thing thing, MyBMWCommandOptionProvider commandOptionProvider,
211             LocationProvider locationProvider, TimeZoneProvider timeZoneProvider, String driveTrain) {
212         super(thing);
213         logger.trace("VehicleHandler.constructor {}, {}", thing.getUID(), driveTrain);
214         this.commandOptionProvider = commandOptionProvider;
215         this.timeZoneProvider = timeZoneProvider;
216         this.locationProvider = locationProvider;
217         if (locationProvider.getLocation() == null) {
218             logger.debug("Home location not available");
219         }
220
221         hasFuel = driveTrain.equals(VehicleType.CONVENTIONAL.toString())
222                 || driveTrain.equals(VehicleType.PLUGIN_HYBRID.toString())
223                 || driveTrain.equals(VehicleType.ELECTRIC_REX.toString())
224                 || driveTrain.equals(VehicleType.MILD_HYBRID.toString());
225         isElectric = driveTrain.equals(VehicleType.PLUGIN_HYBRID.toString())
226                 || driveTrain.equals(VehicleType.ELECTRIC_REX.toString())
227                 || driveTrain.equals(VehicleType.ELECTRIC.toString());
228         isHybrid = hasFuel && isElectric;
229
230         setOptions(CHANNEL_GROUP_REMOTE, REMOTE_SERVICE_COMMAND, RemoteServiceUtils.getOptions(isElectric));
231     }
232
233     private void setOptions(final String group, final String id, List<CommandOption> options) {
234         commandOptionProvider.setCommandOptions(new ChannelUID(thing.getUID(), group, id), options);
235     }
236
237     @Override
238     public void initialize() {
239         logger.trace("VehicleHandler.initialize");
240         updateStatus(ThingStatus.UNKNOWN);
241         vehicleConfiguration = Optional.of(getConfigAs(MyBMWVehicleConfiguration.class));
242
243         Bridge bridge = getBridge();
244         if (bridge != null) {
245             BridgeHandler handler = bridge.getHandler();
246             if (handler != null) {
247                 proxy = ((MyBMWBridgeHandler) handler).getMyBmwProxy();
248                 remote = Optional.of(new RemoteServiceExecutor(this, proxy.get()));
249             } else {
250                 logger.debug("Bridge Handler null");
251             }
252         } else {
253             logger.debug("Bridge null");
254         }
255
256         imageProperties = new ImageProperties();
257         updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, Converter.toTitleCase(imageProperties.viewport),
258                 null);
259
260         // start update schedule
261         startSchedule(vehicleConfiguration.get().getRefreshInterval());
262     }
263
264     private void startSchedule(int interval) {
265         logger.trace("VehicleHandler.startSchedule");
266         refreshJob.ifPresentOrElse(job -> {
267             if (job.isCancelled()) {
268                 refreshJob = Optional
269                         .of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
270             } // else - scheduler is already running!
271         }, () -> {
272             refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::getData, 0, interval, TimeUnit.MINUTES));
273         });
274     }
275
276     @Override
277     public void dispose() {
278         logger.trace("VehicleHandler.dispose");
279         refreshJob.ifPresent(job -> job.cancel(true));
280         editTimeout.ifPresent(job -> job.cancel(true));
281         remote.ifPresent(RemoteServiceExecutor::cancel);
282     }
283
284     public void getData() {
285         logger.trace("VehicleHandler.getData");
286         proxy.ifPresentOrElse(prox -> {
287             vehicleConfiguration.ifPresentOrElse(config -> {
288
289                 boolean stateError = false;
290                 try {
291                     VehicleStateContainer vehicleState = prox.requestVehicleState(config.getVin(),
292                             config.getVehicleBrand());
293                     triggerVehicleStatusUpdate(vehicleState, null);
294                     stateError = false;
295                 } catch (NetworkException e) {
296                     logger.debug("{}", e.toString());
297                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
298                             "Vehicle State Update failed");
299                     stateError = true;
300                 }
301
302                 if (!stateError && isElectric) {
303                     try {
304                         updateChargingStatistics(
305                                 prox.requestChargeStatistics(config.getVin(), config.getVehicleBrand()), null);
306                         updateChargingSessions(prox.requestChargeSessions(config.getVin(), config.getVehicleBrand()),
307                                 null);
308                     } catch (NetworkException e) {
309                         logger.debug("{}", e.toString());
310                     }
311                 }
312                 if (!stateError && !imageCache.isPresent() && !imageProperties.failLimitReached()) {
313                     try {
314                         updateImage(prox.requestImage(config.getVin(), config.getVehicleBrand(), imageProperties));
315                     } catch (NetworkException e) {
316                         logger.debug("{}", e.toString());
317                     }
318                 }
319             }, () -> {
320                 logger.warn("MyBMW Vehicle Configuration isn't present");
321             });
322         }, () -> {
323             logger.warn("MyBMWProxy isn't present");
324         });
325     }
326
327     private void triggerVehicleStatusUpdate(VehicleStateContainer vehicleState, @Nullable String channelToBeUpdated) {
328         logger.trace("VehicleHandler.triggerVehicleStatusUpdate for {}", channelToBeUpdated);
329         if (vehicleConfiguration.isPresent()) {
330             vehicleStatusCache = Optional.of(vehicleState);
331             updateChannel(CHANNEL_GROUP_STATUS, RAW, vehicleState.getRawStateJson(), channelToBeUpdated);
332
333             updateVehicleStatus(vehicleState.getState(), channelToBeUpdated);
334             if (isElectric) {
335                 updateChargingProfile(vehicleState.getState().getChargingProfile(), channelToBeUpdated);
336             }
337
338             updateStatus(ThingStatus.ONLINE);
339         } else {
340             logger.debug("configuration not present");
341         }
342     }
343
344     public void updateRemoteExecutionStatus(@Nullable String service, String status) {
345         updateChannel(CHANNEL_GROUP_REMOTE, REMOTE_STATE,
346                 (service == null ? "-" : service) + Constants.SPACE + status.toLowerCase(), null);
347     }
348
349     public Optional<MyBMWVehicleConfiguration> getVehicleConfiguration() {
350         logger.trace("VehicleHandler.getVehicleConfiguration");
351         return vehicleConfiguration;
352     }
353
354     public ScheduledExecutorService getScheduler() {
355         logger.trace("VehicleHandler.getScheduler");
356         return scheduler;
357     }
358
359     private void updateChannel(final String group, final String id, final String state,
360             @Nullable final String channelToBeUpdated) {
361         updateChannel(group, id, StringType.valueOf(state), channelToBeUpdated);
362     }
363
364     /**
365      * this method sets the state for a single channel. if a channelToBeUpdated is provided, the update will only take
366      * place for that single channel.
367      */
368     private void updateChannel(final String group, final String id, final State state,
369             @Nullable final String channelToBeUpdated) {
370         if (channelToBeUpdated == null || id.equals(channelToBeUpdated)) {
371             if (!"png".equals(id)) {
372                 logger.trace("updating channel {}, {}, {}", group, id, state.toFullString());
373             } else {
374                 logger.trace("updating channel {}, {}, {}", group, id, "not printed");
375             }
376
377             updateState(new ChannelUID(thing.getUID(), group, id), state);
378         }
379     }
380
381     private void updateChargingStatistics(ChargingStatisticsContainer chargingStatisticsContainer,
382             @Nullable String channelToBeUpdated) {
383         if (!"".equals(chargingStatisticsContainer.getDescription())) {
384             updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, TITLE, chargingStatisticsContainer.getDescription(),
385                     channelToBeUpdated);
386             updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, ENERGY, QuantityType
387                     .valueOf(chargingStatisticsContainer.getStatistics().getTotalEnergyCharged(), Units.KILOWATT_HOUR),
388                     channelToBeUpdated);
389             updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, SESSIONS,
390                     DecimalType.valueOf(Integer
391                             .toString(chargingStatisticsContainer.getStatistics().getNumberOfChargingSessions())),
392                     channelToBeUpdated);
393         }
394     }
395
396     /**
397      * updates the channels with the current state of the vehicle
398      *
399      * @param vehicleStateState
400      */
401     private void updateVehicleStatus(VehicleState vehicleStateState, @Nullable String channelToBeUpdated) {
402         boolean isLeftSteering = vehicleStateState.isLeftSteering();
403
404         updateVehicleOverallStatus(vehicleStateState, channelToBeUpdated);
405         updateRange(vehicleStateState, channelToBeUpdated);
406         updateDoors(vehicleStateState.getDoorsState(), isLeftSteering, channelToBeUpdated);
407         updateWindows(vehicleStateState.getWindowsState(), isLeftSteering, channelToBeUpdated);
408         updateRoof(vehicleStateState.getRoofState(), channelToBeUpdated);
409         updatePosition(vehicleStateState.getLocation(), channelToBeUpdated);
410         updateServices(vehicleStateState.getRequiredServices(), channelToBeUpdated);
411         updateCheckControls(vehicleStateState.getCheckControlMessages(), channelToBeUpdated);
412         updateTires(vehicleStateState.getTireState(), channelToBeUpdated);
413     }
414
415     private void updateTires(@Nullable VehicleTireStates vehicleTireStates, @Nullable String channelToBeUpdated) {
416         if (vehicleTireStates == null) {
417             updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT, UnDefType.UNDEF, channelToBeUpdated);
418             updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET, UnDefType.UNDEF, channelToBeUpdated);
419             updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT, UnDefType.UNDEF, channelToBeUpdated);
420             updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET, UnDefType.UNDEF, channelToBeUpdated);
421             updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT, UnDefType.UNDEF, channelToBeUpdated);
422             updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET, UnDefType.UNDEF, channelToBeUpdated);
423             updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT, UnDefType.UNDEF, channelToBeUpdated);
424             updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET, UnDefType.UNDEF, channelToBeUpdated);
425         } else {
426             updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT,
427                     calculatePressure(vehicleTireStates.getFrontLeft().getStatus().getCurrentPressure()),
428                     channelToBeUpdated);
429             updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET,
430                     calculatePressure(vehicleTireStates.getFrontLeft().getStatus().getTargetPressure()),
431                     channelToBeUpdated);
432             updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT,
433                     calculatePressure(vehicleTireStates.getFrontRight().getStatus().getCurrentPressure()),
434                     channelToBeUpdated);
435             updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET,
436                     calculatePressure(vehicleTireStates.getFrontRight().getStatus().getTargetPressure()),
437                     channelToBeUpdated);
438             updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT,
439                     calculatePressure(vehicleTireStates.getRearLeft().getStatus().getCurrentPressure()),
440                     channelToBeUpdated);
441             updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET,
442                     calculatePressure(vehicleTireStates.getRearLeft().getStatus().getTargetPressure()),
443                     channelToBeUpdated);
444             updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT,
445                     calculatePressure(vehicleTireStates.getRearRight().getStatus().getCurrentPressure()),
446                     channelToBeUpdated);
447             updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET,
448                     calculatePressure(vehicleTireStates.getRearRight().getStatus().getTargetPressure()),
449                     channelToBeUpdated);
450         }
451     }
452
453     /**
454      * if the pressure is undef it is < 0
455      *
456      * @param pressure
457      * @return
458      */
459     private State calculatePressure(int pressure) {
460         if (pressure > 0) {
461             return QuantityType.valueOf(pressure / 100.0, Units.BAR);
462         } else {
463             return UnDefType.UNDEF;
464         }
465     }
466
467     private void updateVehicleOverallStatus(VehicleState vehicleState, @Nullable String channelToBeUpdated) {
468         updateChannel(CHANNEL_GROUP_STATUS, LOCK,
469                 Converter.toTitleCase(vehicleState.getDoorsState().getCombinedSecurityState()), channelToBeUpdated);
470         updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE,
471                 VehicleStatusUtils.getNextServiceDate(vehicleState.getRequiredServices()), channelToBeUpdated);
472         updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE,
473                 VehicleStatusUtils.getNextServiceMileage(vehicleState.getRequiredServices()), channelToBeUpdated);
474         updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL,
475                 Converter.toTitleCase(vehicleState.getOverallCheckControlStatus()), channelToBeUpdated);
476         updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
477                 Converter.zonedToLocalDateTime(vehicleState.getLastUpdatedAt(), timeZoneProvider.getTimeZone()),
478                 channelToBeUpdated);
479         updateChannel(CHANNEL_GROUP_STATUS, LAST_FETCHED,
480                 Converter.zonedToLocalDateTime(vehicleState.getLastFetched(), timeZoneProvider.getTimeZone()),
481                 channelToBeUpdated);
482         updateChannel(CHANNEL_GROUP_STATUS, DOORS,
483                 Converter.toTitleCase(vehicleState.getDoorsState().getCombinedState()), channelToBeUpdated);
484         updateChannel(CHANNEL_GROUP_STATUS, WINDOWS,
485                 Converter.toTitleCase(vehicleState.getWindowsState().getCombinedState()), channelToBeUpdated);
486
487         if (isElectric) {
488             updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION,
489                     Converter.getConnectionState(vehicleState.getElectricChargingState().isChargerConnected()),
490                     channelToBeUpdated);
491             updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
492                     Converter.toTitleCase(vehicleState.getElectricChargingState().getChargingStatus()),
493                     channelToBeUpdated);
494
495             int remainingTime = vehicleState.getElectricChargingState().getRemainingChargingMinutes();
496             updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING,
497                     remainingTime >= 0 ? QuantityType.valueOf(remainingTime, Units.MINUTE) : UnDefType.UNDEF,
498                     channelToBeUpdated);
499         }
500     }
501
502     private void updateRange(VehicleState vehicleState, @Nullable String channelToBeUpdated) {
503         // get the right unit
504         Unit<Length> lengthUnit = Constants.KILOMETRE_UNIT;
505
506         if (isElectric) {
507             int rangeElectric = vehicleState.getElectricChargingState().getRange();
508             QuantityType<Length> qtElectricRange = QuantityType.valueOf(rangeElectric, lengthUnit);
509             QuantityType<Length> qtElectricRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeElectric),
510                     lengthUnit);
511             updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC, qtElectricRange, channelToBeUpdated);
512             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC, qtElectricRadius, channelToBeUpdated);
513         }
514
515         if (hasFuel && !isHybrid) {
516             int rangeFuel = vehicleState.getCombustionFuelLevel().getRange();
517             QuantityType<Length> qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit);
518             QuantityType<Length> qtFuelRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeFuel), lengthUnit);
519             updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, qtFuelRange, channelToBeUpdated);
520             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL, qtFuelRadius, channelToBeUpdated);
521         }
522
523         if (isHybrid) {
524             int rangeCombined = vehicleState.getRange();
525
526             // there is a bug/feature in the API that the fuel range is the same like the combined range, hence in case
527             // of hybrid the fuel range has to be subtracted by the electric range
528             int rangeFuel = vehicleState.getCombustionFuelLevel().getRange()
529                     - vehicleState.getElectricChargingState().getRange();
530
531             QuantityType<Length> qtHybridRange = QuantityType.valueOf(rangeCombined, lengthUnit);
532             QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeCombined),
533                     lengthUnit);
534             updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID, qtHybridRange, channelToBeUpdated);
535             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID, qtHybridRadius, channelToBeUpdated);
536
537             QuantityType<Length> qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit);
538             QuantityType<Length> qtFuelRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeFuel), lengthUnit);
539             updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, qtFuelRange, channelToBeUpdated);
540             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL, qtFuelRadius, channelToBeUpdated);
541         }
542
543         if (vehicleState.getCurrentMileage() == Constants.INT_UNDEF) {
544             updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, UnDefType.UNDEF, channelToBeUpdated);
545         } else {
546             updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
547                     QuantityType.valueOf(vehicleState.getCurrentMileage(), lengthUnit), channelToBeUpdated);
548         }
549         if (isElectric) {
550             updateChannel(
551                     CHANNEL_GROUP_RANGE, SOC, QuantityType
552                             .valueOf(vehicleState.getElectricChargingState().getChargingLevelPercent(), Units.PERCENT),
553                     channelToBeUpdated);
554         }
555         if (hasFuel) {
556             updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
557                     QuantityType.valueOf(vehicleState.getCombustionFuelLevel().getRemainingFuelLiters(), Units.LITRE),
558                     channelToBeUpdated);
559
560             if (vehicleState.getCombustionFuelLevel().getRemainingFuelLiters() > 0
561                     && vehicleState.getCombustionFuelLevel().getRange() > 1) {
562                 double estimatedFuelConsumption = vehicleState.getCombustionFuelLevel().getRemainingFuelLiters() * 1.0
563                         / vehicleState.getCombustionFuelLevel().getRange() * 100.0;
564                 updateChannel(CHANNEL_GROUP_RANGE, ESTIMATED_FUEL_L_100KM,
565                         DecimalType.valueOf(estimatedFuelConsumption + ""), channelToBeUpdated);
566                 updateChannel(CHANNEL_GROUP_RANGE, ESTIMATED_FUEL_MPG,
567                         DecimalType.valueOf((235.214583 / estimatedFuelConsumption) + ""), channelToBeUpdated);
568             } else {
569                 updateChannel(CHANNEL_GROUP_RANGE, ESTIMATED_FUEL_L_100KM, UnDefType.UNDEF, channelToBeUpdated);
570                 updateChannel(CHANNEL_GROUP_RANGE, ESTIMATED_FUEL_MPG, UnDefType.UNDEF, channelToBeUpdated);
571             }
572         }
573     }
574
575     private void updateCheckControls(List<CheckControlMessage> checkControlMessages,
576             @Nullable String channelToBeUpdated) {
577         if (checkControlMessages.isEmpty()) {
578             // No Check Control available - show not active
579             CheckControlMessage checkControlMessage = new CheckControlMessage();
580             checkControlMessage.setName(Constants.NO_ENTRIES);
581             checkControlMessage.setDescription(Constants.NO_ENTRIES);
582             checkControlMessage.setSeverity(Constants.NO_ENTRIES);
583             checkControlMessage.setType(Constants.NO_ENTRIES);
584             checkControlMessage.setId(-1);
585             checkControlMessages.add(checkControlMessage);
586         }
587
588         // add all elements to options
589         checkControlList = checkControlMessages;
590         List<CommandOption> ccmDescriptionOptions = new ArrayList<>();
591         boolean isSelectedElementIn = false;
592         int index = 0;
593         for (CheckControlMessage checkControlMessage : checkControlList) {
594             ccmDescriptionOptions.add(
595                     new CommandOption(Integer.toString(index), Converter.toTitleCase(checkControlMessage.getType())));
596             if (selectedCC.equals(checkControlMessage.getType())) {
597                 isSelectedElementIn = true;
598             }
599             index++;
600         }
601         setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
602
603         // if current selected item isn't anymore in the list select first entry
604         if (!isSelectedElementIn) {
605             selectCheckControl(0, channelToBeUpdated);
606         }
607     }
608
609     private void selectCheckControl(int index, @Nullable String channelToBeUpdated) {
610         if (index >= 0 && index < checkControlList.size()) {
611             CheckControlMessage checkControlMessage = checkControlList.get(index);
612             selectedCC = checkControlMessage.getType();
613             updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, Converter.toTitleCase(checkControlMessage.getType()),
614                     channelToBeUpdated);
615             updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS,
616                     StringType.valueOf(checkControlMessage.getDescription()), channelToBeUpdated);
617             updateChannel(CHANNEL_GROUP_CHECK_CONTROL, SEVERITY,
618                     Converter.toTitleCase(checkControlMessage.getSeverity()), channelToBeUpdated);
619         }
620     }
621
622     private void updateServices(List<RequiredService> requiredServiceList, @Nullable String channelToBeUpdated) {
623         // if list is empty add "undefined" element
624         if (requiredServiceList.isEmpty()) {
625             RequiredService requiredService = new RequiredService();
626             requiredService.setType(Constants.NO_ENTRIES);
627             requiredService.setDescription(Constants.NO_ENTRIES);
628             requiredServiceList.add(requiredService);
629         }
630
631         // add all elements to options
632         serviceList = requiredServiceList;
633         List<CommandOption> serviceNameOptions = new ArrayList<>();
634         boolean isSelectedElementIn = false;
635         int index = 0;
636         for (RequiredService requiredService : requiredServiceList) {
637             // create StateOption with "value = list index" and "label = human readable
638             // string"
639             serviceNameOptions
640                     .add(new CommandOption(Integer.toString(index), Converter.toTitleCase(requiredService.getType())));
641             if (selectedService.equals(requiredService.getType())) {
642                 isSelectedElementIn = true;
643             }
644             index++;
645         }
646
647         setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
648
649         // if current selected item isn't anymore in the list select first entry
650         if (!isSelectedElementIn) {
651             selectService(0, channelToBeUpdated);
652         }
653     }
654
655     private void selectService(int index, @Nullable String channelToBeUpdated) {
656         if (index >= 0 && index < serviceList.size()) {
657             RequiredService serviceEntry = serviceList.get(index);
658             selectedService = serviceEntry.getType();
659             updateChannel(CHANNEL_GROUP_SERVICE, NAME, Converter.toTitleCase(serviceEntry.getType()),
660                     channelToBeUpdated);
661             updateChannel(CHANNEL_GROUP_SERVICE, DETAILS, StringType.valueOf(serviceEntry.getDescription()),
662                     channelToBeUpdated);
663             updateChannel(CHANNEL_GROUP_SERVICE, DATE,
664                     Converter.zonedToLocalDateTime(serviceEntry.getDateTime(), timeZoneProvider.getTimeZone()),
665                     channelToBeUpdated);
666
667             if (serviceEntry.getMileage() > 0) {
668                 updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
669                         QuantityType.valueOf(serviceEntry.getMileage(), Constants.KILOMETRE_UNIT), channelToBeUpdated);
670             } else {
671                 updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE, UnDefType.UNDEF, channelToBeUpdated);
672             }
673         }
674     }
675
676     private void updateChargingSessions(ChargingSessionsContainer chargeSessionsContainer,
677             @Nullable String channelToBeUpdated) {
678         List<ChargingSession> chargeSessions = new ArrayList<>();
679
680         if (chargeSessionsContainer.chargingSessions != null
681                 && chargeSessionsContainer.chargingSessions.getSessions() != null
682                 && !chargeSessionsContainer.chargingSessions.getSessions().isEmpty()) {
683             chargeSessions.addAll(chargeSessionsContainer.chargingSessions.getSessions());
684         } else {
685             // if list is empty add "undefined" element
686             ChargingSession cs = new ChargingSession();
687             cs.setTitle(Constants.NO_ENTRIES);
688             chargeSessions.add(cs);
689         }
690
691         // add all elements to options
692         sessionList = chargeSessions;
693         List<CommandOption> sessionNameOptions = new ArrayList<>();
694         boolean isSelectedElementIn = false;
695         int index = 0;
696         for (ChargingSession session : sessionList) {
697             // create StateOption with "value = list index" and "label = human readable
698             // string"
699             sessionNameOptions.add(new CommandOption(Integer.toString(index), session.getTitle()));
700             if (selectedSession.equals(session.getTitle())) {
701                 isSelectedElementIn = true;
702             }
703             index++;
704         }
705         setOptions(CHANNEL_GROUP_CHARGE_SESSION, TITLE, sessionNameOptions);
706
707         // if current selected item isn't anymore in the list select first entry
708         if (!isSelectedElementIn) {
709             selectSession(0, channelToBeUpdated);
710         }
711     }
712
713     private void selectSession(int index, @Nullable String channelToBeUpdated) {
714         if (index >= 0 && index < sessionList.size()) {
715             ChargingSession sessionEntry = sessionList.get(index);
716             selectedSession = sessionEntry.getTitle();
717             updateChannel(CHANNEL_GROUP_CHARGE_SESSION, TITLE, StringType.valueOf(sessionEntry.getTitle()),
718                     channelToBeUpdated);
719             updateChannel(CHANNEL_GROUP_CHARGE_SESSION, SUBTITLE, StringType.valueOf(sessionEntry.getSubtitle()),
720                     channelToBeUpdated);
721             if (sessionEntry.getEnergyCharged() != null) {
722                 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(sessionEntry.getEnergyCharged()),
723                         channelToBeUpdated);
724             } else {
725                 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(Constants.UNDEF),
726                         channelToBeUpdated);
727             }
728             if (sessionEntry.getIssues() != null) {
729                 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(sessionEntry.getIssues()),
730                         channelToBeUpdated);
731             } else {
732                 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(Constants.HYPHEN),
733                         channelToBeUpdated);
734             }
735             updateChannel(CHANNEL_GROUP_CHARGE_SESSION, STATUS, StringType.valueOf(sessionEntry.getSessionStatus()),
736                     channelToBeUpdated);
737         }
738     }
739
740     private void updateChargingProfile(ChargingProfile cp, @Nullable String channelToBeUpdated) {
741         ChargingProfileWrapper cpw = new ChargingProfileWrapper(cp);
742
743         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_PREFERENCE, StringType.valueOf(cpw.getPreference()),
744                 channelToBeUpdated);
745         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_MODE, StringType.valueOf(cpw.getMode()),
746                 channelToBeUpdated);
747         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CONTROL, StringType.valueOf(cpw.getControlType()),
748                 channelToBeUpdated);
749         ChargingSettings cs = cpw.getChargingSettings();
750         if (cs != null) {
751             updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_TARGET,
752                     QuantityType.valueOf(cs.getTargetSoc(), Units.PERCENT), channelToBeUpdated);
753
754             updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_LIMIT,
755                     OnOffType.from(cs.isAcCurrentLimitActive()), channelToBeUpdated);
756         }
757         final Boolean climate = cpw.isEnabled(ProfileKey.CLIMATE);
758         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CLIMATE,
759                 climate == null ? UnDefType.UNDEF : OnOffType.from(climate), channelToBeUpdated);
760         updateTimedState(cpw, ProfileKey.WINDOWSTART, channelToBeUpdated);
761         updateTimedState(cpw, ProfileKey.WINDOWEND, channelToBeUpdated);
762         updateTimedState(cpw, ProfileKey.TIMER1, channelToBeUpdated);
763         updateTimedState(cpw, ProfileKey.TIMER2, channelToBeUpdated);
764         updateTimedState(cpw, ProfileKey.TIMER3, channelToBeUpdated);
765         updateTimedState(cpw, ProfileKey.TIMER4, channelToBeUpdated);
766     }
767
768     private void updateTimedState(ChargingProfileWrapper profile, ProfileKey key, @Nullable String channelToBeUpdated) {
769         final TimedChannel timed = ChargingProfileUtils.getTimedChannel(key);
770         if (timed != null) {
771             final LocalTime time = profile.getTime(key);
772             updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.time,
773                     time.equals(Constants.NULL_LOCAL_TIME) ? UnDefType.UNDEF
774                             : new DateTimeType(ZonedDateTime.of(Constants.EPOCH_DAY, time, ZoneId.systemDefault())),
775                     channelToBeUpdated);
776             if (timed.timer != null) {
777                 final Boolean enabled = profile.isEnabled(key);
778                 updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.timer + CHARGE_ENABLED,
779                         enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled), channelToBeUpdated);
780                 if (timed.hasDays) {
781                     final Set<DayOfWeek> days = profile.getDays(key);
782                     EnumSet.allOf(DayOfWeek.class).forEach(day -> {
783                         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE,
784                                 timed.timer + ChargingProfileUtils.getDaysChannel(day),
785                                 days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)),
786                                 channelToBeUpdated);
787                     });
788                 }
789             }
790         }
791     }
792
793     private void updateDoors(VehicleDoorsState vehicleDoorsState, boolean isLeftSteering,
794             @Nullable String channelToBeUpdated) {
795         updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT,
796                 StringType.valueOf(Converter.toTitleCase(
797                         isLeftSteering ? vehicleDoorsState.getLeftFront() : vehicleDoorsState.getRightFront())),
798                 channelToBeUpdated);
799         updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR,
800                 StringType.valueOf(Converter.toTitleCase(
801                         isLeftSteering ? vehicleDoorsState.getLeftRear() : vehicleDoorsState.getRightRear())),
802                 channelToBeUpdated);
803         updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT,
804                 StringType.valueOf(Converter.toTitleCase(
805                         isLeftSteering ? vehicleDoorsState.getRightFront() : vehicleDoorsState.getLeftFront())),
806                 channelToBeUpdated);
807         updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR,
808                 StringType.valueOf(Converter.toTitleCase(
809                         isLeftSteering ? vehicleDoorsState.getRightRear() : vehicleDoorsState.getLeftRear())),
810                 channelToBeUpdated);
811         updateChannel(CHANNEL_GROUP_DOORS, TRUNK,
812                 StringType.valueOf(Converter.toTitleCase(vehicleDoorsState.getTrunk())), channelToBeUpdated);
813         updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(vehicleDoorsState.getHood())),
814                 channelToBeUpdated);
815     }
816
817     private void updateWindows(VehicleWindowsState vehicleWindowState, boolean isLeftSteering,
818             @Nullable String channelToBeUpdated) {
819         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT,
820                 StringType.valueOf(Converter.toTitleCase(
821                         isLeftSteering ? vehicleWindowState.getLeftFront() : vehicleWindowState.getRightFront())),
822                 channelToBeUpdated);
823         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR,
824                 StringType.valueOf(Converter.toTitleCase(
825                         isLeftSteering ? vehicleWindowState.getLeftRear() : vehicleWindowState.getRightRear())),
826                 channelToBeUpdated);
827         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT,
828                 StringType.valueOf(Converter.toTitleCase(
829                         isLeftSteering ? vehicleWindowState.getRightFront() : vehicleWindowState.getLeftFront())),
830                 channelToBeUpdated);
831         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR,
832                 StringType.valueOf(Converter.toTitleCase(
833                         isLeftSteering ? vehicleWindowState.getRightRear() : vehicleWindowState.getLeftRear())),
834                 channelToBeUpdated);
835     }
836
837     private void updateRoof(VehicleRoofState vehicleRoofState, @Nullable String channelToBeUpdated) {
838         updateChannel(CHANNEL_GROUP_DOORS, SUNROOF,
839                 StringType.valueOf(Converter.toTitleCase(vehicleRoofState.getRoofState())), channelToBeUpdated);
840     }
841
842     private void updatePosition(VehicleLocation location, @Nullable String channelToBeUpdated) {
843         if (location.getCoordinates().getLatitude() < 0) {
844             updateChannel(CHANNEL_GROUP_LOCATION, GPS, UnDefType.UNDEF, channelToBeUpdated);
845             updateChannel(CHANNEL_GROUP_LOCATION, HEADING, UnDefType.UNDEF, channelToBeUpdated);
846             updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, UnDefType.UNDEF, channelToBeUpdated);
847             updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, UnDefType.UNDEF, channelToBeUpdated);
848         } else {
849             PointType vehicleLocation = PointType.valueOf(Double.toString(location.getCoordinates().getLatitude()) + ","
850                     + Double.toString(location.getCoordinates().getLongitude()));
851             updateChannel(CHANNEL_GROUP_LOCATION, GPS, vehicleLocation, channelToBeUpdated);
852             updateChannel(CHANNEL_GROUP_LOCATION, HEADING,
853                     QuantityType.valueOf(location.getHeading(), Units.DEGREE_ANGLE), channelToBeUpdated);
854             updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, StringType.valueOf(location.getAddress().getFormatted()),
855                     channelToBeUpdated);
856             PointType homeLocation = locationProvider.getLocation();
857             if (homeLocation != null) {
858                 updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE,
859                         QuantityType.valueOf(vehicleLocation.distanceFrom(homeLocation).intValue(), SIUnits.METRE),
860                         channelToBeUpdated);
861             } else {
862                 updateChannel(CHANNEL_GROUP_LOCATION, HOME_DISTANCE, UnDefType.UNDEF, channelToBeUpdated);
863             }
864         }
865     }
866
867     @Override
868     public void handleCommand(ChannelUID channelUID, Command command) {
869         logger.trace("VehicleHandler.handleCommand {}, {}, {}", command.toFullString(), channelUID.getAsString(),
870                 channelUID.getIdWithoutGroup());
871         String group = channelUID.getGroupId();
872
873         if (group == null) {
874             logger.debug("Cannot handle command {}, no group for channel {}", command.toFullString(),
875                     channelUID.getAsString());
876             return;
877         }
878
879         if (command instanceof RefreshType) {
880             // Refresh of Channels with cached values
881             if (CHANNEL_GROUP_VEHICLE_IMAGE.equals(group)) {
882                 imageCache.ifPresent(image -> updateImage(image));
883             } else {
884                 vehicleStatusCache.ifPresent(
885                         vehicleStatus -> triggerVehicleStatusUpdate(vehicleStatus, channelUID.getIdWithoutGroup()));
886             }
887         } else if (command instanceof StringType) {
888             // Check for Channel Group and corresponding Actions
889             switch (group) {
890                 case CHANNEL_GROUP_REMOTE:
891                     // Executing Remote Services
892                     String serviceCommand = ((StringType) command).toFullString();
893                     remote.ifPresent(remot -> {
894                         RemoteServiceUtils.getRemoteServiceFromCommand(serviceCommand)
895                                 .ifPresentOrElse(service -> remot.execute(service), () -> {
896                                     logger.debug("Remote service execution {} unknown", serviceCommand);
897                                 });
898                     });
899                     break;
900                 case CHANNEL_GROUP_VEHICLE_IMAGE:
901                     // Image Change
902                     vehicleConfiguration.ifPresent(config -> {
903                         if (channelUID.getIdWithoutGroup().equals(IMAGE_VIEWPORT)) {
904                             String newViewport = command.toString();
905                             synchronized (imageProperties) {
906                                 if (!imageProperties.viewport.equals(newViewport)) {
907                                     imageProperties = new ImageProperties(newViewport);
908                                     imageCache = Optional.empty();
909                                     Optional<byte[]> imageContent = proxy.map(prox -> {
910                                         try {
911                                             return prox.requestImage(config.getVin(), config.getVehicleBrand(),
912                                                     imageProperties);
913                                         } catch (NetworkException e) {
914                                             logger.debug("{}", e.toString());
915                                             return "".getBytes();
916                                         }
917                                     });
918                                     imageContent.ifPresent(imageContentData -> updateImage(imageContentData));
919                                 }
920                             }
921                             updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_VIEWPORT, StringType.valueOf(newViewport),
922                                     IMAGE_VIEWPORT);
923                         }
924                     });
925                     break;
926                 case CHANNEL_GROUP_SERVICE:
927                     int serviceIndex = Converter.parseIntegerString(command.toFullString());
928                     if (serviceIndex != -1) {
929                         selectService(serviceIndex, null);
930                     } else {
931                         logger.debug("Cannot select Service index {}", command.toFullString());
932                     }
933                     break;
934                 case CHANNEL_GROUP_CHECK_CONTROL:
935                     int checkControlIndex = Converter.parseIntegerString(command.toFullString());
936                     if (checkControlIndex != -1) {
937                         selectCheckControl(checkControlIndex, null);
938                     } else {
939                         logger.debug("Cannot select CheckControl index {}", command.toFullString());
940                     }
941                     break;
942                 case CHANNEL_GROUP_CHARGE_SESSION:
943                     int sessionIndex = Converter.parseIntegerString(command.toFullString());
944                     if (sessionIndex != -1) {
945                         selectSession(sessionIndex, null);
946                     } else {
947                         logger.debug("Cannot select Session index {}", command.toFullString());
948                     }
949                     break;
950                 default:
951                     logger.debug("Cannot handle command {}, channel {} in group {} not a command channel",
952                             command.toFullString(), channelUID.getAsString(), group);
953             }
954         }
955     }
956
957     private void updateImage(byte[] imageContent) {
958         if (imageContent.length > 0) {
959             imageCache = Optional.of(imageContent);
960             String contentType = HttpUtil.guessContentTypeFromData(imageContent);
961             updateChannel(CHANNEL_GROUP_VEHICLE_IMAGE, IMAGE_FORMAT, new RawType(imageContent, contentType),
962                     IMAGE_FORMAT);
963         } else {
964             synchronized (imageProperties) {
965                 imageProperties.failed();
966             }
967         }
968     }
969 }