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