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