]> git.basschouten.com Git - openhab-addons.git/blob
e5241e7fa2bafec1c21d316b107978ce58e5328a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.bmwconnecteddrive.internal.handler;
14
15 import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
16
17 import java.time.DayOfWeek;
18 import java.time.LocalTime;
19 import java.time.ZoneId;
20 import java.time.ZonedDateTime;
21 import java.util.ArrayList;
22 import java.util.EnumSet;
23 import java.util.List;
24 import java.util.Optional;
25 import java.util.Set;
26
27 import javax.measure.quantity.Length;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
31 import org.openhab.binding.bmwconnecteddrive.internal.dto.Destination;
32 import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips;
33 import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip;
34 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CBSMessage;
35 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.CCMMessage;
36 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Doors;
37 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Position;
38 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
39 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Windows;
40 import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils;
41 import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileUtils.TimedChannel;
42 import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper;
43 import org.openhab.binding.bmwconnecteddrive.internal.utils.ChargeProfileWrapper.ProfileKey;
44 import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
45 import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
46 import org.openhab.binding.bmwconnecteddrive.internal.utils.RemoteServiceUtils;
47 import org.openhab.binding.bmwconnecteddrive.internal.utils.VehicleStatusUtils;
48 import org.openhab.core.library.types.DateTimeType;
49 import org.openhab.core.library.types.OnOffType;
50 import org.openhab.core.library.types.PointType;
51 import org.openhab.core.library.types.QuantityType;
52 import org.openhab.core.library.types.StringType;
53 import org.openhab.core.library.unit.ImperialUnits;
54 import org.openhab.core.library.unit.Units;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.binding.BaseThingHandler;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.StateOption;
60 import org.openhab.core.types.UnDefType;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 import com.google.gson.JsonSyntaxException;
65
66 /**
67  * The {@link VehicleChannelHandler} is responsible for handling commands, which are
68  * sent to one of the channels.
69  *
70  * @author Bernd Weymann - Initial contribution
71  * @author Norbert Truchsess - edit & send of charge profile
72  */
73 @NonNullByDefault
74 public abstract class VehicleChannelHandler extends BaseThingHandler {
75     protected final Logger logger = LoggerFactory.getLogger(VehicleChannelHandler.class);
76     protected boolean imperial = false;
77     protected boolean hasFuel = false;
78     protected boolean isElectric = false;
79     protected boolean isHybrid = false;
80
81     // List Interfaces
82     protected List<CBSMessage> serviceList = new ArrayList<CBSMessage>();
83     protected String selectedService = Constants.UNDEF;
84     protected List<CCMMessage> checkControlList = new ArrayList<CCMMessage>();
85     protected String selectedCC = Constants.UNDEF;
86     protected List<Destination> destinationList = new ArrayList<Destination>();
87     protected String selectedDestination = Constants.UNDEF;
88
89     protected BMWConnectedDriveOptionProvider optionProvider;
90
91     // Data Caches
92     protected Optional<String> vehicleStatusCache = Optional.empty();
93     protected Optional<String> lastTripCache = Optional.empty();
94     protected Optional<String> allTripsCache = Optional.empty();
95     protected Optional<String> chargeProfileCache = Optional.empty();
96     protected Optional<String> rangeMapCache = Optional.empty();
97     protected Optional<String> destinationCache = Optional.empty();
98     protected Optional<byte[]> imageCache = Optional.empty();
99
100     public VehicleChannelHandler(Thing thing, BMWConnectedDriveOptionProvider op, String type, boolean imperial) {
101         super(thing);
102         optionProvider = op;
103
104         this.imperial = imperial;
105         hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
106                 || type.equals(VehicleType.ELECTRIC_REX.toString());
107         isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
108                 || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
109         isHybrid = hasFuel && isElectric;
110
111         setOptions(CHANNEL_GROUP_REMOTE, REMOTE_SERVICE_COMMAND, RemoteServiceUtils.getOptions(isElectric));
112     }
113
114     private void setOptions(final String group, final String id, List<StateOption> options) {
115         optionProvider.setStateOptions(new ChannelUID(thing.getUID(), group, id), options);
116     }
117
118     protected void updateChannel(final String group, final String id, final State state) {
119         updateState(new ChannelUID(thing.getUID(), group, id), state);
120     }
121
122     protected void updateCheckControls(List<CCMMessage> ccl) {
123         if (ccl.isEmpty()) {
124             // No Check Control available - show not active
125             CCMMessage ccm = new CCMMessage();
126             ccm.ccmDescriptionLong = Constants.NO_ENTRIES;
127             ccm.ccmDescriptionShort = Constants.NO_ENTRIES;
128             ccm.ccmId = -1;
129             ccm.ccmMileage = -1;
130             ccl.add(ccm);
131         }
132
133         // add all elements to options
134         checkControlList = ccl;
135         List<StateOption> ccmDescriptionOptions = new ArrayList<>();
136         List<StateOption> ccmDetailsOptions = new ArrayList<>();
137         List<StateOption> ccmMileageOptions = new ArrayList<>();
138         boolean isSelectedElementIn = false;
139         int index = 0;
140         for (CCMMessage ccEntry : checkControlList) {
141             ccmDescriptionOptions.add(new StateOption(Integer.toString(index), ccEntry.ccmDescriptionShort));
142             ccmDetailsOptions.add(new StateOption(Integer.toString(index), ccEntry.ccmDescriptionLong));
143             ccmMileageOptions.add(new StateOption(Integer.toString(index), Integer.toString(ccEntry.ccmMileage)));
144             if (selectedCC.equals(ccEntry.ccmDescriptionShort)) {
145                 isSelectedElementIn = true;
146             }
147             index++;
148         }
149         setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
150         setOptions(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, ccmDetailsOptions);
151         setOptions(CHANNEL_GROUP_CHECK_CONTROL, MILEAGE, ccmMileageOptions);
152
153         // if current selected item isn't anymore in the list select first entry
154         if (!isSelectedElementIn) {
155             selectCheckControl(0);
156         }
157     }
158
159     protected void selectCheckControl(int index) {
160         if (index >= 0 && index < checkControlList.size()) {
161             CCMMessage ccEntry = checkControlList.get(index);
162             selectedCC = ccEntry.ccmDescriptionShort;
163             updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, StringType.valueOf(ccEntry.ccmDescriptionShort));
164             updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, StringType.valueOf(ccEntry.ccmDescriptionLong));
165             updateChannel(CHANNEL_GROUP_CHECK_CONTROL, MILEAGE, QuantityType.valueOf(
166                     Converter.round(ccEntry.ccmMileage), imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
167         }
168     }
169
170     protected void updateServices(List<CBSMessage> sl) {
171         // if list is empty add "undefined" element
172         if (sl.isEmpty()) {
173             CBSMessage cbsm = new CBSMessage();
174             cbsm.cbsType = Constants.NO_ENTRIES;
175             cbsm.cbsDescription = Constants.NO_ENTRIES;
176             sl.add(cbsm);
177         }
178
179         // add all elements to options
180         serviceList = sl;
181         List<StateOption> serviceNameOptions = new ArrayList<>();
182         List<StateOption> serviceDetailsOptions = new ArrayList<>();
183         List<StateOption> serviceDateOptions = new ArrayList<>();
184         List<StateOption> serviceMileageOptions = new ArrayList<>();
185         boolean isSelectedElementIn = false;
186         int index = 0;
187         for (CBSMessage serviceEntry : serviceList) {
188             // create StateOption with "value = list index" and "label = human readable string"
189             serviceNameOptions.add(new StateOption(Integer.toString(index), serviceEntry.getType()));
190             serviceDetailsOptions.add(new StateOption(Integer.toString(index), serviceEntry.getDescription()));
191             serviceDateOptions.add(new StateOption(Integer.toString(index), serviceEntry.getDueDate()));
192             serviceMileageOptions
193                     .add(new StateOption(Integer.toString(index), Integer.toString(serviceEntry.cbsRemainingMileage)));
194             if (selectedService.equals(serviceEntry.getType())) {
195                 isSelectedElementIn = true;
196             }
197             index++;
198         }
199         setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
200         setOptions(CHANNEL_GROUP_SERVICE, DETAILS, serviceDetailsOptions);
201         setOptions(CHANNEL_GROUP_SERVICE, DATE, serviceDateOptions);
202         setOptions(CHANNEL_GROUP_SERVICE, MILEAGE, serviceMileageOptions);
203
204         // if current selected item isn't anymore in the list select first entry
205         if (!isSelectedElementIn) {
206             selectService(0);
207         }
208     }
209
210     protected void selectService(int index) {
211         if (index >= 0 && index < serviceList.size()) {
212             CBSMessage serviceEntry = serviceList.get(index);
213             selectedService = serviceEntry.cbsType;
214             updateChannel(CHANNEL_GROUP_SERVICE, NAME,
215                     StringType.valueOf(Converter.toTitleCase(serviceEntry.getType())));
216             updateChannel(CHANNEL_GROUP_SERVICE, DETAILS,
217                     StringType.valueOf(Converter.toTitleCase(serviceEntry.getDescription())));
218             updateChannel(CHANNEL_GROUP_SERVICE, DATE,
219                     DateTimeType.valueOf(Converter.getLocalDateTime(serviceEntry.getDueDate())));
220             updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
221                     QuantityType.valueOf(Converter.round(serviceEntry.cbsRemainingMileage),
222                             imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
223         }
224     }
225
226     protected void updateDestinations(List<Destination> dl) {
227         // if list is empty add "undefined" element
228         if (dl.isEmpty()) {
229             Destination dest = new Destination();
230             dest.city = Constants.NO_ENTRIES;
231             dest.lat = -1;
232             dest.lon = -1;
233             dl.add(dest);
234         }
235
236         // add all elements to options
237         destinationList = dl;
238         List<StateOption> destinationNameOptions = new ArrayList<>();
239         List<StateOption> destinationGPSOptions = new ArrayList<>();
240         boolean isSelectedElementIn = false;
241         int index = 0;
242         for (Destination destination : destinationList) {
243             destinationNameOptions.add(new StateOption(Integer.toString(index), destination.getAddress()));
244             destinationGPSOptions.add(new StateOption(Integer.toString(index), destination.getCoordinates()));
245             if (selectedDestination.equals(destination.getAddress())) {
246                 isSelectedElementIn = true;
247             }
248             index++;
249         }
250         setOptions(CHANNEL_GROUP_DESTINATION, NAME, destinationNameOptions);
251         setOptions(CHANNEL_GROUP_DESTINATION, GPS, destinationGPSOptions);
252
253         // if current selected item isn't anymore in the list select first entry
254         if (!isSelectedElementIn) {
255             selectDestination(0);
256         }
257     }
258
259     protected void selectDestination(int index) {
260         if (index >= 0 && index < destinationList.size()) {
261             Destination destinationEntry = destinationList.get(index);
262             // update selected Item
263             selectedDestination = destinationEntry.getAddress();
264             // update coordinates according to new set location
265             updateChannel(CHANNEL_GROUP_DESTINATION, NAME, StringType.valueOf(destinationEntry.getAddress()));
266             updateChannel(CHANNEL_GROUP_DESTINATION, GPS, PointType.valueOf(destinationEntry.getCoordinates()));
267         }
268     }
269
270     protected void updateAllTrips(AllTrips allTrips) {
271         QuantityType<Length> qtTotalElectric = QuantityType
272                 .valueOf(Converter.round(allTrips.totalElectricDistance.userTotal), Constants.KILOMETRE_UNIT);
273         QuantityType<Length> qtLongestElectricRange = QuantityType
274                 .valueOf(Converter.round(allTrips.chargecycleRange.userHigh), Constants.KILOMETRE_UNIT);
275         QuantityType<Length> qtDistanceSinceCharge = QuantityType
276                 .valueOf(Converter.round(allTrips.chargecycleRange.userCurrentChargeCycle), Constants.KILOMETRE_UNIT);
277
278         updateChannel(CHANNEL_GROUP_LIFETIME, TOTAL_DRIVEN_DISTANCE,
279                 imperial ? Converter.getMiles(qtTotalElectric) : qtTotalElectric);
280         updateChannel(CHANNEL_GROUP_LIFETIME, SINGLE_LONGEST_DISTANCE,
281                 imperial ? Converter.getMiles(qtLongestElectricRange) : qtLongestElectricRange);
282         updateChannel(CHANNEL_GROUP_LAST_TRIP, DISTANCE_SINCE_CHARGING,
283                 imperial ? Converter.getMiles(qtDistanceSinceCharge) : qtDistanceSinceCharge);
284
285         // Conversion from kwh/100km to kwh/10mi has to be done manually
286         double avgConsumotion = imperial ? allTrips.avgElectricConsumption.userAverage * Converter.MILES_TO_KM_RATIO
287                 : allTrips.avgElectricConsumption.userAverage;
288         double avgCombinedConsumption = imperial
289                 ? allTrips.avgCombinedConsumption.userAverage * Converter.MILES_TO_KM_RATIO
290                 : allTrips.avgCombinedConsumption.userAverage;
291         double avgRecuperation = imperial ? allTrips.avgRecuperation.userAverage * Converter.MILES_TO_KM_RATIO
292                 : allTrips.avgRecuperation.userAverage;
293
294         updateChannel(CHANNEL_GROUP_LIFETIME, AVG_CONSUMPTION,
295                 QuantityType.valueOf(Converter.round(avgConsumotion), Units.KILOWATT_HOUR));
296         updateChannel(CHANNEL_GROUP_LIFETIME, AVG_COMBINED_CONSUMPTION,
297                 QuantityType.valueOf(Converter.round(avgCombinedConsumption), Units.LITRE));
298         updateChannel(CHANNEL_GROUP_LIFETIME, AVG_RECUPERATION,
299                 QuantityType.valueOf(Converter.round(avgRecuperation), Units.KILOWATT_HOUR));
300     }
301
302     protected void updateLastTrip(LastTrip trip) {
303         // Whyever the Last Trip DateTime is delivered without offest - so LocalTime
304         updateChannel(CHANNEL_GROUP_LAST_TRIP, DATE,
305                 DateTimeType.valueOf(Converter.getLocalDateTimeWithoutOffest(trip.date)));
306         updateChannel(CHANNEL_GROUP_LAST_TRIP, DURATION, QuantityType.valueOf(trip.duration, Units.MINUTE));
307
308         QuantityType<Length> qtTotalDistance = QuantityType.valueOf(Converter.round(trip.totalDistance),
309                 Constants.KILOMETRE_UNIT);
310         updateChannel(CHANNEL_GROUP_LAST_TRIP, DISTANCE,
311                 imperial ? Converter.getMiles(qtTotalDistance) : qtTotalDistance);
312
313         // Conversion from kwh/100km to kwh/10mi has to be done manually
314         double avgConsumtption = imperial ? trip.avgElectricConsumption * Converter.MILES_TO_KM_RATIO
315                 : trip.avgElectricConsumption;
316         double avgCombinedConsumption = imperial ? trip.avgCombinedConsumption * Converter.MILES_TO_KM_RATIO
317                 : trip.avgCombinedConsumption;
318         double avgRecuperation = imperial ? trip.avgRecuperation * Converter.MILES_TO_KM_RATIO : trip.avgRecuperation;
319
320         updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_CONSUMPTION,
321                 QuantityType.valueOf(Converter.round(avgConsumtption), Units.KILOWATT_HOUR));
322         updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_COMBINED_CONSUMPTION,
323                 QuantityType.valueOf(Converter.round(avgCombinedConsumption), Units.LITRE));
324         updateChannel(CHANNEL_GROUP_LAST_TRIP, AVG_RECUPERATION,
325                 QuantityType.valueOf(Converter.round(avgRecuperation), Units.KILOWATT_HOUR));
326     }
327
328     protected void updateChargeProfileFromContent(String content) {
329         ChargeProfileWrapper.fromJson(content).ifPresent(this::updateChargeProfile);
330     }
331
332     protected void updateChargeProfile(ChargeProfileWrapper wrapper) {
333         updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_PREFERENCE,
334                 StringType.valueOf(Converter.toTitleCase(wrapper.getPreference())));
335         updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_MODE,
336                 StringType.valueOf(Converter.toTitleCase(wrapper.getMode())));
337         final Boolean climate = wrapper.isEnabled(ProfileKey.CLIMATE);
338         updateChannel(CHANNEL_GROUP_CHARGE, CHARGE_PROFILE_CLIMATE,
339                 climate == null ? UnDefType.UNDEF : OnOffType.from(climate));
340         updateTimedState(wrapper, ProfileKey.WINDOWSTART);
341         updateTimedState(wrapper, ProfileKey.WINDOWEND);
342         updateTimedState(wrapper, ProfileKey.TIMER1);
343         updateTimedState(wrapper, ProfileKey.TIMER2);
344         updateTimedState(wrapper, ProfileKey.TIMER3);
345         updateTimedState(wrapper, ProfileKey.OVERRIDE);
346     }
347
348     protected void updateTimedState(ChargeProfileWrapper profile, ProfileKey key) {
349         final TimedChannel timed = ChargeProfileUtils.getTimedChannel(key);
350         if (timed != null) {
351             final LocalTime time = profile.getTime(key);
352             updateChannel(CHANNEL_GROUP_CHARGE, timed.time, time == null ? UnDefType.UNDEF
353                     : new DateTimeType(ZonedDateTime.of(Constants.EPOCH_DAY, time, ZoneId.systemDefault())));
354             if (timed.timer != null) {
355                 final Boolean enabled = profile.isEnabled(key);
356                 updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + CHARGE_ENABLED,
357                         enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled));
358                 if (timed.hasDays) {
359                     final Set<DayOfWeek> days = profile.getDays(key);
360                     updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + CHARGE_DAYS,
361                             days == null ? UnDefType.UNDEF : StringType.valueOf(ChargeProfileUtils.formatDays(days)));
362                     EnumSet.allOf(DayOfWeek.class).forEach(day -> {
363                         updateChannel(CHANNEL_GROUP_CHARGE, timed.timer + ChargeProfileUtils.getDaysChannel(day),
364                                 days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)));
365                     });
366                 }
367             }
368         }
369     }
370
371     protected void updateDoors(Doors doorState) {
372         updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT,
373                 StringType.valueOf(Converter.toTitleCase(doorState.doorDriverFront)));
374         updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR,
375                 StringType.valueOf(Converter.toTitleCase(doorState.doorDriverRear)));
376         updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT,
377                 StringType.valueOf(Converter.toTitleCase(doorState.doorPassengerFront)));
378         updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR,
379                 StringType.valueOf(Converter.toTitleCase(doorState.doorPassengerRear)));
380         updateChannel(CHANNEL_GROUP_DOORS, TRUNK, StringType.valueOf(Converter.toTitleCase(doorState.trunk)));
381         updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(doorState.hood)));
382     }
383
384     protected void updateWindows(Windows windowState) {
385         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT,
386                 StringType.valueOf(Converter.toTitleCase(windowState.windowDriverFront)));
387         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR,
388                 StringType.valueOf(Converter.toTitleCase(windowState.windowDriverRear)));
389         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT,
390                 StringType.valueOf(Converter.toTitleCase(windowState.windowPassengerFront)));
391         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR,
392                 StringType.valueOf(Converter.toTitleCase(windowState.windowPassengerRear)));
393         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_REAR,
394                 StringType.valueOf(Converter.toTitleCase(windowState.rearWindow)));
395         updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, StringType.valueOf(Converter.toTitleCase(windowState.sunroof)));
396     }
397
398     protected void updatePosition(Position pos) {
399         updateChannel(CHANNEL_GROUP_LOCATION, GPS, PointType.valueOf(pos.getCoordinates()));
400         updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE));
401     }
402
403     protected void updateVehicleStatus(VehicleStatus vStatus) {
404         // Vehicle Status
405         updateChannel(CHANNEL_GROUP_STATUS, LOCK, StringType.valueOf(Converter.toTitleCase(vStatus.doorLockState)));
406
407         // Service Updates
408         updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE,
409                 DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getNextServiceDate(vStatus))));
410
411         updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE,
412                 QuantityType.valueOf(Converter.round(VehicleStatusUtils.getNextServiceMileage(vStatus)),
413                         imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
414         // CheckControl Active?
415         updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL,
416                 StringType.valueOf(Converter.toTitleCase(VehicleStatusUtils.checkControlActive(vStatus))));
417         // last update Time
418         updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
419                 DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus))));
420         // last update reason
421         updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE_REASON,
422                 StringType.valueOf(Converter.toTitleCase(vStatus.updateReason)));
423
424         Doors doorState = null;
425         try {
426             doorState = Converter.getGson().fromJson(Converter.getGson().toJson(vStatus), Doors.class);
427         } catch (JsonSyntaxException jse) {
428             logger.debug("Doors parse exception {}", jse.getMessage());
429         }
430         if (doorState != null) {
431             updateChannel(CHANNEL_GROUP_STATUS, DOORS, StringType.valueOf(VehicleStatusUtils.checkClosed(doorState)));
432             updateDoors(doorState);
433         }
434         Windows windowState = null;
435         try {
436             windowState = Converter.getGson().fromJson(Converter.getGson().toJson(vStatus), Windows.class);
437         } catch (JsonSyntaxException jse) {
438             logger.debug("Windows parse exception {}", jse.getMessage());
439         }
440         if (windowState != null) {
441             updateChannel(CHANNEL_GROUP_STATUS, WINDOWS,
442                     StringType.valueOf(VehicleStatusUtils.checkClosed(windowState)));
443             updateWindows(windowState);
444         }
445
446         // Range values
447         // based on unit of length decide if range shall be reported in km or miles
448         double totalRange = 0;
449         double maxTotalRange = 0;
450         if (isElectric) {
451             totalRange += vStatus.remainingRangeElectric;
452             QuantityType<Length> qtElectricRange = QuantityType.valueOf(vStatus.remainingRangeElectric,
453                     Constants.KILOMETRE_UNIT);
454             QuantityType<Length> qtElectricRadius = QuantityType
455                     .valueOf(Converter.guessRangeRadius(vStatus.remainingRangeElectric), Constants.KILOMETRE_UNIT);
456
457             updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC,
458                     imperial ? Converter.getMiles(qtElectricRange) : qtElectricRange);
459             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC,
460                     imperial ? Converter.getMiles(qtElectricRadius) : qtElectricRadius);
461
462             maxTotalRange += vStatus.maxRangeElectric;
463             QuantityType<Length> qtMaxElectricRange = QuantityType.valueOf(vStatus.maxRangeElectric,
464                     Constants.KILOMETRE_UNIT);
465             QuantityType<Length> qtMaxElectricRadius = QuantityType
466                     .valueOf(Converter.guessRangeRadius(vStatus.maxRangeElectric), Constants.KILOMETRE_UNIT);
467
468             updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC_MAX,
469                     imperial ? Converter.getMiles(qtMaxElectricRange) : qtMaxElectricRange);
470             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC_MAX,
471                     imperial ? Converter.getMiles(qtMaxElectricRadius) : qtMaxElectricRadius);
472         }
473         if (hasFuel) {
474             totalRange += vStatus.remainingRangeFuel;
475             maxTotalRange += vStatus.remainingRangeFuel;
476             QuantityType<Length> qtFuelRange = QuantityType.valueOf(vStatus.remainingRangeFuel,
477                     Constants.KILOMETRE_UNIT);
478             QuantityType<Length> qtFuelRadius = QuantityType
479                     .valueOf(Converter.guessRangeRadius(vStatus.remainingRangeFuel), Constants.KILOMETRE_UNIT);
480
481             updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, imperial ? Converter.getMiles(qtFuelRange) : qtFuelRange);
482             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL,
483                     imperial ? Converter.getMiles(qtFuelRadius) : qtFuelRadius);
484         }
485         if (isHybrid) {
486             QuantityType<Length> qtHybridRange = QuantityType.valueOf(totalRange, Constants.KILOMETRE_UNIT);
487             QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(totalRange),
488                     Constants.KILOMETRE_UNIT);
489             QuantityType<Length> qtMaxHybridRange = QuantityType.valueOf(maxTotalRange, Constants.KILOMETRE_UNIT);
490             QuantityType<Length> qtMaxHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(maxTotalRange),
491                     Constants.KILOMETRE_UNIT);
492             updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID,
493                     imperial ? Converter.getMiles(qtHybridRange) : qtHybridRange);
494             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID,
495                     imperial ? Converter.getMiles(qtHybridRadius) : qtHybridRadius);
496             updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID_MAX,
497                     imperial ? Converter.getMiles(qtMaxHybridRange) : qtMaxHybridRange);
498             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID_MAX,
499                     imperial ? Converter.getMiles(qtMaxHybridRadius) : qtMaxHybridRadius);
500         }
501
502         updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
503                 QuantityType.valueOf(vStatus.mileage, imperial ? ImperialUnits.MILE : Constants.KILOMETRE_UNIT));
504         if (isElectric) {
505             updateChannel(CHANNEL_GROUP_RANGE, SOC, QuantityType.valueOf(vStatus.chargingLevelHv, Units.PERCENT));
506         }
507         if (hasFuel) {
508             updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
509                     QuantityType.valueOf(vStatus.remainingFuel, Units.LITRE));
510         }
511
512         // Charge Values
513         if (isElectric) {
514             if (vStatus.connectionStatus != null) {
515                 updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION,
516                         StringType.valueOf(Converter.toTitleCase(vStatus.connectionStatus)));
517             } else {
518                 updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION, UnDefType.NULL);
519             }
520             if (vStatus.chargingStatus != null) {
521                 if (Constants.INVALID.equals(vStatus.chargingStatus)) {
522                     updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
523                             StringType.valueOf(Converter.toTitleCase(vStatus.lastChargingEndReason)));
524                 } else {
525                     // State INVALID is somehow misleading. Instead show the Last Charging End Reason
526                     updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
527                             StringType.valueOf(Converter.toTitleCase(vStatus.chargingStatus)));
528                 }
529             } else {
530                 updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS, UnDefType.NULL);
531             }
532             if (vStatus.chargingTimeRemaining != null) {
533                 try {
534                     updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING,
535                             QuantityType.valueOf(vStatus.chargingTimeRemaining, Units.MINUTE));
536                 } catch (NumberFormatException nfe) {
537                     updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING, UnDefType.UNDEF);
538                 }
539             } else {
540                 updateChannel(CHANNEL_GROUP_STATUS, CHARGE_REMAINING, UnDefType.NULL);
541             }
542         }
543     }
544 }