]> git.basschouten.com Git - openhab-addons.git/blob
fa515b95a46d883072565318691893d397d69385
[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.mercedesme.internal.handler;
14
15 import static org.openhab.binding.mercedesme.internal.Constants.*;
16
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Locale;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.UUID;
27
28 import javax.measure.Unit;
29 import javax.measure.quantity.Length;
30 import javax.measure.quantity.Temperature;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.json.JSONObject;
35 import org.openhab.binding.mercedesme.internal.Constants;
36 import org.openhab.binding.mercedesme.internal.MercedesMeCommandOptionProvider;
37 import org.openhab.binding.mercedesme.internal.MercedesMeStateOptionProvider;
38 import org.openhab.binding.mercedesme.internal.actions.VehicleActions;
39 import org.openhab.binding.mercedesme.internal.config.VehicleConfiguration;
40 import org.openhab.binding.mercedesme.internal.utils.ChannelStateMap;
41 import org.openhab.binding.mercedesme.internal.utils.Mapper;
42 import org.openhab.binding.mercedesme.internal.utils.UOMObserver;
43 import org.openhab.binding.mercedesme.internal.utils.Utils;
44 import org.openhab.core.i18n.LocationProvider;
45 import org.openhab.core.library.types.DateTimeType;
46 import org.openhab.core.library.types.DecimalType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.PointType;
49 import org.openhab.core.library.types.QuantityType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.library.unit.ImperialUnits;
52 import org.openhab.core.library.unit.SIUnits;
53 import org.openhab.core.library.unit.Units;
54 import org.openhab.core.thing.Bridge;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.binding.BaseThingHandler;
60 import org.openhab.core.thing.binding.BridgeHandler;
61 import org.openhab.core.thing.binding.ThingHandlerService;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.CommandOption;
64 import org.openhab.core.types.RefreshType;
65 import org.openhab.core.types.State;
66 import org.openhab.core.types.StateOption;
67 import org.openhab.core.types.UnDefType;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
70
71 import com.daimler.mbcarkit.proto.Acp.ACP.CommandType;
72 import com.daimler.mbcarkit.proto.Acp.VehicleAPI.CommandState;
73 import com.daimler.mbcarkit.proto.Client.ClientMessage;
74 import com.daimler.mbcarkit.proto.VehicleCommands.AuxheatStart;
75 import com.daimler.mbcarkit.proto.VehicleCommands.AuxheatStop;
76 import com.daimler.mbcarkit.proto.VehicleCommands.ChargeProgramConfigure;
77 import com.daimler.mbcarkit.proto.VehicleCommands.CommandRequest;
78 import com.daimler.mbcarkit.proto.VehicleCommands.DoorsLock;
79 import com.daimler.mbcarkit.proto.VehicleCommands.DoorsUnlock;
80 import com.daimler.mbcarkit.proto.VehicleCommands.EngineStart;
81 import com.daimler.mbcarkit.proto.VehicleCommands.EngineStop;
82 import com.daimler.mbcarkit.proto.VehicleCommands.SigPosStart;
83 import com.daimler.mbcarkit.proto.VehicleCommands.SigPosStart.HornType;
84 import com.daimler.mbcarkit.proto.VehicleCommands.SigPosStart.LightType;
85 import com.daimler.mbcarkit.proto.VehicleCommands.SigPosStart.SigposType;
86 import com.daimler.mbcarkit.proto.VehicleCommands.SunroofClose;
87 import com.daimler.mbcarkit.proto.VehicleCommands.SunroofLift;
88 import com.daimler.mbcarkit.proto.VehicleCommands.SunroofOpen;
89 import com.daimler.mbcarkit.proto.VehicleCommands.TemperatureConfigure;
90 import com.daimler.mbcarkit.proto.VehicleCommands.TemperatureConfigure.TemperaturePoint;
91 import com.daimler.mbcarkit.proto.VehicleCommands.WindowsClose;
92 import com.daimler.mbcarkit.proto.VehicleCommands.WindowsOpen;
93 import com.daimler.mbcarkit.proto.VehicleCommands.WindowsVentilate;
94 import com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningConfigureSeats;
95 import com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningStart;
96 import com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningStop;
97 import com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningType;
98 import com.daimler.mbcarkit.proto.VehicleEvents;
99 import com.daimler.mbcarkit.proto.VehicleEvents.ChargeProgramParameters;
100 import com.daimler.mbcarkit.proto.VehicleEvents.ChargeProgramsValue;
101 import com.daimler.mbcarkit.proto.VehicleEvents.TemperaturePointsValue;
102 import com.daimler.mbcarkit.proto.VehicleEvents.VEPUpdate;
103 import com.daimler.mbcarkit.proto.VehicleEvents.VehicleAttributeStatus;
104 import com.daimler.mbcarkit.proto.Vehicleapi.AppTwinCommandStatus;
105 import com.daimler.mbcarkit.proto.Vehicleapi.AppTwinCommandStatusUpdatesByPID;
106 import com.google.protobuf.BoolValue;
107 import com.google.protobuf.Int32Value;
108
109 /**
110  * {@link VehicleHandler} transform data into state updates and handling of vehicle commands
111  *
112  * @author Bernd Weymann - Initial contribution
113  */
114 @NonNullByDefault
115 public class VehicleHandler extends BaseThingHandler {
116     private static final List<String> HVAC_SEAT_LIST = Arrays
117             .asList(new String[] { GROUP_HVAC + "#" + OH_CHANNEL_FRONT_LEFT, GROUP_HVAC + "#" + OH_CHANNEL_FRONT_RIGHT,
118                     GROUP_HVAC + "#" + OH_CHANNEL_REAR_LEFT, GROUP_HVAC + "#" + OH_CHANNEL_REAR_RIGHT });
119
120     private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
121     private final LocationProvider locationProvider;
122     private final MercedesMeCommandOptionProvider mmcop;
123     private final MercedesMeStateOptionProvider mmsop;
124
125     private Map<String, UOMObserver> unitStorage = new HashMap<>();
126     private int ignitionState = -1;
127     private boolean chargingState = false;
128     private int selectedChargeProgram = -1;
129     private int activeTemperaturePoint = -1;
130     private Map<Integer, QuantityType<Temperature>> temperaturePointsStorage = new HashMap<>();
131     private JSONObject chargeGroupValueStorage = new JSONObject();
132     private Map<String, State> hvacGroupValueStorage = new HashMap<>();
133     private String vehicleType = NOT_SET;
134
135     Map<String, ChannelStateMap> eventStorage = new HashMap<>();
136     Optional<AccountHandler> accountHandler = Optional.empty();
137     Optional<VehicleConfiguration> config = Optional.empty();
138
139     public VehicleHandler(Thing thing, LocationProvider lp, MercedesMeCommandOptionProvider cop,
140             MercedesMeStateOptionProvider sop) {
141         super(thing);
142         vehicleType = thing.getThingTypeUID().getId();
143         locationProvider = lp;
144         mmcop = cop;
145         mmsop = sop;
146     }
147
148     @Override
149     public void initialize() {
150         config = Optional.of(getConfigAs(VehicleConfiguration.class));
151         Bridge bridge = getBridge();
152         if (bridge != null) {
153             updateStatus(ThingStatus.UNKNOWN);
154             BridgeHandler handler = bridge.getHandler();
155             if (handler != null) {
156                 setCommandStateOptions();
157                 accountHandler = Optional.of((AccountHandler) handler);
158                 accountHandler.get().registerVin(config.get().vin, this);
159             } else {
160                 throw new IllegalStateException("BridgeHandler is null");
161             }
162         } else {
163             String textKey = Constants.STATUS_TEXT_PREFIX + "vehicle" + Constants.STATUS_BRIDGE_MISSING;
164             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, textKey);
165         }
166     }
167
168     @Override
169     public void dispose() {
170         accountHandler.get().unregisterVin(config.get().vin);
171         super.dispose();
172     }
173
174     @Override
175     public void handleCommand(ChannelUID channelUID, Command command) {
176         /**
177          * Commands shall be not that frequent so trace level for identifying problems should be feasible
178          */
179         logger.trace("Received command {} {} for {}", command.getClass(), command, channelUID);
180         if (command instanceof RefreshType) {
181             if (MB_KEY_FEATURE_CAPABILITIES.equals(channelUID.getIdWithoutGroup())
182                     || MB_KEY_COMMAND_CAPABILITIES.equals(channelUID.getIdWithoutGroup())) {
183                 accountHandler.ifPresent(ah -> {
184                     ah.getVehicleCapabilities(config.get().vin);
185                 });
186             } else {
187                 // deliver from event storage
188                 ChannelStateMap csm = eventStorage.get(channelUID.getId());
189                 if (csm != null) {
190                     updateChannel(csm);
191                 }
192             }
193             // ensure unit update
194             unitStorage.remove(channelUID.getIdWithoutGroup());
195         } else if (Constants.GROUP_VEHICLE.equals(channelUID.getGroupId())) {
196             /**
197              * Commands for Vehicle
198              */
199             if (OH_CHANNEL_IGNITION.equals(channelUID.getIdWithoutGroup())) {
200                 String supported = thing.getProperties().get(MB_KEY_COMMAND_ENGINE_START);
201                 if (Boolean.FALSE.toString().equals(supported)) {
202                     logger.trace("Engine Start/Stop not supported");
203                 } else {
204                     int commandValue = ((DecimalType) command).intValue();
205                     if (commandValue == 4) {
206                         String pin = accountHandler.get().config.get().pin;
207                         if (Constants.NOT_SET.equals(pin)) {
208                             logger.trace("Security PIN missing in Account bridge");
209                         } else {
210                             EngineStart eStart = EngineStart.newBuilder().setPin(pin).build();
211                             CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
212                                     .setRequestId(UUID.randomUUID().toString()).setEngineStart(eStart).build();
213                             ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
214                             accountHandler.get().sendCommand(cm);
215                         }
216                     } else if (commandValue == 0) {
217                         EngineStop eStop = EngineStop.newBuilder().build();
218                         CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
219                                 .setRequestId(UUID.randomUUID().toString()).setEngineStop(eStop).build();
220                         ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
221                         accountHandler.get().sendCommand(cm);
222                     }
223                 }
224             } else if (OH_CHANNEL_WINDOWS.equals(channelUID.getIdWithoutGroup())) {
225                 String supported = thing.getProperties().get(MB_KEY_COMMAND_WINDOWS_OPEN);
226                 String pin = accountHandler.get().config.get().pin;
227                 if (Boolean.FALSE.toString().equals(supported)) {
228                     logger.trace("Windows control not supported");
229                 } else {
230                     CommandRequest cr;
231                     ClientMessage cm;
232                     switch (((DecimalType) command).intValue()) {
233                         case 0:
234                             if (Constants.NOT_SET.equals(pin)) {
235                                 logger.trace("Security PIN missing in Account bridge");
236                             } else {
237                                 WindowsVentilate wv = WindowsVentilate.newBuilder().setPin(pin).build();
238                                 cr = CommandRequest.newBuilder().setVin(config.get().vin)
239                                         .setRequestId(UUID.randomUUID().toString()).setWindowsVentilate(wv).build();
240                                 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
241                                 accountHandler.get().sendCommand(cm);
242                             }
243                             break;
244                         case 1:
245                             WindowsClose wc = WindowsClose.newBuilder().build();
246                             cr = CommandRequest.newBuilder().setVin(config.get().vin)
247                                     .setRequestId(UUID.randomUUID().toString()).setWindowsClose(wc).build();
248                             cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
249                             accountHandler.get().sendCommand(cm);
250                             break;
251                         case 2:
252                             if (Constants.NOT_SET.equals(pin)) {
253                                 logger.trace("Security PIN missing in Account bridge");
254                             } else {
255                                 WindowsOpen wo = WindowsOpen.newBuilder().setPin(pin).build();
256                                 cr = CommandRequest.newBuilder().setVin(config.get().vin)
257                                         .setRequestId(UUID.randomUUID().toString()).setWindowsOpen(wo).build();
258                                 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
259                                 accountHandler.get().sendCommand(cm);
260                             }
261                             break;
262                         default:
263                             logger.trace("No Windows movement known for {}", command);
264                             break;
265                     }
266                 }
267             } else if (OH_CHANNEL_LOCK.equals(channelUID.getIdWithoutGroup())) {
268                 String pin = accountHandler.get().config.get().pin;
269                 String supported = thing.getProperties().get(MB_KEY_COMMAND_DOORS_LOCK);
270                 if (Boolean.FALSE.toString().equals(supported)) {
271                     logger.trace("Door Lock not supported");
272                 } else {
273                     switch (((DecimalType) command).intValue()) {
274                         case 0:
275                             DoorsLock dl = DoorsLock.newBuilder().build();
276                             CommandRequest lockCr = CommandRequest.newBuilder().setVin(config.get().vin)
277                                     .setRequestId(UUID.randomUUID().toString()).setDoorsLock(dl).build();
278                             ClientMessage lockCm = ClientMessage.newBuilder().setCommandRequest(lockCr).build();
279                             accountHandler.get().sendCommand(lockCm);
280                             break;
281                         case 1:
282                             if (Constants.NOT_SET.equals(pin)) {
283                                 logger.trace("Security PIN missing in Account bridge");
284                             } else {
285                                 DoorsUnlock du = DoorsUnlock.newBuilder().setPin(pin).build();
286                                 CommandRequest unlockCr = CommandRequest.newBuilder().setVin(config.get().vin)
287                                         .setRequestId(UUID.randomUUID().toString()).setDoorsUnlock(du).build();
288                                 ClientMessage unlockCm = ClientMessage.newBuilder().setCommandRequest(unlockCr).build();
289                                 accountHandler.get().sendCommand(unlockCm);
290                             }
291                             break;
292                         default:
293                             logger.trace("No lock command mapped to {}", command);
294                             break;
295                     }
296                 }
297             }
298         } else if (Constants.GROUP_HVAC.equals(channelUID.getGroupId())) {
299             /**
300              * Commands for HVAC
301              */
302             if (OH_CHANNEL_TEMPERATURE.equals(channelUID.getIdWithoutGroup())) {
303                 String supported = thing.getProperties().get(MB_KEY_COMMAND_ZEV_PRECONDITION_CONFIGURE);
304                 if (Boolean.FALSE.toString().equals(supported)) {
305                     logger.trace("Air Conditioning Temperature Setting not supported");
306                 } else {
307                     QuantityType<?> targetTemp = (QuantityType<?>) command;
308                     QuantityType<?> targetTempCelsius = targetTemp.toInvertibleUnit(SIUnits.CELSIUS);
309                     if (targetTempCelsius != null) {
310                         TemperatureConfigure tc = TemperatureConfigure.newBuilder()
311                                 .addTemperaturePoints(TemperaturePoint.newBuilder().setZoneValue(activeTemperaturePoint)
312                                         .setTemperatureInCelsius(targetTempCelsius.intValue()).build())
313                                 .build();
314                         CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
315                                 .setRequestId(UUID.randomUUID().toString()).setTemperatureConfigure(tc).build();
316                         ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
317                         accountHandler.get().sendCommand(cm);
318                     }
319                 }
320             } else if (OH_CHANNEL_ACTIVE.equals(channelUID.getIdWithoutGroup())) {
321                 String supported = thing.getProperties().get(MB_KEY_COMMAND_ZEV_PRECONDITIONING_START);
322                 if (Boolean.FALSE.toString().equals(supported)) {
323                     logger.trace("Air Conditioning not supported");
324                 } else {
325                     if (OnOffType.ON.equals(command)) {
326                         ZEVPreconditioningStart precondStart = ZEVPreconditioningStart.newBuilder()
327                                 .setType(ZEVPreconditioningType.NOW).build();
328                         CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
329                                 .setRequestId(UUID.randomUUID().toString()).setZevPreconditioningStart(precondStart)
330                                 .build();
331                         ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
332                         accountHandler.get().sendCommand(cm);
333                     } else {
334                         ZEVPreconditioningStop precondStop = ZEVPreconditioningStop.newBuilder()
335                                 .setType(ZEVPreconditioningType.NOW).build();
336                         CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
337                                 .setRequestId(UUID.randomUUID().toString()).setZevPreconditioningStop(precondStop)
338                                 .build();
339                         ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
340                         accountHandler.get().sendCommand(cm);
341                     }
342                 }
343             } else if (OH_CHANNEL_FRONT_LEFT.equals(channelUID.getIdWithoutGroup())) {
344                 configureSeats(channelUID, (State) command);
345             } else if (OH_CHANNEL_FRONT_RIGHT.equals(channelUID.getIdWithoutGroup())) {
346                 configureSeats(channelUID, (State) command);
347             } else if (OH_CHANNEL_REAR_LEFT.equals(channelUID.getIdWithoutGroup())) {
348                 configureSeats(channelUID, (State) command);
349             } else if (OH_CHANNEL_REAR_RIGHT.equals(channelUID.getIdWithoutGroup())) {
350                 configureSeats(channelUID, (State) command);
351             } else if (OH_CHANNEL_AUX_HEAT.equals(channelUID.getIdWithoutGroup())) {
352                 String supported = thing.getProperties().get(MB_KEY_FEATURE_AUX_HEAT);
353                 if (Boolean.FALSE.toString().equals(supported)) {
354                     logger.trace("Auxiliary Heating not supported");
355                 } else {
356                     if (OnOffType.ON.equals(command)) {
357                         AuxheatStart auxHeatStart = AuxheatStart.newBuilder().build();
358                         CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
359                                 .setRequestId(UUID.randomUUID().toString()).setAuxheatStart(auxHeatStart).build();
360                         ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
361                         accountHandler.get().sendCommand(cm);
362                     } else {
363                         AuxheatStop auxHeatStop = AuxheatStop.newBuilder().build();
364                         CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
365                                 .setRequestId(UUID.randomUUID().toString()).setAuxheatStop(auxHeatStop).build();
366                         ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
367                         accountHandler.get().sendCommand(cm);
368                     }
369                 }
370             } else if (OH_CHANNEL_ZONE.equals(channelUID.getIdWithoutGroup())) {
371                 int zone = ((DecimalType) command).intValue();
372                 if (temperaturePointsStorage.containsKey(zone)) {
373                     ChannelStateMap zoneMap = new ChannelStateMap(OH_CHANNEL_ZONE, GROUP_HVAC, (DecimalType) command);
374                     updateChannel(zoneMap);
375                     QuantityType<Temperature> selectedTemp = temperaturePointsStorage.get(zone);
376                     if (selectedTemp != null) {
377                         ChannelStateMap tempCSM = new ChannelStateMap(OH_CHANNEL_TEMPERATURE, GROUP_HVAC, selectedTemp);
378                         updateChannel(tempCSM);
379                     }
380                 } else {
381                     logger.trace("No Temperature Zone found for {}", command);
382                 }
383             }
384         } else if (Constants.GROUP_POSITION.equals(channelUID.getGroupId())) {
385             /**
386              * Commands for Positioning
387              */
388             if (OH_CHANNEL_SIGNAL.equals(channelUID.getIdWithoutGroup())) {
389                 String supported = thing.getProperties().get(MB_KEY_COMMAND_SIGPOS_START);
390                 if (Boolean.FALSE.toString().equals(supported)) {
391                     logger.trace("Signal Position not supported");
392                 } else {
393                     SigPosStart sps;
394                     CommandRequest cr;
395                     ClientMessage cm;
396                     switch (((DecimalType) command).intValue()) {
397                         case 0: // light
398                             sps = SigPosStart.newBuilder().setSigposType(SigposType.LIGHT_ONLY)
399                                     .setLightType(LightType.DIPPED_HEAD_LIGHT).setSigposDuration(10).build();
400                             cr = CommandRequest.newBuilder().setVin(config.get().vin)
401                                     .setRequestId(UUID.randomUUID().toString()).setSigposStart(sps).build();
402                             cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
403                             accountHandler.get().sendCommand(cm);
404                             break;
405                         case 1: // horn
406                             sps = SigPosStart.newBuilder().setSigposType(SigposType.HORN_ONLY).setHornRepeat(3)
407                                     .setHornType(HornType.HORN_LOW_VOLUME).build();
408                             cr = CommandRequest.newBuilder().setVin(config.get().vin)
409                                     .setRequestId(UUID.randomUUID().toString()).setSigposStart(sps).build();
410                             cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
411                             accountHandler.get().sendCommand(cm);
412                             break;
413                         default:
414                             logger.trace("No Positioning known for {}", command);
415                     }
416                 }
417             }
418         } else if (Constants.GROUP_CHARGE.equals(channelUID.getGroupId())) {
419             /**
420              * Commands for Charging
421              */
422             synchronized (chargeGroupValueStorage) {
423                 int maxSocToSelect = 80;
424                 boolean autoUnlockToSelect = false;
425                 String supported = thing.getProperties().get(MB_KEY_COMMAND_CHARGE_PROGRAM_CONFIGURE);
426                 if (Boolean.FALSE.toString().equals(supported)) {
427                     logger.trace("Charge Program Configure not supported");
428                 } else {
429                     boolean sendCommand = false;
430                     if (OH_CHANNEL_PROGRAM.equals(channelUID.getIdWithoutGroup())) {
431                         selectedChargeProgram = ((DecimalType) command).intValue();
432                         if (chargeGroupValueStorage.has(Integer.toString(selectedChargeProgram))) {
433                             maxSocToSelect = chargeGroupValueStorage
434                                     .getJSONObject(Integer.toString(selectedChargeProgram))
435                                     .getInt(Constants.MAX_SOC_KEY);
436                             autoUnlockToSelect = chargeGroupValueStorage
437                                     .getJSONObject(Integer.toString(selectedChargeProgram))
438                                     .getBoolean(Constants.AUTO_UNLOCK_KEY);
439                             updateChannel(new ChannelStateMap(OH_CHANNEL_MAX_SOC, GROUP_CHARGE,
440                                     QuantityType.valueOf(maxSocToSelect, Units.PERCENT)));
441                             updateChannel(new ChannelStateMap(OH_CHANNEL_AUTO_UNLOCK, GROUP_CHARGE,
442                                     OnOffType.from(autoUnlockToSelect)));
443                             sendCommand = true;
444                         } else {
445                             logger.trace("No charge program found for {}", selectedChargeProgram);
446                         }
447                     }
448                     if (OH_CHANNEL_AUTO_UNLOCK.equals(channelUID.getIdWithoutGroup())) {
449                         autoUnlockToSelect = ((OnOffType) command).equals(OnOffType.ON);
450                         sendCommand = true;
451                     } else if (OH_CHANNEL_MAX_SOC.equals(channelUID.getIdWithoutGroup())) {
452                         maxSocToSelect = ((QuantityType<?>) command).intValue();
453                         sendCommand = true;
454                     } // else - nothing to be sent
455                     if (sendCommand) {
456                         Int32Value maxSocValue = Int32Value.newBuilder().setValue(maxSocToSelect).build();
457                         BoolValue autoUnlockValue = BoolValue.newBuilder().setValue(autoUnlockToSelect).build();
458                         ChargeProgramConfigure cpc = ChargeProgramConfigure.newBuilder()
459                                 .setChargeProgramValue(selectedChargeProgram).setMaxSoc(maxSocValue)
460                                 .setAutoUnlock(autoUnlockValue).build();
461                         CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
462                                 .setRequestId(UUID.randomUUID().toString()).setChargeProgramConfigure(cpc).build();
463                         ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
464                         accountHandler.get().sendCommand(cm);
465                     }
466                 }
467             }
468         } else if (Constants.GROUP_DOORS.equals(channelUID.getGroupId())) {
469             /**
470              * Commands for Doors
471              */
472             if (OH_CHANNEL_SUNROOF.equals(channelUID.getIdWithoutGroup())) {
473                 String supported = thing.getProperties().get(MB_KEY_COMMAND_SUNROOF_OPEN);
474                 String pin = accountHandler.get().config.get().pin;
475                 if (Boolean.FALSE.toString().equals(supported)) {
476                     logger.trace("Sunroof control not supported");
477                 } else {
478                     CommandRequest cr;
479                     ClientMessage cm;
480                     switch (((DecimalType) command).intValue()) {
481                         case 0:
482                             SunroofClose sc = SunroofClose.newBuilder().build();
483                             cr = CommandRequest.newBuilder().setVin(config.get().vin)
484                                     .setRequestId(UUID.randomUUID().toString()).setSunroofClose(sc).build();
485                             cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
486                             accountHandler.get().sendCommand(cm);
487                             break;
488                         case 1:
489                             if (Constants.NOT_SET.equals(pin)) {
490                                 logger.trace("Security PIN missing in Account bridge");
491                             } else {
492                                 SunroofOpen so = SunroofOpen.newBuilder().setPin(pin).build();
493                                 cr = CommandRequest.newBuilder().setVin(config.get().vin)
494                                         .setRequestId(UUID.randomUUID().toString()).setSunroofOpen(so).build();
495                                 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
496                                 accountHandler.get().sendCommand(cm);
497                             }
498                             break;
499                         case 2:
500                             if (Constants.NOT_SET.equals(pin)) {
501                                 logger.trace("Security PIN missing in Account bridge");
502                             } else {
503                                 SunroofLift sl = SunroofLift.newBuilder().setPin(pin).build();
504                                 cr = CommandRequest.newBuilder().setVin(config.get().vin)
505                                         .setRequestId(UUID.randomUUID().toString()).setSunroofLift(sl).build();
506                                 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
507                                 accountHandler.get().sendCommand(cm);
508                             }
509                             break;
510                         default:
511                             logger.trace("No Sunroof movement known for {}", command);
512                     }
513                 }
514             }
515         } else {
516             logger.trace("No command {} found for {}", command, channelUID.getAsString());
517         }
518     }
519
520     private void configureSeats(ChannelUID channelUID, State command) {
521         String supported = thing.getProperties().get(MB_KEY_COMMAND_ZEV_PRECONDITION_CONFIGURE_SEATS);
522         if (Boolean.FALSE.toString().equals(supported)) {
523             logger.trace("Seat Conditioning not supported");
524         } else {
525             com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningConfigureSeats.Builder builder = ZEVPreconditioningConfigureSeats
526                     .newBuilder();
527
528             HVAC_SEAT_LIST.forEach(seat -> {
529                 ChannelStateMap csm = eventStorage.get(seat);
530                 if (csm != null) {
531                     if (csm.getState() != UnDefType.UNDEF && !seat.equals(channelUID.getId())) {
532                         OnOffType oot = (OnOffType) csm.getState();
533                         switch (seat) {
534                             case GROUP_HVAC + "#" + OH_CHANNEL_FRONT_LEFT:
535                                 builder.setFrontLeft(OnOffType.ON.equals(oot));
536                                 break;
537                             case GROUP_HVAC + "#" + OH_CHANNEL_FRONT_RIGHT:
538                                 builder.setFrontRight(OnOffType.ON.equals(oot));
539                                 break;
540                             case GROUP_HVAC + "#" + OH_CHANNEL_REAR_LEFT:
541                                 builder.setRearLeft(OnOffType.ON.equals(oot));
542                                 break;
543                             case GROUP_HVAC + "#" + OH_CHANNEL_REAR_RIGHT:
544                                 builder.setRearRight(OnOffType.ON.equals(oot));
545                                 break;
546                         }
547                     }
548                 }
549             });
550             ZEVPreconditioningConfigureSeats seats = builder.build();
551             CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
552                     .setRequestId(UUID.randomUUID().toString()).setZevPreconditionConfigureSeats(seats).build();
553             ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
554             accountHandler.get().sendCommand(cm);
555         }
556     }
557
558     public void distributeCommandStatus(AppTwinCommandStatusUpdatesByPID cmdUpdates) {
559         Map<Long, AppTwinCommandStatus> updates = cmdUpdates.getUpdatesByPidMap();
560         updates.forEach((key, value) -> {
561             // Command name
562             ChannelStateMap csmCommand = new ChannelStateMap(OH_CHANNEL_CMD_NAME, GROUP_COMMAND,
563                     new DecimalType(value.getType().getNumber()));
564             updateChannel(csmCommand);
565             // Command State
566             ChannelStateMap csmState = new ChannelStateMap(OH_CHANNEL_CMD_STATE, GROUP_COMMAND,
567                     new DecimalType(value.getState().getNumber()));
568             updateChannel(csmState);
569             // Command Time
570             DateTimeType dtt = Utils.getDateTimeType(value.getTimestampInMs());
571             UOMObserver observer = null;
572             if (Locale.US.getCountry().equals(Utils.getCountry())) {
573                 observer = new UOMObserver(UOMObserver.TIME_US);
574             } else {
575                 observer = new UOMObserver(UOMObserver.TIME_ROW);
576             }
577             ChannelStateMap csmUpdated = new ChannelStateMap(OH_CHANNEL_CMD_LAST_UPDATE, GROUP_COMMAND, dtt, observer);
578             updateChannel(csmUpdated);
579         });
580     }
581
582     public void distributeContent(VEPUpdate data) {
583         updateStatus(ThingStatus.ONLINE);
584         boolean fullUpdate = data.getFullUpdate();
585         /**
586          * Deliver proto update
587          */
588         String newProto = Utils.proto2Json(data, thing.getThingTypeUID());
589         String combinedProto = newProto;
590         ChannelUID protoUpdateChannelUID = new ChannelUID(thing.getUID(), GROUP_VEHICLE, OH_CHANNEL_PROTO_UPDATE);
591         ChannelStateMap oldProtoMap = eventStorage.get(protoUpdateChannelUID.getId());
592         if (oldProtoMap != null) {
593             String oldProto = ((StringType) oldProtoMap.getState()).toFullString();
594             Map<?, ?> combinedMap = Utils.combineMaps(new JSONObject(oldProto).toMap(),
595                     new JSONObject(newProto).toMap());
596             combinedProto = (new JSONObject(combinedMap)).toString();
597         }
598         // proto updates causing large printouts in openhab.log
599         // update channel in case of user connected this channel with an item
600         ChannelStateMap dataUpdateMap = new ChannelStateMap(OH_CHANNEL_PROTO_UPDATE, GROUP_VEHICLE,
601                 StringType.valueOf(combinedProto));
602         updateChannel(dataUpdateMap);
603
604         Map<String, VehicleAttributeStatus> atts = data.getAttributesMap();
605         /**
606          * handle "simple" values
607          */
608         atts.forEach((key, value) -> {
609             ChannelStateMap csm = Mapper.getChannelStateMap(key, value);
610             if (csm.isValid()) {
611                 /**
612                  * Store some values and UOM Observer
613                  */
614                 if (GROUP_HVAC.equals(csm.getGroup())) {
615                     hvacGroupValueStorage.put(csm.getChannel(), csm.getState());
616                 }
617
618                 /**
619                  * handle some specific channels
620                  */
621                 String channel = csm.getChannel();
622                 // handle range channels very specific regarding to vehicle type
623                 boolean block = false;
624                 switch (channel) {
625                     case OH_CHANNEL_RANGE_ELECTRIC:
626                         if (!Constants.COMBUSTION.equals(vehicleType)) {
627                             ChannelStateMap radiusElectric = new ChannelStateMap(OH_CHANNEL_RADIUS_ELECTRIC,
628                                     GROUP_RANGE, guessRangeRadius(csm.getState()), csm.getUomObserver());
629                             updateChannel(radiusElectric);
630                         } else {
631                             block = true;
632                         }
633                         break;
634                     case OH_CHANNEL_RANGE_FUEL:
635                         if (!Constants.BEV.equals(vehicleType)) {
636                             ChannelStateMap radiusFuel = new ChannelStateMap(OH_CHANNEL_RADIUS_FUEL, GROUP_RANGE,
637                                     guessRangeRadius(csm.getState()), csm.getUomObserver());
638                             updateChannel(radiusFuel);
639                         } else {
640                             block = true;
641                         }
642                         break;
643                     case OH_CHANNEL_RANGE_HYBRID:
644                         if (Constants.HYBRID.equals(vehicleType)) {
645                             ChannelStateMap radiusHybrid = new ChannelStateMap(OH_CHANNEL_RADIUS_HYBRID, GROUP_RANGE,
646                                     guessRangeRadius(csm.getState()), csm.getUomObserver());
647                             updateChannel(radiusHybrid);
648                         } else {
649                             block = true;
650                         }
651                         break;
652                     case OH_CHANNEL_SOC:
653                         if (!Constants.COMBUSTION.equals(vehicleType)) {
654                             if (config.get().batteryCapacity > 0) {
655                                 float socValue = ((QuantityType<?>) csm.getState()).floatValue();
656                                 float batteryCapacity = config.get().batteryCapacity;
657                                 float chargedValue = Math.round(socValue * 1000 * batteryCapacity / 1000) / (float) 100;
658                                 ChannelStateMap charged = new ChannelStateMap(OH_CHANNEL_CHARGED, GROUP_RANGE,
659                                         QuantityType.valueOf(chargedValue, Units.KILOWATT_HOUR));
660                                 updateChannel(charged);
661                                 float unchargedValue = Math.round((100 - socValue) * 1000 * batteryCapacity / 1000)
662                                         / (float) 100;
663                                 ChannelStateMap uncharged = new ChannelStateMap(OH_CHANNEL_UNCHARGED, GROUP_RANGE,
664                                         QuantityType.valueOf(unchargedValue, Units.KILOWATT_HOUR));
665                                 updateChannel(uncharged);
666                             } else {
667                                 ChannelStateMap charged = new ChannelStateMap(OH_CHANNEL_CHARGED, GROUP_RANGE,
668                                         QuantityType.valueOf(0, Units.KILOWATT_HOUR));
669                                 updateChannel(charged);
670                                 ChannelStateMap uncharged = new ChannelStateMap(OH_CHANNEL_UNCHARGED, GROUP_RANGE,
671                                         QuantityType.valueOf(0, Units.KILOWATT_HOUR));
672                                 updateChannel(uncharged);
673                             }
674                         } else {
675                             block = true;
676                         }
677                         break;
678                     case OH_CHANNEL_FUEL_LEVEL:
679                         if (!Constants.BEV.equals(vehicleType)) {
680                             if (config.get().fuelCapacity > 0) {
681                                 float fuelLevelValue = ((QuantityType<?>) csm.getState()).floatValue();
682                                 float fuelCapacity = config.get().fuelCapacity;
683                                 float litersInTank = Math.round(fuelLevelValue * 1000 * fuelCapacity / 1000)
684                                         / (float) 100;
685                                 ChannelStateMap tankFilled = new ChannelStateMap(OH_CHANNEL_TANK_REMAIN, GROUP_RANGE,
686                                         QuantityType.valueOf(litersInTank, Mapper.defaultVolumeUnit));
687                                 updateChannel(tankFilled);
688                                 float litersFree = Math.round((100 - fuelLevelValue) * 1000 * fuelCapacity / 1000)
689                                         / (float) 100;
690                                 ChannelStateMap tankOpen = new ChannelStateMap(OH_CHANNEL_TANK_OPEN, GROUP_RANGE,
691                                         QuantityType.valueOf(litersFree, Mapper.defaultVolumeUnit));
692                                 updateChannel(tankOpen);
693                             } else {
694                                 ChannelStateMap tankFilled = new ChannelStateMap(OH_CHANNEL_TANK_REMAIN, GROUP_RANGE,
695                                         QuantityType.valueOf(0, Mapper.defaultVolumeUnit));
696                                 updateChannel(tankFilled);
697                                 ChannelStateMap tankOpen = new ChannelStateMap(OH_CHANNEL_TANK_OPEN, GROUP_RANGE,
698                                         QuantityType.valueOf(0, Mapper.defaultVolumeUnit));
699                                 updateChannel(tankOpen);
700                             }
701                         } else {
702                             block = true;
703                         }
704                         break;
705                     case OH_CHANNEL_COOLANT_FLUID:
706                     case OH_CHANNEL_ENGINE:
707                     case OH_CHANNEL_GAS_FLAP:
708                         if (Constants.BEV.equals(vehicleType)) {
709                             block = true;
710                         }
711                         break;
712                 }
713                 if (!block) {
714                     updateChannel(csm);
715                 }
716             }
717         });
718         /**
719          * handle GPS
720          */
721         if (atts.containsKey(MB_KEY_POSITION_LAT) && atts.containsKey(MB_KEY_POSITION_LONG)) {
722             double lat = Utils.getDouble(atts.get(MB_KEY_POSITION_LAT));
723             double lon = Utils.getDouble(atts.get(MB_KEY_POSITION_LONG));
724             if (lat != -1 && lon != -1) {
725                 PointType pt = new PointType(lat + "," + lon);
726                 updateChannel(new ChannelStateMap(OH_CHANNEL_GPS, Constants.GROUP_POSITION, pt));
727
728                 // calculate distance to home
729                 PointType homePoint = locationProvider.getLocation();
730                 Unit<Length> lengthUnit = KILOMETRE_UNIT;
731                 if (homePoint != null) {
732                     double distance = Utils.distance(homePoint.getLatitude().doubleValue(), lat,
733                             homePoint.getLongitude().doubleValue(), lon, 0.0, 0.0);
734                     UOMObserver observer = new UOMObserver(UOMObserver.LENGTH_KM_UNIT);
735                     if (Locale.US.getCountry().equals(Utils.getCountry())) {
736                         observer = new UOMObserver(UOMObserver.LENGTH_MILES_UNIT);
737                         lengthUnit = ImperialUnits.MILE;
738                     }
739                     updateChannel(new ChannelStateMap(OH_CHANNEL_HOME_DISTANCE, Constants.GROUP_RANGE,
740                             QuantityType.valueOf(distance / 1000, lengthUnit), observer));
741                 } else {
742                     logger.trace("No home location found");
743                 }
744
745             } else {
746                 if (fullUpdate) {
747                     logger.trace("Either Latitude {} or Longitude {} attribute nil", lat, lon);
748                     updateChannel(new ChannelStateMap(OH_CHANNEL_GPS, Constants.GROUP_POSITION, UnDefType.UNDEF));
749                 }
750             }
751         }
752
753         /**
754          * handle temperature point
755          */
756         if (atts.containsKey(MB_KEY_TEMPERATURE_POINTS)) {
757             VehicleAttributeStatus hvacTemperaturePointAttribute = atts.get(MB_KEY_TEMPERATURE_POINTS);
758             if (hvacTemperaturePointAttribute != null) {
759                 if (hvacTemperaturePointAttribute.hasTemperaturePointsValue()) {
760                     TemperaturePointsValue tpValue = hvacTemperaturePointAttribute.getTemperaturePointsValue();
761                     if (tpValue.getTemperaturePointsCount() > 0) {
762                         List<VehicleEvents.TemperaturePoint> tPointList = tpValue.getTemperaturePointsList();
763                         List<CommandOption> commandOptions = new ArrayList<>();
764                         List<StateOption> stateOptions = new ArrayList<>();
765                         tPointList.forEach(point -> {
766                             String zoneName = point.getZone();
767                             int zoneNumber = Utils.getZoneNumber(zoneName);
768                             Unit<Temperature> temperatureUnit = Mapper.defaultTemperatureUnit;
769                             UOMObserver observer = null;
770                             if (hvacTemperaturePointAttribute.hasTemperatureUnit()) {
771                                 observer = new UOMObserver(
772                                         hvacTemperaturePointAttribute.getTemperatureUnit().toString());
773                                 Unit<?> observerUnit = observer.getUnit();
774                                 if (observerUnit != null) {
775                                     temperatureUnit = observerUnit.asType(Temperature.class);
776                                 }
777                             }
778                             ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_HVAC, OH_CHANNEL_TEMPERATURE);
779                             mmcop.setCommandOptions(cuid, Utils.getTemperatureOptions(temperatureUnit));
780                             if (zoneNumber > 0) {
781                                 if (activeTemperaturePoint == -1) {
782                                     activeTemperaturePoint = zoneNumber;
783                                 }
784                                 double temperature = point.getTemperature();
785                                 if (point.getTemperatureDisplayValue() != null) {
786                                     if (point.getTemperatureDisplayValue().strip().length() > 0) {
787                                         try {
788                                             temperature = Double.valueOf(point.getTemperatureDisplayValue());
789                                         } catch (NumberFormatException nfe) {
790                                             logger.trace("Cannot transform Temperature Display Value {} into Double",
791                                                     point.getTemperatureDisplayValue());
792                                         }
793                                     }
794                                 }
795                                 QuantityType<Temperature> temperatureState = QuantityType.valueOf(temperature,
796                                         temperatureUnit);
797                                 temperaturePointsStorage.put(zoneNumber, temperatureState);
798                                 if (activeTemperaturePoint == zoneNumber) {
799                                     ChannelStateMap zoneCSM = new ChannelStateMap(OH_CHANNEL_ZONE, Constants.GROUP_HVAC,
800                                             new DecimalType(activeTemperaturePoint));
801                                     updateChannel(zoneCSM);
802                                     ChannelStateMap tempCSM = new ChannelStateMap(OH_CHANNEL_TEMPERATURE,
803                                             Constants.GROUP_HVAC, temperatureState, observer);
804                                     updateChannel(tempCSM);
805                                 }
806                             } else {
807                                 logger.trace("No Integer mapping found for Temperature Zone {}", zoneName);
808                             }
809                             commandOptions.add(new CommandOption(Integer.toString(zoneNumber), zoneName));
810                             stateOptions.add(new StateOption(Integer.toString(zoneNumber), zoneName));
811                         });
812                         ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_HVAC, OH_CHANNEL_ZONE);
813                         mmcop.setCommandOptions(cuid, commandOptions);
814                         mmsop.setStateOptions(cuid, stateOptions);
815                     } else {
816                         // don't set to undef - maybe partial update
817                         logger.trace("No TemperaturePoints found - list empty");
818                     }
819                 } else {
820                     // don't set to undef - maybe partial update
821                     logger.trace("No TemperaturePointsValue found");
822                 }
823             } else {
824                 // don't set to undef - maybe partial update
825                 logger.trace("No TemperaturePoints found");
826             }
827         } else {
828             // full update acknowledged - set to undef
829             if (fullUpdate) {
830                 ChannelStateMap zoneMap = new ChannelStateMap(OH_CHANNEL_ZONE, Constants.GROUP_HVAC, UnDefType.UNDEF);
831                 updateChannel(zoneMap);
832                 QuantityType<Temperature> tempState = QuantityType.valueOf(-1, Mapper.defaultTemperatureUnit);
833                 ChannelStateMap tempMap = new ChannelStateMap(OH_CHANNEL_TEMPERATURE, Constants.GROUP_HVAC, tempState);
834                 updateChannel(tempMap);
835             }
836         }
837
838         /**
839          * handle Charge Program
840          */
841         if (Constants.BEV.equals(thing.getThingTypeUID().getId())
842                 || Constants.HYBRID.equals(thing.getThingTypeUID().getId())) {
843             VehicleAttributeStatus vas = atts.get(MB_KEY_CHARGE_PROGRAMS);
844             if (vas != null) {
845                 ChargeProgramsValue cpv = vas.getChargeProgramsValue();
846                 if (cpv.getChargeProgramParametersCount() > 0) {
847                     List<ChargeProgramParameters> chargeProgramParameters = cpv.getChargeProgramParametersList();
848                     List<CommandOption> commandOptions = new ArrayList<>();
849                     List<StateOption> stateOptions = new ArrayList<>();
850                     synchronized (chargeGroupValueStorage) {
851                         chargeGroupValueStorage.clear();
852                         chargeProgramParameters.forEach(program -> {
853                             String programName = program.getChargeProgram().name();
854                             int number = Utils.getChargeProgramNumber(programName);
855                             if (number >= 0) {
856                                 JSONObject programValuesJson = new JSONObject();
857                                 programValuesJson.put(Constants.MAX_SOC_KEY, program.getMaxSoc());
858                                 programValuesJson.put(Constants.AUTO_UNLOCK_KEY, program.getAutoUnlock());
859                                 chargeGroupValueStorage.put(Integer.toString(number), programValuesJson);
860                                 commandOptions.add(new CommandOption(Integer.toString(number), programName));
861                                 stateOptions.add(new StateOption(Integer.toString(number), programName));
862
863                             }
864                         });
865                     }
866                     ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_CHARGE, OH_CHANNEL_PROGRAM);
867                     mmcop.setCommandOptions(cuid, commandOptions);
868                     mmsop.setStateOptions(cuid, stateOptions);
869                     vas = atts.get(MB_KEY_SELECTED_CHARGE_PROGRAM);
870                     if (vas != null) {
871                         selectedChargeProgram = (int) vas.getIntValue();
872                         ChargeProgramParameters cpp = cpv.getChargeProgramParameters(selectedChargeProgram);
873                         ChannelStateMap programMap = new ChannelStateMap(OH_CHANNEL_PROGRAM, GROUP_CHARGE,
874                                 DecimalType.valueOf(Integer.toString(selectedChargeProgram)));
875                         updateChannel(programMap);
876                         ChannelStateMap maxSocMap = new ChannelStateMap(OH_CHANNEL_MAX_SOC, GROUP_CHARGE,
877                                 QuantityType.valueOf((double) cpp.getMaxSoc(), Units.PERCENT));
878                         updateChannel(maxSocMap);
879                         ChannelStateMap autoUnlockMap = new ChannelStateMap(OH_CHANNEL_AUTO_UNLOCK, GROUP_CHARGE,
880                                 OnOffType.from(cpp.getAutoUnlock()));
881                         updateChannel(autoUnlockMap);
882                     }
883                 } else {
884                     logger.trace("No Charge Program property available for {}", thing.getThingTypeUID());
885                 }
886             } else {
887                 if (fullUpdate) {
888                     logger.trace("No Charge Programs found");
889                 }
890             }
891         }
892
893         /**
894          * Check if Websocket shall be kept alive
895          */
896         accountHandler.get().keepAlive(ignitionState == 4 || chargingState);
897     }
898
899     /**
900      * Easy function but there's some measures behind:
901      * Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
902      * project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
903      * line from Location A to B.
904      * I've taken some measurements to calculate the overhead factor based on Google Maps
905      * Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
906      * Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
907      * After measuring more distances you'll find out that the outcome is between 70% and 90%. So
908      *
909      * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
910      *
911      * @param s
912      * @return mapping from air-line distance to "real road" distance
913      */
914     public static State guessRangeRadius(State state) {
915         if (state instanceof QuantityType<?> qt) {
916             double radius = qt.intValue() * 0.8;
917             return QuantityType.valueOf(Math.round(radius), qt.getUnit());
918         }
919         return QuantityType.valueOf(-1, Units.ONE);
920     }
921
922     protected void updateChannel(ChannelStateMap csm) {
923         String channel = csm.getChannel();
924         ChannelUID cuid = new ChannelUID(thing.getUID(), csm.getGroup(), channel);
925         eventStorage.put(cuid.getId(), csm);
926
927         /**
928          * proto updates causing large printouts in openhab.log
929          * only log in case of channel is connected to an item
930          */
931         if (OH_CHANNEL_PROTO_UPDATE.equals(csm.getChannel())) {
932             ChannelUID protoUpdateChannelUID = new ChannelUID(thing.getUID(), GROUP_VEHICLE, OH_CHANNEL_PROTO_UPDATE);
933             if (!isLinked(protoUpdateChannelUID)) {
934                 eventStorage.put(protoUpdateChannelUID.getId(), csm);
935                 return;
936             }
937         }
938
939         /**
940          * Check correct channel patterns
941          */
942         if (csm.hasUomObserver()) {
943             UOMObserver deliveredObserver = csm.getUomObserver();
944             UOMObserver storedObserver = unitStorage.get(channel);
945             boolean change = true;
946             if (storedObserver != null) {
947                 change = !storedObserver.equals(deliveredObserver);
948             }
949             // Channel adaptions for items with configurable units
950             String pattern = deliveredObserver.getPattern(csm.getGroup(), csm.getChannel());
951             if (pattern != null) {
952                 if (pattern.startsWith("%") && change) {
953                     mmsop.setStatePattern(cuid, pattern);
954                 } else {
955                     handleComplexTripPattern(channel, pattern);
956                 }
957             }
958             unitStorage.put(channel, deliveredObserver);
959         }
960
961         /**
962          * Check if Websocket shall be kept alive during charging or driving
963          */
964         if (!UnDefType.UNDEF.equals(csm.getState())) {
965             if (GROUP_VEHICLE.equals(csm.getGroup()) && OH_CHANNEL_IGNITION.equals(csm.getChannel())) {
966                 ignitionState = ((DecimalType) csm.getState()).intValue();
967             } else if (GROUP_CHARGE.equals(csm.getGroup()) && OH_CHANNEL_ACTIVE.equals(csm.getChannel())) {
968                 chargingState = OnOffType.ON.equals((csm.getState()));
969             }
970         }
971
972         if (OH_CHANNEL_ZONE.equals(channel) && !UnDefType.UNDEF.equals(csm.getState())) {
973             activeTemperaturePoint = ((DecimalType) csm.getState()).intValue();
974         }
975
976         updateState(cuid, csm.getState());
977     }
978
979     private void handleComplexTripPattern(String channel, String pattern) {
980         switch (channel) {
981             case OH_CHANNEL_CONS_EV:
982             case OH_CHANNEL_CONS_EV_RESET:
983                 StringType consumptionUnitEv = StringType.valueOf(pattern);
984                 ChannelStateMap csmEv = new ChannelStateMap(OH_CHANNEL_CONS_EV_UNIT, GROUP_TRIP, consumptionUnitEv);
985                 updateChannel(csmEv);
986                 break;
987             case OH_CHANNEL_CONS_CONV:
988             case OH_CHANNEL_CONS_CONV_RESET:
989                 StringType consumptionUnitFuel = StringType.valueOf(pattern);
990                 ChannelStateMap csmFuel = new ChannelStateMap(OH_CHANNEL_CONS_CONV_UNIT, GROUP_TRIP,
991                         consumptionUnitFuel);
992                 updateChannel(csmFuel);
993                 break;
994         }
995     }
996
997     @Override
998     public void updateStatus(ThingStatus ts, ThingStatusDetail tsd, @Nullable String details) {
999         super.updateStatus(ts, tsd, details);
1000     }
1001
1002     @Override
1003     public void updateStatus(ThingStatus ts) {
1004         if (ThingStatus.ONLINE.equals(ts) && !ThingStatus.ONLINE.equals(thing.getStatus())) {
1005             if (accountHandler.isPresent()) {
1006                 accountHandler.get().getVehicleCapabilities(config.get().vin);
1007             }
1008         }
1009         super.updateStatus(ts);
1010     }
1011
1012     public void setFeatureCapabilities(@Nullable String capabilities) {
1013         if (capabilities != null) {
1014             ChannelStateMap csm = new ChannelStateMap(MB_KEY_FEATURE_CAPABILITIES, GROUP_VEHICLE,
1015                     StringType.valueOf(capabilities));
1016             updateChannel(csm);
1017         }
1018     }
1019
1020     public void setCommandCapabilities(@Nullable String capabilities) {
1021         if (capabilities != null) {
1022             ChannelStateMap csm = new ChannelStateMap(MB_KEY_COMMAND_CAPABILITIES, GROUP_VEHICLE,
1023                     StringType.valueOf(capabilities));
1024             updateChannel(csm);
1025         }
1026     }
1027
1028     private void setCommandStateOptions() {
1029         List<StateOption> commandTypeOptions = new ArrayList<>();
1030         CommandType[] ctValues = CommandType.values();
1031         for (int i = 0; i < ctValues.length; i++) {
1032             if (!UNRECOGNIZED.equals(ctValues[i].toString())) {
1033                 StateOption co = new StateOption(Integer.toString(ctValues[i].getNumber()), ctValues[i].toString());
1034                 commandTypeOptions.add(co);
1035             }
1036         }
1037         mmsop.setStateOptions(new ChannelUID(thing.getUID(), GROUP_COMMAND, OH_CHANNEL_CMD_NAME), commandTypeOptions);
1038         List<StateOption> commandStateOptions = new ArrayList<>();
1039         CommandState[] csValues = CommandState.values();
1040         for (int j = 0; j < csValues.length; j++) {
1041             if (!UNRECOGNIZED.equals(csValues[j].toString())) {
1042                 StateOption so = new StateOption(Integer.toString(csValues[j].getNumber()), csValues[j].toString());
1043                 commandStateOptions.add(so);
1044             }
1045         }
1046         mmsop.setStateOptions(new ChannelUID(thing.getUID(), GROUP_COMMAND, OH_CHANNEL_CMD_STATE), commandStateOptions);
1047     }
1048
1049     /**
1050      * Vehicle Actions
1051      */
1052     @Override
1053     public Collection<Class<? extends ThingHandlerService>> getServices() {
1054         return Collections.singleton(VehicleActions.class);
1055     }
1056
1057     public void sendPoi(JSONObject poi) {
1058         accountHandler.get().sendPoi(config.get().vin, poi);
1059     }
1060 }