2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mybmw.internal.handler;
15 import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
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;
27 import javax.measure.Unit;
28 import javax.measure.quantity.Length;
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;
69 * The {@link VehicleChannelHandler} handles Channel updates
71 * @author Bernd Weymann - Initial contribution
72 * @author Norbert Truchsess - edit & send of charge profile
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;
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;
89 protected MyBMWCommandOptionProvider commandOptionProvider;
92 protected Optional<String> vehicleStatusCache = Optional.empty();
93 protected Optional<byte[]> imageCache = Optional.empty();
95 public VehicleChannelHandler(Thing thing, MyBMWCommandOptionProvider cop, String type) {
97 commandOptionProvider = cop;
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;
105 setOptions(CHANNEL_GROUP_REMOTE, REMOTE_SERVICE_COMMAND, RemoteServiceUtils.getOptions(isElectric));
108 private void setOptions(final String group, final String id, List<CommandOption> options) {
109 commandOptionProvider.setCommandOptions(new ChannelUID(thing.getUID(), group, id), options);
112 protected void updateChannel(final String group, final String id, final State state) {
113 updateState(new ChannelUID(thing.getUID(), group, id), state);
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)));
124 protected void updateVehicle(Vehicle v) {
125 updateVehicleStatus(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);
135 private void updateTires(@Nullable Tires tires) {
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);
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));
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));
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))));
189 protected void updateRange(Vehicle v) {
190 // get the right unit
191 Unit<Length> lengthUnit = VehicleStatusUtils.getLengthUnit(v.status.fuelIndicators);
192 if (lengthUnit == null) {
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),
200 updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC, qtElectricRange);
201 updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC, qtElectricRadius);
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);
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),
215 updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID, qtHybridRange);
216 updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID, qtHybridRadius);
218 if (v.status.currentMileage.mileage == Constants.INT_UNDEF) {
219 updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, UnDefType.UNDEF);
221 updateChannel(CHANNEL_GROUP_RANGE, MILEAGE,
222 QuantityType.valueOf(v.status.currentMileage.mileage, lengthUnit));
225 updateChannel(CHANNEL_GROUP_RANGE, SOC,
226 QuantityType.valueOf(v.properties.chargingState.chargePercentage, Units.PERCENT));
229 updateChannel(CHANNEL_GROUP_RANGE, REMAINING_FUEL,
230 QuantityType.valueOf(v.properties.fuelLevel.value, Units.LITRE));
234 protected void updateCheckControls(List<CCMMessage> ccl) {
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;
244 // add all elements to options
245 checkControlList = ccl;
246 List<CommandOption> ccmDescriptionOptions = new ArrayList<>();
247 boolean isSelectedElementIn = false;
249 for (CCMMessage ccEntry : checkControlList) {
250 ccmDescriptionOptions.add(new CommandOption(Integer.toString(index), ccEntry.title));
251 if (selectedCC.equals(ccEntry.title)) {
252 isSelectedElementIn = true;
256 setOptions(CHANNEL_GROUP_CHECK_CONTROL, NAME, ccmDescriptionOptions);
258 // if current selected item isn't anymore in the list select first entry
259 if (!isSelectedElementIn) {
260 selectCheckControl(0);
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));
274 protected void updateServices(List<CBS> sl) {
275 // if list is empty add "undefined" element
277 CBS cbsm = new CBS();
278 cbsm.type = Constants.NO_ENTRIES;
282 // add all elements to options
284 List<CommandOption> serviceNameOptions = new ArrayList<>();
285 boolean isSelectedElementIn = false;
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;
295 setOptions(CHANNEL_GROUP_SERVICE, NAME, serviceNameOptions);
297 // if current selected item isn't anymore in the list select first entry
298 if (!isSelectedElementIn) {
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)));
312 updateChannel(CHANNEL_GROUP_SERVICE, DATE, UnDefType.UNDEF);
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));
319 updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
320 QuantityType.valueOf(serviceEntry.distance.value, ImperialUnits.MILE));
323 updateChannel(CHANNEL_GROUP_SERVICE, MILEAGE,
324 QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT));
329 protected void updateSessions(List<ChargeSession> sl) {
330 // if list is empty add "undefined" element
332 ChargeSession cs = new ChargeSession();
333 cs.title = Constants.NO_ENTRIES;
337 // add all elements to options
339 List<CommandOption> sessionNameOptions = new ArrayList<>();
340 boolean isSelectedElementIn = false;
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;
350 setOptions(CHANNEL_GROUP_CHARGE_SESSION, TITLE, sessionNameOptions);
352 // if current selected item isn't anymore in the list select first entry
353 if (!isSelectedElementIn) {
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));
367 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ENERGY, StringType.valueOf(Constants.UNDEF));
369 if (sessionEntry.issues != null) {
370 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(sessionEntry.issues));
372 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, ISSUE, StringType.valueOf(Constants.HYPHEN));
374 updateChannel(CHANNEL_GROUP_CHARGE_SESSION, STATUS, StringType.valueOf(sessionEntry.sessionStatus));
378 protected void updateChargeProfile(ChargeProfile cp) {
379 ChargeProfileWrapper cpw = new ChargeProfileWrapper(cp);
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();
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));
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);
402 protected void updateTimedState(ChargeProfileWrapper profile, ProfileKey key) {
403 final TimedChannel timed = ChargeProfileUtils.getTimedChannel(key);
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));
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)));
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)));
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)));
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));