]> git.basschouten.com Git - openhab-addons.git/blob
1dec0d62e6bba98429621d31d151037780fffb33
[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.mybmw.internal.handler;
14
15 import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
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.Unit;
28 import javax.measure.quantity.Length;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType;
33 import org.openhab.binding.mybmw.internal.dto.charge.ChargeProfile;
34 import org.openhab.binding.mybmw.internal.dto.charge.ChargeSession;
35 import org.openhab.binding.mybmw.internal.dto.charge.ChargeStatisticsContainer;
36 import org.openhab.binding.mybmw.internal.dto.charge.ChargingSettings;
37 import org.openhab.binding.mybmw.internal.dto.properties.CBS;
38 import org.openhab.binding.mybmw.internal.dto.properties.DoorsWindows;
39 import org.openhab.binding.mybmw.internal.dto.properties.Location;
40 import org.openhab.binding.mybmw.internal.dto.properties.Tires;
41 import org.openhab.binding.mybmw.internal.dto.status.CCMMessage;
42 import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
43 import org.openhab.binding.mybmw.internal.utils.ChargeProfileUtils;
44 import org.openhab.binding.mybmw.internal.utils.ChargeProfileUtils.TimedChannel;
45 import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper;
46 import org.openhab.binding.mybmw.internal.utils.ChargeProfileWrapper.ProfileKey;
47 import org.openhab.binding.mybmw.internal.utils.Constants;
48 import org.openhab.binding.mybmw.internal.utils.Converter;
49 import org.openhab.binding.mybmw.internal.utils.RemoteServiceUtils;
50 import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
51 import org.openhab.core.library.types.DateTimeType;
52 import org.openhab.core.library.types.DecimalType;
53 import org.openhab.core.library.types.OnOffType;
54 import org.openhab.core.library.types.PointType;
55 import org.openhab.core.library.types.QuantityType;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.library.unit.ImperialUnits;
58 import org.openhab.core.library.unit.Units;
59 import org.openhab.core.thing.ChannelUID;
60 import org.openhab.core.thing.Thing;
61 import org.openhab.core.thing.binding.BaseThingHandler;
62 import org.openhab.core.types.CommandOption;
63 import org.openhab.core.types.State;
64 import org.openhab.core.types.UnDefType;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 /**
69  * The {@link VehicleChannelHandler} handles Channel updates
70  *
71  * @author Bernd Weymann - Initial contribution
72  * @author Norbert Truchsess - edit & send of charge profile
73  */
74 @NonNullByDefault
75 public abstract class VehicleChannelHandler extends BaseThingHandler {
76     protected final Logger logger = LoggerFactory.getLogger(VehicleChannelHandler.class);
77     protected boolean hasFuel = false;
78     protected boolean isElectric = false;
79     protected boolean isHybrid = false;
80
81     // List Interfaces
82     protected List<CBS> serviceList = new ArrayList<CBS>();
83     protected String selectedService = Constants.UNDEF;
84     protected List<CCMMessage> checkControlList = new ArrayList<CCMMessage>();
85     protected String selectedCC = Constants.UNDEF;
86     protected List<ChargeSession> sessionList = new ArrayList<ChargeSession>();
87     protected String selectedSession = Constants.UNDEF;
88
89     protected MyBMWCommandOptionProvider commandOptionProvider;
90
91     // Data Caches
92     protected Optional<String> vehicleStatusCache = Optional.empty();
93     protected Optional<byte[]> imageCache = Optional.empty();
94
95     public VehicleChannelHandler(Thing thing, MyBMWCommandOptionProvider cop, String type) {
96         super(thing);
97         commandOptionProvider = cop;
98
99         hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
100                 || type.equals(VehicleType.ELECTRIC_REX.toString());
101         isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
102                 || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
103         isHybrid = hasFuel && isElectric;
104
105         setOptions(CHANNEL_GROUP_REMOTE, REMOTE_SERVICE_COMMAND, RemoteServiceUtils.getOptions(isElectric));
106     }
107
108     private void setOptions(final String group, final String id, List<CommandOption> options) {
109         commandOptionProvider.setCommandOptions(new ChannelUID(thing.getUID(), group, id), options);
110     }
111
112     protected void updateChannel(final String group, final String id, final State state) {
113         updateState(new ChannelUID(thing.getUID(), group, id), state);
114     }
115
116     protected void updateChargeStatistics(ChargeStatisticsContainer csc) {
117         updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, TITLE, StringType.valueOf(csc.description));
118         updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, ENERGY,
119                 QuantityType.valueOf(csc.statistics.totalEnergyCharged, Units.KILOWATT_HOUR));
120         updateChannel(CHANNEL_GROUP_CHARGE_STATISTICS, SESSIONS,
121                 DecimalType.valueOf(Integer.toString(csc.statistics.numberOfChargingSessions)));
122     }
123
124     protected void updateVehicle(Vehicle v) {
125         updateVehicleStatus(v);
126         updateRange(v);
127         updateDoors(v.properties.doorsAndWindows);
128         updateWindows(v.properties.doorsAndWindows);
129         updatePosition(v.properties.vehicleLocation);
130         updateServices(v.properties.serviceRequired);
131         updateCheckControls(v.status.checkControlMessages);
132         updateTires(v.properties.tires);
133     }
134
135     private void updateTires(@Nullable Tires tires) {
136         if (tires == null) {
137             updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT, UnDefType.UNDEF);
138             updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET, UnDefType.UNDEF);
139             updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT, UnDefType.UNDEF);
140             updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET, UnDefType.UNDEF);
141             updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT, UnDefType.UNDEF);
142             updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET, UnDefType.UNDEF);
143             updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT, UnDefType.UNDEF);
144             updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET, UnDefType.UNDEF);
145         } else {
146             updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_CURRENT,
147                     QuantityType.valueOf(tires.frontLeft.status.currentPressure / 100, Units.BAR));
148             updateChannel(CHANNEL_GROUP_TIRES, FRONT_LEFT_TARGET,
149                     QuantityType.valueOf(tires.frontLeft.status.targetPressure / 100, Units.BAR));
150             updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_CURRENT,
151                     QuantityType.valueOf(tires.frontRight.status.currentPressure / 100, Units.BAR));
152             updateChannel(CHANNEL_GROUP_TIRES, FRONT_RIGHT_TARGET,
153                     QuantityType.valueOf(tires.frontRight.status.targetPressure / 100, Units.BAR));
154             updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_CURRENT,
155                     QuantityType.valueOf(tires.rearLeft.status.currentPressure / 100, Units.BAR));
156             updateChannel(CHANNEL_GROUP_TIRES, REAR_LEFT_TARGET,
157                     QuantityType.valueOf(tires.rearLeft.status.targetPressure / 100, Units.BAR));
158             updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_CURRENT,
159                     QuantityType.valueOf(tires.rearRight.status.currentPressure / 100, Units.BAR));
160             updateChannel(CHANNEL_GROUP_TIRES, REAR_RIGHT_TARGET,
161                     QuantityType.valueOf(tires.rearRight.status.targetPressure / 100, Units.BAR));
162         }
163     }
164
165     protected void updateVehicleStatus(Vehicle v) {
166         updateChannel(CHANNEL_GROUP_STATUS, LOCK, Converter.getLockState(v.properties.areDoorsLocked));
167         updateChannel(CHANNEL_GROUP_STATUS, SERVICE_DATE,
168                 VehicleStatusUtils.getNextServiceDate(v.properties.serviceRequired));
169         updateChannel(CHANNEL_GROUP_STATUS, SERVICE_MILEAGE,
170                 VehicleStatusUtils.getNextServiceMileage(v.properties.serviceRequired));
171         updateChannel(CHANNEL_GROUP_STATUS, CHECK_CONTROL,
172                 StringType.valueOf(v.status.checkControlMessagesGeneralState));
173         updateChannel(CHANNEL_GROUP_STATUS, MOTION, OnOffType.from(v.properties.inMotion));
174         updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE,
175                 DateTimeType.valueOf(Converter.zonedToLocalDateTime(v.properties.lastUpdatedAt)));
176         updateChannel(CHANNEL_GROUP_STATUS, DOORS, Converter.getClosedState(v.properties.areDoorsClosed));
177         updateChannel(CHANNEL_GROUP_STATUS, WINDOWS, Converter.getClosedState(v.properties.areWindowsClosed));
178
179         if (isElectric) {
180             updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION,
181                     Converter.getConnectionState(v.properties.chargingState.isChargerConnected));
182             updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS,
183                     StringType.valueOf(Converter.toTitleCase(VehicleStatusUtils.getChargStatus(v))));
184             updateChannel(CHANNEL_GROUP_STATUS, CHARGE_INFO,
185                     StringType.valueOf(Converter.getLocalTime(VehicleStatusUtils.getChargeInfo(v))));
186         }
187     }
188
189     protected void updateRange(Vehicle v) {
190         // get the right unit
191         Unit<Length> lengthUnit = VehicleStatusUtils.getLengthUnit(v.status.fuelIndicators);
192         if (lengthUnit == null) {
193             return;
194         }
195         if (isElectric) {
196             int rangeElectric = VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, v);
197             QuantityType<Length> qtElectricRange = QuantityType.valueOf(rangeElectric, lengthUnit);
198             QuantityType<Length> qtElectricRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeElectric),
199                     lengthUnit);
200             updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC, qtElectricRange);
201             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC, qtElectricRadius);
202         }
203         if (hasFuel) {
204             int rangeFuel = VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, v);
205             QuantityType<Length> qtFuelRange = QuantityType.valueOf(rangeFuel, lengthUnit);
206             QuantityType<Length> qtFuelRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeFuel), lengthUnit);
207             updateChannel(CHANNEL_GROUP_RANGE, RANGE_FUEL, qtFuelRange);
208             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_FUEL, qtFuelRadius);
209         }
210         if (isHybrid) {
211             int rangeCombined = VehicleStatusUtils.getRange(Constants.PHEV, v);
212             QuantityType<Length> qtHybridRange = QuantityType.valueOf(rangeCombined, lengthUnit);
213             QuantityType<Length> qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(rangeCombined),
214                     lengthUnit);
215             updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID, qtHybridRange);
216             updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID, qtHybridRadius);
217         }
218         if (v.status.currentMileage.mileage == Constants.INT_UNDEF) {
219             updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, UnDefType.UNDEF);
220         } else {
221             updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
222                     QuantityType.valueOf(v.status.currentMileage.mileage, lengthUnit));
223         }
224         if (isElectric) {
225             updateChannel(CHANNEL_GROUP_RANGE, SOC,
226                     QuantityType.valueOf(v.properties.chargingState.chargePercentage, Units.PERCENT));
227         }
228         if (hasFuel) {
229             updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
230                     QuantityType.valueOf(v.properties.fuelLevel.value, Units.LITRE));
231         }
232     }
233
234     protected void updateCheckControls(List<CCMMessage> ccl) {
235         if (ccl.isEmpty()) {
236             // No Check Control available - show not active
237             CCMMessage ccm = new CCMMessage();
238             ccm.title = Constants.NO_ENTRIES;
239             ccm.longDescription = Constants.NO_ENTRIES;
240             ccm.state = Constants.NO_ENTRIES;
241             ccl.add(ccm);
242         }
243
244         // add all elements to options
245         checkControlList = ccl;
246         List<CommandOption> ccmDescriptionOptions = new ArrayList<>();
247         boolean isSelectedElementIn = false;
248         int index = 0;
249         for (CCMMessage ccEntry : checkControlList) {
250             ccmDescriptionOptions.add(new CommandOption(Integer.toString(index), ccEntry.title));
251             if (selectedCC.equals(ccEntry.title)) {
252                 isSelectedElementIn = true;
253             }
254             index++;
255         }
256         setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
257
258         // if current selected item isn't anymore in the list select first entry
259         if (!isSelectedElementIn) {
260             selectCheckControl(0);
261         }
262     }
263
264     protected void selectCheckControl(int index) {
265         if (index >= 0 && index < checkControlList.size()) {
266             CCMMessage ccEntry = checkControlList.get(index);
267             selectedCC = ccEntry.title;
268             updateChannel(CHANNEL_GROUP_CHECK_CONTROL, NAME, StringType.valueOf(ccEntry.title));
269             updateChannel(CHANNEL_GROUP_CHECK_CONTROL, DETAILS, StringType.valueOf(ccEntry.longDescription));
270             updateChannel(CHANNEL_GROUP_CHECK_CONTROL, SEVERITY, StringType.valueOf(ccEntry.state));
271         }
272     }
273
274     protected void updateServices(List<CBS> sl) {
275         // if list is empty add "undefined" element
276         if (sl.isEmpty()) {
277             CBS cbsm = new CBS();
278             cbsm.type = Constants.NO_ENTRIES;
279             sl.add(cbsm);
280         }
281
282         // add all elements to options
283         serviceList = sl;
284         List<CommandOption> serviceNameOptions = new ArrayList<>();
285         boolean isSelectedElementIn = false;
286         int index = 0;
287         for (CBS serviceEntry : serviceList) {
288             // create StateOption with "value = list index" and "label = human readable string"
289             serviceNameOptions.add(new CommandOption(Integer.toString(index), serviceEntry.type));
290             if (selectedService.equals(serviceEntry.type)) {
291                 isSelectedElementIn = true;
292             }
293             index++;
294         }
295         setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
296
297         // if current selected item isn't anymore in the list select first entry
298         if (!isSelectedElementIn) {
299             selectService(0);
300         }
301     }
302
303     protected void selectService(int index) {
304         if (index >= 0 && index < serviceList.size()) {
305             CBS serviceEntry = serviceList.get(index);
306             selectedService = serviceEntry.type;
307             updateChannel(CHANNEL_GROUP_SERVICE, NAME, StringType.valueOf(Converter.toTitleCase(serviceEntry.type)));
308             if (serviceEntry.dateTime != null) {
309                 updateChannel(CHANNEL_GROUP_SERVICE, DATE,
310                         DateTimeType.valueOf(Converter.zonedToLocalDateTime(serviceEntry.dateTime)));
311             } else {
312                 updateChannel(CHANNEL_GROUP_SERVICE, DATE, UnDefType.UNDEF);
313             }
314             if (serviceEntry.distance != null) {
315                 if (Constants.KILOMETERS_JSON.equals(serviceEntry.distance.units)) {
316                     updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
317                             QuantityType.valueOf(serviceEntry.distance.value, Constants.KILOMETRE_UNIT));
318                 } else {
319                     updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
320                             QuantityType.valueOf(serviceEntry.distance.value, ImperialUnits.MILE));
321                 }
322             } else {
323                 updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
324                         QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT));
325             }
326         }
327     }
328
329     protected void updateSessions(List<ChargeSession> sl) {
330         // if list is empty add "undefined" element
331         if (sl.isEmpty()) {
332             ChargeSession cs = new ChargeSession();
333             cs.title = Constants.NO_ENTRIES;
334             sl.add(cs);
335         }
336
337         // add all elements to options
338         sessionList = sl;
339         List<CommandOption> sessionNameOptions = new ArrayList<>();
340         boolean isSelectedElementIn = false;
341         int index = 0;
342         for (ChargeSession session : sessionList) {
343             // create StateOption with "value = list index" and "label = human readable string"
344             sessionNameOptions.add(new CommandOption(Integer.toString(index), session.title));
345             if (selectedService.equals(session.title)) {
346                 isSelectedElementIn = true;
347             }
348             index++;
349         }
350         setOptions(CHANNEL_GROUP_CHARGE_SESSION, TITLE, sessionNameOptions);
351
352         // if current selected item isn't anymore in the list select first entry
353         if (!isSelectedElementIn) {
354             selectSession(0);
355         }
356     }
357
358     protected void selectSession(int index) {
359         if (index >= 0 && index < sessionList.size()) {
360             ChargeSession sessionEntry = sessionList.get(index);
361             selectedService = sessionEntry.title;
362             updateChannel(CHANNEL_GROUP_CHARGE_SESSION, TITLE, StringType.valueOf(sessionEntry.title));
363             updateChannel(CHANNEL_GROUP_CHARGE_SESSION, SUBTITLE, StringType.valueOf(sessionEntry.subtitle));
364             if (sessionEntry.energyCharged != null) {
365                 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(sessionEntry.energyCharged));
366             } else {
367                 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(Constants.UNDEF));
368             }
369             if (sessionEntry.issues != null) {
370                 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(sessionEntry.issues));
371             } else {
372                 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(Constants.HYPHEN));
373             }
374             updateChannel(CHANNEL_GROUP_CHARGE_SESSION, STATUS, StringType.valueOf(sessionEntry.sessionStatus));
375         }
376     }
377
378     protected void updateChargeProfile(ChargeProfile cp) {
379         ChargeProfileWrapper cpw = new ChargeProfileWrapper(cp);
380
381         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_PREFERENCE, StringType.valueOf(cpw.getPreference()));
382         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_MODE, StringType.valueOf(cpw.getMode()));
383         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CONTROL, StringType.valueOf(cpw.getControlType()));
384         ChargingSettings cs = cpw.getChargeSettings();
385         if (cs != null) {
386             updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_TARGET,
387                     DecimalType.valueOf(Integer.toString(cs.targetSoc)));
388             updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_LIMIT,
389                     OnOffType.from(cs.isAcCurrentLimitActive));
390         }
391         final Boolean climate = cpw.isEnabled(ProfileKey.CLIMATE);
392         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, CHARGE_PROFILE_CLIMATE,
393                 climate == null ? UnDefType.UNDEF : OnOffType.from(climate));
394         updateTimedState(cpw, ProfileKey.WINDOWSTART);
395         updateTimedState(cpw, ProfileKey.WINDOWEND);
396         updateTimedState(cpw, ProfileKey.TIMER1);
397         updateTimedState(cpw, ProfileKey.TIMER2);
398         updateTimedState(cpw, ProfileKey.TIMER3);
399         updateTimedState(cpw, ProfileKey.TIMER4);
400     }
401
402     protected void updateTimedState(ChargeProfileWrapper profile, ProfileKey key) {
403         final TimedChannel timed = ChargeProfileUtils.getTimedChannel(key);
404         if (timed != null) {
405             final LocalTime time = profile.getTime(key);
406             updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.time,
407                     time.equals(Constants.NULL_LOCAL_TIME) ? UnDefType.UNDEF
408                             : new DateTimeType(ZonedDateTime.of(Constants.EPOCH_DAY, time, ZoneId.systemDefault())));
409             if (timed.timer != null) {
410                 final Boolean enabled = profile.isEnabled(key);
411                 updateChannel(CHANNEL_GROUP_CHARGE_PROFILE, timed.timer + CHARGE_ENABLED,
412                         enabled == null ? UnDefType.UNDEF : OnOffType.from(enabled));
413                 if (timed.hasDays) {
414                     final Set<DayOfWeek> days = profile.getDays(key);
415                     EnumSet.allOf(DayOfWeek.class).forEach(day -> {
416                         updateChannel(CHANNEL_GROUP_CHARGE_PROFILE,
417                                 timed.timer + ChargeProfileUtils.getDaysChannel(day),
418                                 days == null ? UnDefType.UNDEF : OnOffType.from(days.contains(day)));
419                     });
420                 }
421             }
422         }
423     }
424
425     protected void updateDoors(DoorsWindows dw) {
426         updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_FRONT,
427                 StringType.valueOf(Converter.toTitleCase(dw.doors.driverFront)));
428         updateChannel(CHANNEL_GROUP_DOORS, DOOR_DRIVER_REAR,
429                 StringType.valueOf(Converter.toTitleCase(dw.doors.driverRear)));
430         updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_FRONT,
431                 StringType.valueOf(Converter.toTitleCase(dw.doors.passengerFront)));
432         updateChannel(CHANNEL_GROUP_DOORS, DOOR_PASSENGER_REAR,
433                 StringType.valueOf(Converter.toTitleCase(dw.doors.passengerRear)));
434         updateChannel(CHANNEL_GROUP_DOORS, TRUNK, StringType.valueOf(Converter.toTitleCase(dw.trunk)));
435         updateChannel(CHANNEL_GROUP_DOORS, HOOD, StringType.valueOf(Converter.toTitleCase(dw.hood)));
436     }
437
438     protected void updateWindows(DoorsWindows dw) {
439         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_FRONT,
440                 StringType.valueOf(Converter.toTitleCase(dw.windows.driverFront)));
441         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_DRIVER_REAR,
442                 StringType.valueOf(Converter.toTitleCase(dw.windows.driverRear)));
443         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_FRONT,
444                 StringType.valueOf(Converter.toTitleCase(dw.windows.passengerFront)));
445         updateChannel(CHANNEL_GROUP_DOORS, WINDOW_DOOR_PASSENGER_REAR,
446                 StringType.valueOf(Converter.toTitleCase(dw.windows.passengerRear)));
447         updateChannel(CHANNEL_GROUP_DOORS, SUNROOF, StringType.valueOf(Converter.toTitleCase(dw.moonroof)));
448     }
449
450     protected void updatePosition(Location pos) {
451         updateChannel(CHANNEL_GROUP_LOCATION, GPS, PointType
452                 .valueOf(Double.toString(pos.coordinates.latitude) + "," + Double.toString(pos.coordinates.longitude)));
453         updateChannel(CHANNEL_GROUP_LOCATION, HEADING, QuantityType.valueOf(pos.heading, Units.DEGREE_ANGLE));
454         updateChannel(CHANNEL_GROUP_LOCATION, ADDRESS, StringType.valueOf(pos.address.formatted));
455     }
456 }