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