2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mercedesme.internal.handler;
15 import static org.openhab.binding.mercedesme.internal.Constants.*;
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;
25 import java.util.Optional;
26 import java.util.UUID;
28 import javax.measure.Unit;
29 import javax.measure.quantity.Length;
30 import javax.measure.quantity.Temperature;
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;
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;
110 * {@link VehicleHandler} transform data into state updates and handling of vehicle commands
112 * @author Bernd Weymann - Initial contribution
115 public class VehicleHandler extends BaseThingHandler {
116 private static final List<String> HVAC_SEAT_LIST = Arrays
117 .asList(new String[] { GROUP_HVAC + "#" + OH_CHANNEL_FRONT_LEFT, GROUP_HVAC + "#" + OH_CHANNEL_FRONT_RIGHT,
118 GROUP_HVAC + "#" + OH_CHANNEL_REAR_LEFT, GROUP_HVAC + "#" + OH_CHANNEL_REAR_RIGHT });
120 private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
121 private final LocationProvider locationProvider;
122 private final MercedesMeCommandOptionProvider mmcop;
123 private final MercedesMeStateOptionProvider mmsop;
125 private Map<String, UOMObserver> unitStorage = new HashMap<>();
126 private int ignitionState = -1;
127 private boolean chargingState = false;
128 private int selectedChargeProgram = -1;
129 private int activeTemperaturePoint = -1;
130 private Map<Integer, QuantityType<Temperature>> temperaturePointsStorage = new HashMap<>();
131 private JSONObject chargeGroupValueStorage = new JSONObject();
132 private Map<String, State> hvacGroupValueStorage = new HashMap<>();
133 private String vehicleType = NOT_SET;
135 Map<String, ChannelStateMap> eventStorage = new HashMap<>();
136 Optional<AccountHandler> accountHandler = Optional.empty();
137 Optional<VehicleConfiguration> config = Optional.empty();
139 public VehicleHandler(Thing thing, LocationProvider lp, MercedesMeCommandOptionProvider cop,
140 MercedesMeStateOptionProvider sop) {
142 vehicleType = thing.getThingTypeUID().getId();
143 locationProvider = lp;
149 public void initialize() {
150 config = Optional.of(getConfigAs(VehicleConfiguration.class));
151 Bridge bridge = getBridge();
152 if (bridge != null) {
153 updateStatus(ThingStatus.UNKNOWN);
154 BridgeHandler handler = bridge.getHandler();
155 if (handler != null) {
156 setCommandStateOptions();
157 accountHandler = Optional.of((AccountHandler) handler);
158 accountHandler.get().registerVin(config.get().vin, this);
160 throw new IllegalStateException("BridgeHandler is null");
163 String textKey = Constants.STATUS_TEXT_PREFIX + "vehicle" + Constants.STATUS_BRIDGE_MISSING;
164 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, textKey);
169 public void dispose() {
170 accountHandler.get().unregisterVin(config.get().vin);
175 public void handleCommand(ChannelUID channelUID, Command command) {
177 * Commands shall be not that frequent so trace level for identifying problems should be feasible
179 logger.trace("Received command {} {} for {}", command.getClass(), command, channelUID);
180 if (command instanceof RefreshType) {
181 if (MB_KEY_FEATURE_CAPABILITIES.equals(channelUID.getIdWithoutGroup())
182 || MB_KEY_COMMAND_CAPABILITIES.equals(channelUID.getIdWithoutGroup())) {
183 accountHandler.ifPresent(ah -> {
184 ah.getVehicleCapabilities(config.get().vin);
187 // deliver from event storage
188 ChannelStateMap csm = eventStorage.get(channelUID.getId());
193 // ensure unit update
194 unitStorage.remove(channelUID.getIdWithoutGroup());
195 } else if (Constants.GROUP_VEHICLE.equals(channelUID.getGroupId())) {
197 * Commands for Vehicle
199 if (OH_CHANNEL_IGNITION.equals(channelUID.getIdWithoutGroup())) {
200 String supported = thing.getProperties().get(MB_KEY_COMMAND_ENGINE_START);
201 if (Boolean.FALSE.toString().equals(supported)) {
202 logger.trace("Engine Start/Stop not supported");
204 int commandValue = ((DecimalType) command).intValue();
205 if (commandValue == 4) {
206 String pin = accountHandler.get().config.get().pin;
207 if (Constants.NOT_SET.equals(pin)) {
208 logger.trace("Security PIN missing in Account bridge");
210 EngineStart eStart = EngineStart.newBuilder().setPin(pin).build();
211 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
212 .setRequestId(UUID.randomUUID().toString()).setEngineStart(eStart).build();
213 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
214 accountHandler.get().sendCommand(cm);
216 } else if (commandValue == 0) {
217 EngineStop eStop = EngineStop.newBuilder().build();
218 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
219 .setRequestId(UUID.randomUUID().toString()).setEngineStop(eStop).build();
220 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
221 accountHandler.get().sendCommand(cm);
224 } else if (OH_CHANNEL_WINDOWS.equals(channelUID.getIdWithoutGroup())) {
225 String supported = thing.getProperties().get(MB_KEY_COMMAND_WINDOWS_OPEN);
226 String pin = accountHandler.get().config.get().pin;
227 if (Boolean.FALSE.toString().equals(supported)) {
228 logger.trace("Windows control not supported");
232 switch (((DecimalType) command).intValue()) {
234 if (Constants.NOT_SET.equals(pin)) {
235 logger.trace("Security PIN missing in Account bridge");
237 WindowsVentilate wv = WindowsVentilate.newBuilder().setPin(pin).build();
238 cr = CommandRequest.newBuilder().setVin(config.get().vin)
239 .setRequestId(UUID.randomUUID().toString()).setWindowsVentilate(wv).build();
240 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
241 accountHandler.get().sendCommand(cm);
245 WindowsClose wc = WindowsClose.newBuilder().build();
246 cr = CommandRequest.newBuilder().setVin(config.get().vin)
247 .setRequestId(UUID.randomUUID().toString()).setWindowsClose(wc).build();
248 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
249 accountHandler.get().sendCommand(cm);
252 if (Constants.NOT_SET.equals(pin)) {
253 logger.trace("Security PIN missing in Account bridge");
255 WindowsOpen wo = WindowsOpen.newBuilder().setPin(pin).build();
256 cr = CommandRequest.newBuilder().setVin(config.get().vin)
257 .setRequestId(UUID.randomUUID().toString()).setWindowsOpen(wo).build();
258 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
259 accountHandler.get().sendCommand(cm);
263 logger.trace("No Windows movement known for {}", command);
267 } else if (OH_CHANNEL_LOCK.equals(channelUID.getIdWithoutGroup())) {
268 String pin = accountHandler.get().config.get().pin;
269 String supported = thing.getProperties().get(MB_KEY_COMMAND_DOORS_LOCK);
270 if (Boolean.FALSE.toString().equals(supported)) {
271 logger.trace("Door Lock not supported");
273 switch (((DecimalType) command).intValue()) {
275 DoorsLock dl = DoorsLock.newBuilder().build();
276 CommandRequest lockCr = CommandRequest.newBuilder().setVin(config.get().vin)
277 .setRequestId(UUID.randomUUID().toString()).setDoorsLock(dl).build();
278 ClientMessage lockCm = ClientMessage.newBuilder().setCommandRequest(lockCr).build();
279 accountHandler.get().sendCommand(lockCm);
282 if (Constants.NOT_SET.equals(pin)) {
283 logger.trace("Security PIN missing in Account bridge");
285 DoorsUnlock du = DoorsUnlock.newBuilder().setPin(pin).build();
286 CommandRequest unlockCr = CommandRequest.newBuilder().setVin(config.get().vin)
287 .setRequestId(UUID.randomUUID().toString()).setDoorsUnlock(du).build();
288 ClientMessage unlockCm = ClientMessage.newBuilder().setCommandRequest(unlockCr).build();
289 accountHandler.get().sendCommand(unlockCm);
293 logger.trace("No lock command mapped to {}", command);
298 } else if (Constants.GROUP_HVAC.equals(channelUID.getGroupId())) {
302 if (OH_CHANNEL_TEMPERATURE.equals(channelUID.getIdWithoutGroup())) {
303 String supported = thing.getProperties().get(MB_KEY_COMMAND_ZEV_PRECONDITION_CONFIGURE);
304 if (Boolean.FALSE.toString().equals(supported)) {
305 logger.trace("Air Conditioning Temperature Setting not supported");
307 QuantityType<?> targetTemp = (QuantityType<?>) command;
308 QuantityType<?> targetTempCelsius = targetTemp.toInvertibleUnit(SIUnits.CELSIUS);
309 if (targetTempCelsius != null) {
310 TemperatureConfigure tc = TemperatureConfigure.newBuilder()
311 .addTemperaturePoints(TemperaturePoint.newBuilder().setZoneValue(activeTemperaturePoint)
312 .setTemperatureInCelsius(targetTempCelsius.intValue()).build())
314 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
315 .setRequestId(UUID.randomUUID().toString()).setTemperatureConfigure(tc).build();
316 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
317 accountHandler.get().sendCommand(cm);
320 } else if (OH_CHANNEL_ACTIVE.equals(channelUID.getIdWithoutGroup())) {
321 String supported = thing.getProperties().get(MB_KEY_COMMAND_ZEV_PRECONDITIONING_START);
322 if (Boolean.FALSE.toString().equals(supported)) {
323 logger.trace("Air Conditioning not supported");
325 if (OnOffType.ON.equals(command)) {
326 ZEVPreconditioningStart precondStart = ZEVPreconditioningStart.newBuilder()
327 .setType(ZEVPreconditioningType.NOW).build();
328 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
329 .setRequestId(UUID.randomUUID().toString()).setZevPreconditioningStart(precondStart)
331 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
332 accountHandler.get().sendCommand(cm);
334 ZEVPreconditioningStop precondStop = ZEVPreconditioningStop.newBuilder()
335 .setType(ZEVPreconditioningType.NOW).build();
336 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
337 .setRequestId(UUID.randomUUID().toString()).setZevPreconditioningStop(precondStop)
339 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
340 accountHandler.get().sendCommand(cm);
343 } else if (OH_CHANNEL_FRONT_LEFT.equals(channelUID.getIdWithoutGroup())) {
344 configureSeats(channelUID, (State) command);
345 } else if (OH_CHANNEL_FRONT_RIGHT.equals(channelUID.getIdWithoutGroup())) {
346 configureSeats(channelUID, (State) command);
347 } else if (OH_CHANNEL_REAR_LEFT.equals(channelUID.getIdWithoutGroup())) {
348 configureSeats(channelUID, (State) command);
349 } else if (OH_CHANNEL_REAR_RIGHT.equals(channelUID.getIdWithoutGroup())) {
350 configureSeats(channelUID, (State) command);
351 } else if (OH_CHANNEL_AUX_HEAT.equals(channelUID.getIdWithoutGroup())) {
352 String supported = thing.getProperties().get(MB_KEY_FEATURE_AUX_HEAT);
353 if (Boolean.FALSE.toString().equals(supported)) {
354 logger.trace("Auxiliary Heating not supported");
356 if (OnOffType.ON.equals(command)) {
357 AuxheatStart auxHeatStart = AuxheatStart.newBuilder().build();
358 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
359 .setRequestId(UUID.randomUUID().toString()).setAuxheatStart(auxHeatStart).build();
360 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
361 accountHandler.get().sendCommand(cm);
363 AuxheatStop auxHeatStop = AuxheatStop.newBuilder().build();
364 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
365 .setRequestId(UUID.randomUUID().toString()).setAuxheatStop(auxHeatStop).build();
366 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
367 accountHandler.get().sendCommand(cm);
370 } else if (OH_CHANNEL_ZONE.equals(channelUID.getIdWithoutGroup())) {
371 int zone = ((DecimalType) command).intValue();
372 if (temperaturePointsStorage.containsKey(zone)) {
373 ChannelStateMap zoneMap = new ChannelStateMap(OH_CHANNEL_ZONE, GROUP_HVAC, (DecimalType) command);
374 updateChannel(zoneMap);
375 QuantityType<Temperature> selectedTemp = temperaturePointsStorage.get(zone);
376 if (selectedTemp != null) {
377 ChannelStateMap tempCSM = new ChannelStateMap(OH_CHANNEL_TEMPERATURE, GROUP_HVAC, selectedTemp);
378 updateChannel(tempCSM);
381 logger.trace("No Temperature Zone found for {}", command);
384 } else if (Constants.GROUP_POSITION.equals(channelUID.getGroupId())) {
386 * Commands for Positioning
388 if (OH_CHANNEL_SIGNAL.equals(channelUID.getIdWithoutGroup())) {
389 String supported = thing.getProperties().get(MB_KEY_COMMAND_SIGPOS_START);
390 if (Boolean.FALSE.toString().equals(supported)) {
391 logger.trace("Signal Position not supported");
396 switch (((DecimalType) command).intValue()) {
398 sps = SigPosStart.newBuilder().setSigposType(SigposType.LIGHT_ONLY)
399 .setLightType(LightType.DIPPED_HEAD_LIGHT).setSigposDuration(10).build();
400 cr = CommandRequest.newBuilder().setVin(config.get().vin)
401 .setRequestId(UUID.randomUUID().toString()).setSigposStart(sps).build();
402 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
403 accountHandler.get().sendCommand(cm);
406 sps = SigPosStart.newBuilder().setSigposType(SigposType.HORN_ONLY).setHornRepeat(3)
407 .setHornType(HornType.HORN_LOW_VOLUME).build();
408 cr = CommandRequest.newBuilder().setVin(config.get().vin)
409 .setRequestId(UUID.randomUUID().toString()).setSigposStart(sps).build();
410 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
411 accountHandler.get().sendCommand(cm);
414 logger.trace("No Positioning known for {}", command);
418 } else if (Constants.GROUP_CHARGE.equals(channelUID.getGroupId())) {
420 * Commands for Charging
422 synchronized (chargeGroupValueStorage) {
423 int maxSocToSelect = 80;
424 boolean autoUnlockToSelect = false;
425 String supported = thing.getProperties().get(MB_KEY_COMMAND_CHARGE_PROGRAM_CONFIGURE);
426 if (Boolean.FALSE.toString().equals(supported)) {
427 logger.trace("Charge Program Configure not supported");
429 boolean sendCommand = false;
430 if (OH_CHANNEL_PROGRAM.equals(channelUID.getIdWithoutGroup())) {
431 selectedChargeProgram = ((DecimalType) command).intValue();
432 if (chargeGroupValueStorage.has(Integer.toString(selectedChargeProgram))) {
433 maxSocToSelect = chargeGroupValueStorage
434 .getJSONObject(Integer.toString(selectedChargeProgram))
435 .getInt(Constants.MAX_SOC_KEY);
436 autoUnlockToSelect = chargeGroupValueStorage
437 .getJSONObject(Integer.toString(selectedChargeProgram))
438 .getBoolean(Constants.AUTO_UNLOCK_KEY);
439 updateChannel(new ChannelStateMap(OH_CHANNEL_MAX_SOC, GROUP_CHARGE,
440 QuantityType.valueOf(maxSocToSelect, Units.PERCENT)));
441 updateChannel(new ChannelStateMap(OH_CHANNEL_AUTO_UNLOCK, GROUP_CHARGE,
442 OnOffType.from(autoUnlockToSelect)));
445 logger.trace("No charge program found for {}", selectedChargeProgram);
448 if (OH_CHANNEL_AUTO_UNLOCK.equals(channelUID.getIdWithoutGroup())) {
449 autoUnlockToSelect = ((OnOffType) command).equals(OnOffType.ON);
451 } else if (OH_CHANNEL_MAX_SOC.equals(channelUID.getIdWithoutGroup())) {
452 maxSocToSelect = ((QuantityType<?>) command).intValue();
454 } // else - nothing to be sent
456 Int32Value maxSocValue = Int32Value.newBuilder().setValue(maxSocToSelect).build();
457 BoolValue autoUnlockValue = BoolValue.newBuilder().setValue(autoUnlockToSelect).build();
458 ChargeProgramConfigure cpc = ChargeProgramConfigure.newBuilder()
459 .setChargeProgramValue(selectedChargeProgram).setMaxSoc(maxSocValue)
460 .setAutoUnlock(autoUnlockValue).build();
461 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
462 .setRequestId(UUID.randomUUID().toString()).setChargeProgramConfigure(cpc).build();
463 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
464 accountHandler.get().sendCommand(cm);
468 } else if (Constants.GROUP_DOORS.equals(channelUID.getGroupId())) {
472 if (OH_CHANNEL_SUNROOF.equals(channelUID.getIdWithoutGroup())) {
473 String supported = thing.getProperties().get(MB_KEY_COMMAND_SUNROOF_OPEN);
474 String pin = accountHandler.get().config.get().pin;
475 if (Boolean.FALSE.toString().equals(supported)) {
476 logger.trace("Sunroof control not supported");
480 switch (((DecimalType) command).intValue()) {
482 SunroofClose sc = SunroofClose.newBuilder().build();
483 cr = CommandRequest.newBuilder().setVin(config.get().vin)
484 .setRequestId(UUID.randomUUID().toString()).setSunroofClose(sc).build();
485 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
486 accountHandler.get().sendCommand(cm);
489 if (Constants.NOT_SET.equals(pin)) {
490 logger.trace("Security PIN missing in Account bridge");
492 SunroofOpen so = SunroofOpen.newBuilder().setPin(pin).build();
493 cr = CommandRequest.newBuilder().setVin(config.get().vin)
494 .setRequestId(UUID.randomUUID().toString()).setSunroofOpen(so).build();
495 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
496 accountHandler.get().sendCommand(cm);
500 if (Constants.NOT_SET.equals(pin)) {
501 logger.trace("Security PIN missing in Account bridge");
503 SunroofLift sl = SunroofLift.newBuilder().setPin(pin).build();
504 cr = CommandRequest.newBuilder().setVin(config.get().vin)
505 .setRequestId(UUID.randomUUID().toString()).setSunroofLift(sl).build();
506 cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
507 accountHandler.get().sendCommand(cm);
511 logger.trace("No Sunroof movement known for {}", command);
516 logger.trace("No command {} found for {}", command, channelUID.getAsString());
520 private void configureSeats(ChannelUID channelUID, State command) {
521 String supported = thing.getProperties().get(MB_KEY_COMMAND_ZEV_PRECONDITION_CONFIGURE_SEATS);
522 if (Boolean.FALSE.toString().equals(supported)) {
523 logger.trace("Seat Conditioning not supported");
525 com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningConfigureSeats.Builder builder = ZEVPreconditioningConfigureSeats
528 HVAC_SEAT_LIST.forEach(seat -> {
529 ChannelStateMap csm = eventStorage.get(seat);
531 if (csm.getState() != UnDefType.UNDEF && !seat.equals(channelUID.getId())) {
532 OnOffType oot = (OnOffType) csm.getState();
534 case GROUP_HVAC + "#" + OH_CHANNEL_FRONT_LEFT:
535 builder.setFrontLeft(OnOffType.ON.equals(oot));
537 case GROUP_HVAC + "#" + OH_CHANNEL_FRONT_RIGHT:
538 builder.setFrontRight(OnOffType.ON.equals(oot));
540 case GROUP_HVAC + "#" + OH_CHANNEL_REAR_LEFT:
541 builder.setRearLeft(OnOffType.ON.equals(oot));
543 case GROUP_HVAC + "#" + OH_CHANNEL_REAR_RIGHT:
544 builder.setRearRight(OnOffType.ON.equals(oot));
550 ZEVPreconditioningConfigureSeats seats = builder.build();
551 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
552 .setRequestId(UUID.randomUUID().toString()).setZevPreconditionConfigureSeats(seats).build();
553 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
554 accountHandler.get().sendCommand(cm);
558 public void distributeCommandStatus(AppTwinCommandStatusUpdatesByPID cmdUpdates) {
559 Map<Long, AppTwinCommandStatus> updates = cmdUpdates.getUpdatesByPidMap();
560 updates.forEach((key, value) -> {
562 ChannelStateMap csmCommand = new ChannelStateMap(OH_CHANNEL_CMD_NAME, GROUP_COMMAND,
563 new DecimalType(value.getType().getNumber()));
564 updateChannel(csmCommand);
566 ChannelStateMap csmState = new ChannelStateMap(OH_CHANNEL_CMD_STATE, GROUP_COMMAND,
567 new DecimalType(value.getState().getNumber()));
568 updateChannel(csmState);
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);
575 observer = new UOMObserver(UOMObserver.TIME_ROW);
577 ChannelStateMap csmUpdated = new ChannelStateMap(OH_CHANNEL_CMD_LAST_UPDATE, GROUP_COMMAND, dtt, observer);
578 updateChannel(csmUpdated);
582 public void distributeContent(VEPUpdate data) {
583 updateStatus(ThingStatus.ONLINE);
584 boolean fullUpdate = data.getFullUpdate();
586 * Deliver proto update
588 String newProto = Utils.proto2Json(data, thing.getThingTypeUID());
589 String combinedProto = newProto;
590 ChannelUID protoUpdateChannelUID = new ChannelUID(thing.getUID(), GROUP_VEHICLE, OH_CHANNEL_PROTO_UPDATE);
591 ChannelStateMap oldProtoMap = eventStorage.get(protoUpdateChannelUID.getId());
592 if (oldProtoMap != null) {
593 String oldProto = ((StringType) oldProtoMap.getState()).toFullString();
594 Map<?, ?> combinedMap = Utils.combineMaps(new JSONObject(oldProto).toMap(),
595 new JSONObject(newProto).toMap());
596 combinedProto = (new JSONObject(combinedMap)).toString();
598 // proto updates causing large printouts in openhab.log
599 // update channel in case of user connected this channel with an item
600 ChannelStateMap dataUpdateMap = new ChannelStateMap(OH_CHANNEL_PROTO_UPDATE, GROUP_VEHICLE,
601 StringType.valueOf(combinedProto));
602 updateChannel(dataUpdateMap);
604 Map<String, VehicleAttributeStatus> atts = data.getAttributesMap();
606 * handle "simple" values
608 atts.forEach((key, value) -> {
609 ChannelStateMap csm = Mapper.getChannelStateMap(key, value);
612 * Store some values and UOM Observer
614 if (GROUP_HVAC.equals(csm.getGroup())) {
615 hvacGroupValueStorage.put(csm.getChannel(), csm.getState());
619 * handle some specific channels
621 String channel = csm.getChannel();
622 // handle range channels very specific regarding to vehicle type
623 boolean block = false;
625 case OH_CHANNEL_RANGE_ELECTRIC:
626 if (!Constants.COMBUSTION.equals(vehicleType)) {
627 ChannelStateMap radiusElectric = new ChannelStateMap(OH_CHANNEL_RADIUS_ELECTRIC,
628 GROUP_RANGE, guessRangeRadius(csm.getState()), csm.getUomObserver());
629 updateChannel(radiusElectric);
634 case OH_CHANNEL_RANGE_FUEL:
635 if (!Constants.BEV.equals(vehicleType)) {
636 ChannelStateMap radiusFuel = new ChannelStateMap(OH_CHANNEL_RADIUS_FUEL, GROUP_RANGE,
637 guessRangeRadius(csm.getState()), csm.getUomObserver());
638 updateChannel(radiusFuel);
643 case OH_CHANNEL_RANGE_HYBRID:
644 if (Constants.HYBRID.equals(vehicleType)) {
645 ChannelStateMap radiusHybrid = new ChannelStateMap(OH_CHANNEL_RADIUS_HYBRID, GROUP_RANGE,
646 guessRangeRadius(csm.getState()), csm.getUomObserver());
647 updateChannel(radiusHybrid);
653 if (!Constants.COMBUSTION.equals(vehicleType)) {
654 if (config.get().batteryCapacity > 0) {
655 float socValue = ((QuantityType<?>) csm.getState()).floatValue();
656 float batteryCapacity = config.get().batteryCapacity;
657 float chargedValue = Math.round(socValue * 1000 * batteryCapacity / 1000) / (float) 100;
658 ChannelStateMap charged = new ChannelStateMap(OH_CHANNEL_CHARGED, GROUP_RANGE,
659 QuantityType.valueOf(chargedValue, Units.KILOWATT_HOUR));
660 updateChannel(charged);
661 float unchargedValue = Math.round((100 - socValue) * 1000 * batteryCapacity / 1000)
663 ChannelStateMap uncharged = new ChannelStateMap(OH_CHANNEL_UNCHARGED, GROUP_RANGE,
664 QuantityType.valueOf(unchargedValue, Units.KILOWATT_HOUR));
665 updateChannel(uncharged);
667 ChannelStateMap charged = new ChannelStateMap(OH_CHANNEL_CHARGED, GROUP_RANGE,
668 QuantityType.valueOf(0, Units.KILOWATT_HOUR));
669 updateChannel(charged);
670 ChannelStateMap uncharged = new ChannelStateMap(OH_CHANNEL_UNCHARGED, GROUP_RANGE,
671 QuantityType.valueOf(0, Units.KILOWATT_HOUR));
672 updateChannel(uncharged);
678 case OH_CHANNEL_FUEL_LEVEL:
679 if (!Constants.BEV.equals(vehicleType)) {
680 if (config.get().fuelCapacity > 0) {
681 float fuelLevelValue = ((QuantityType<?>) csm.getState()).floatValue();
682 float fuelCapacity = config.get().fuelCapacity;
683 float litersInTank = Math.round(fuelLevelValue * 1000 * fuelCapacity / 1000)
685 ChannelStateMap tankFilled = new ChannelStateMap(OH_CHANNEL_TANK_REMAIN, GROUP_RANGE,
686 QuantityType.valueOf(litersInTank, Mapper.defaultVolumeUnit));
687 updateChannel(tankFilled);
688 float litersFree = Math.round((100 - fuelLevelValue) * 1000 * fuelCapacity / 1000)
690 ChannelStateMap tankOpen = new ChannelStateMap(OH_CHANNEL_TANK_OPEN, GROUP_RANGE,
691 QuantityType.valueOf(litersFree, Mapper.defaultVolumeUnit));
692 updateChannel(tankOpen);
694 ChannelStateMap tankFilled = new ChannelStateMap(OH_CHANNEL_TANK_REMAIN, GROUP_RANGE,
695 QuantityType.valueOf(0, Mapper.defaultVolumeUnit));
696 updateChannel(tankFilled);
697 ChannelStateMap tankOpen = new ChannelStateMap(OH_CHANNEL_TANK_OPEN, GROUP_RANGE,
698 QuantityType.valueOf(0, Mapper.defaultVolumeUnit));
699 updateChannel(tankOpen);
705 case OH_CHANNEL_COOLANT_FLUID:
706 case OH_CHANNEL_ENGINE:
707 case OH_CHANNEL_GAS_FLAP:
708 if (Constants.BEV.equals(vehicleType)) {
721 if (atts.containsKey(MB_KEY_POSITION_LAT) && atts.containsKey(MB_KEY_POSITION_LONG)) {
722 double lat = Utils.getDouble(atts.get(MB_KEY_POSITION_LAT));
723 double lon = Utils.getDouble(atts.get(MB_KEY_POSITION_LONG));
724 if (lat != -1 && lon != -1) {
725 PointType pt = new PointType(lat + "," + lon);
726 updateChannel(new ChannelStateMap(OH_CHANNEL_GPS, Constants.GROUP_POSITION, pt));
728 // calculate distance to home
729 PointType homePoint = locationProvider.getLocation();
730 Unit<Length> lengthUnit = KILOMETRE_UNIT;
731 if (homePoint != null) {
732 double distance = Utils.distance(homePoint.getLatitude().doubleValue(), lat,
733 homePoint.getLongitude().doubleValue(), lon, 0.0, 0.0);
734 UOMObserver observer = new UOMObserver(UOMObserver.LENGTH_KM_UNIT);
735 if (Locale.US.getCountry().equals(Utils.getCountry())) {
736 observer = new UOMObserver(UOMObserver.LENGTH_MILES_UNIT);
737 lengthUnit = ImperialUnits.MILE;
739 updateChannel(new ChannelStateMap(OH_CHANNEL_HOME_DISTANCE, Constants.GROUP_RANGE,
740 QuantityType.valueOf(distance / 1000, lengthUnit), observer));
742 logger.trace("No home location found");
747 logger.trace("Either Latitude {} or Longitude {} attribute nil", lat, lon);
748 updateChannel(new ChannelStateMap(OH_CHANNEL_GPS, Constants.GROUP_POSITION, UnDefType.UNDEF));
754 * handle temperature point
756 if (atts.containsKey(MB_KEY_TEMPERATURE_POINTS)) {
757 VehicleAttributeStatus hvacTemperaturePointAttribute = atts.get(MB_KEY_TEMPERATURE_POINTS);
758 if (hvacTemperaturePointAttribute != null) {
759 if (hvacTemperaturePointAttribute.hasTemperaturePointsValue()) {
760 TemperaturePointsValue tpValue = hvacTemperaturePointAttribute.getTemperaturePointsValue();
761 if (tpValue.getTemperaturePointsCount() > 0) {
762 List<VehicleEvents.TemperaturePoint> tPointList = tpValue.getTemperaturePointsList();
763 List<CommandOption> commandOptions = new ArrayList<>();
764 List<StateOption> stateOptions = new ArrayList<>();
765 tPointList.forEach(point -> {
766 String zoneName = point.getZone();
767 int zoneNumber = Utils.getZoneNumber(zoneName);
768 Unit<Temperature> temperatureUnit = Mapper.defaultTemperatureUnit;
769 UOMObserver observer = null;
770 if (hvacTemperaturePointAttribute.hasTemperatureUnit()) {
771 observer = new UOMObserver(
772 hvacTemperaturePointAttribute.getTemperatureUnit().toString());
773 Unit<?> observerUnit = observer.getUnit();
774 if (observerUnit != null) {
775 temperatureUnit = observerUnit.asType(Temperature.class);
778 ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_HVAC, OH_CHANNEL_TEMPERATURE);
779 mmcop.setCommandOptions(cuid, Utils.getTemperatureOptions(temperatureUnit));
780 if (zoneNumber > 0) {
781 if (activeTemperaturePoint == -1) {
782 activeTemperaturePoint = zoneNumber;
784 double temperature = point.getTemperature();
785 if (point.getTemperatureDisplayValue() != null) {
786 if (point.getTemperatureDisplayValue().strip().length() > 0) {
788 temperature = Double.valueOf(point.getTemperatureDisplayValue());
789 } catch (NumberFormatException nfe) {
790 logger.trace("Cannot transform Temperature Display Value {} into Double",
791 point.getTemperatureDisplayValue());
795 QuantityType<Temperature> temperatureState = QuantityType.valueOf(temperature,
797 temperaturePointsStorage.put(zoneNumber, temperatureState);
798 if (activeTemperaturePoint == zoneNumber) {
799 ChannelStateMap zoneCSM = new ChannelStateMap(OH_CHANNEL_ZONE, Constants.GROUP_HVAC,
800 new DecimalType(activeTemperaturePoint));
801 updateChannel(zoneCSM);
802 ChannelStateMap tempCSM = new ChannelStateMap(OH_CHANNEL_TEMPERATURE,
803 Constants.GROUP_HVAC, temperatureState, observer);
804 updateChannel(tempCSM);
807 logger.trace("No Integer mapping found for Temperature Zone {}", zoneName);
809 commandOptions.add(new CommandOption(Integer.toString(zoneNumber), zoneName));
810 stateOptions.add(new StateOption(Integer.toString(zoneNumber), zoneName));
812 ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_HVAC, OH_CHANNEL_ZONE);
813 mmcop.setCommandOptions(cuid, commandOptions);
814 mmsop.setStateOptions(cuid, stateOptions);
816 // don't set to undef - maybe partial update
817 logger.trace("No TemperaturePoints found - list empty");
820 // don't set to undef - maybe partial update
821 logger.trace("No TemperaturePointsValue found");
824 // don't set to undef - maybe partial update
825 logger.trace("No TemperaturePoints found");
828 // full update acknowledged - set to undef
830 ChannelStateMap zoneMap = new ChannelStateMap(OH_CHANNEL_ZONE, Constants.GROUP_HVAC, UnDefType.UNDEF);
831 updateChannel(zoneMap);
832 QuantityType<Temperature> tempState = QuantityType.valueOf(-1, Mapper.defaultTemperatureUnit);
833 ChannelStateMap tempMap = new ChannelStateMap(OH_CHANNEL_TEMPERATURE, Constants.GROUP_HVAC, tempState);
834 updateChannel(tempMap);
839 * handle Charge Program
841 if (Constants.BEV.equals(thing.getThingTypeUID().getId())
842 || Constants.HYBRID.equals(thing.getThingTypeUID().getId())) {
843 VehicleAttributeStatus vas = atts.get(MB_KEY_CHARGE_PROGRAMS);
845 ChargeProgramsValue cpv = vas.getChargeProgramsValue();
846 if (cpv.getChargeProgramParametersCount() > 0) {
847 List<ChargeProgramParameters> chargeProgramParameters = cpv.getChargeProgramParametersList();
848 List<CommandOption> commandOptions = new ArrayList<>();
849 List<StateOption> stateOptions = new ArrayList<>();
850 synchronized (chargeGroupValueStorage) {
851 chargeGroupValueStorage.clear();
852 chargeProgramParameters.forEach(program -> {
853 String programName = program.getChargeProgram().name();
854 int number = Utils.getChargeProgramNumber(programName);
856 JSONObject programValuesJson = new JSONObject();
857 programValuesJson.put(Constants.MAX_SOC_KEY, program.getMaxSoc());
858 programValuesJson.put(Constants.AUTO_UNLOCK_KEY, program.getAutoUnlock());
859 chargeGroupValueStorage.put(Integer.toString(number), programValuesJson);
860 commandOptions.add(new CommandOption(Integer.toString(number), programName));
861 stateOptions.add(new StateOption(Integer.toString(number), programName));
866 ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_CHARGE, OH_CHANNEL_PROGRAM);
867 mmcop.setCommandOptions(cuid, commandOptions);
868 mmsop.setStateOptions(cuid, stateOptions);
869 vas = atts.get(MB_KEY_SELECTED_CHARGE_PROGRAM);
871 selectedChargeProgram = (int) vas.getIntValue();
872 ChargeProgramParameters cpp = cpv.getChargeProgramParameters(selectedChargeProgram);
873 ChannelStateMap programMap = new ChannelStateMap(OH_CHANNEL_PROGRAM, GROUP_CHARGE,
874 DecimalType.valueOf(Integer.toString(selectedChargeProgram)));
875 updateChannel(programMap);
876 ChannelStateMap maxSocMap = new ChannelStateMap(OH_CHANNEL_MAX_SOC, GROUP_CHARGE,
877 QuantityType.valueOf((double) cpp.getMaxSoc(), Units.PERCENT));
878 updateChannel(maxSocMap);
879 ChannelStateMap autoUnlockMap = new ChannelStateMap(OH_CHANNEL_AUTO_UNLOCK, GROUP_CHARGE,
880 OnOffType.from(cpp.getAutoUnlock()));
881 updateChannel(autoUnlockMap);
884 logger.trace("No Charge Program property available for {}", thing.getThingTypeUID());
888 logger.trace("No Charge Programs found");
894 * Check if Websocket shall be kept alive
896 accountHandler.get().keepAlive(ignitionState == 4 || chargingState);
900 * Easy function but there's some measures behind:
901 * Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
902 * project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
903 * line from Location A to B.
904 * I've taken some measurements to calculate the overhead factor based on Google Maps
905 * Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
906 * Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
907 * After measuring more distances you'll find out that the outcome is between 70% and 90%. So
909 * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
912 * @return mapping from air-line distance to "real road" distance
914 public static State guessRangeRadius(State state) {
915 if (state instanceof QuantityType<?> qt) {
916 double radius = qt.intValue() * 0.8;
917 return QuantityType.valueOf(Math.round(radius), qt.getUnit());
919 return QuantityType.valueOf(-1, Units.ONE);
922 protected void updateChannel(ChannelStateMap csm) {
923 String channel = csm.getChannel();
924 ChannelUID cuid = new ChannelUID(thing.getUID(), csm.getGroup(), channel);
925 eventStorage.put(cuid.getId(), csm);
928 * proto updates causing large printouts in openhab.log
929 * only log in case of channel is connected to an item
931 if (OH_CHANNEL_PROTO_UPDATE.equals(csm.getChannel())) {
932 ChannelUID protoUpdateChannelUID = new ChannelUID(thing.getUID(), GROUP_VEHICLE, OH_CHANNEL_PROTO_UPDATE);
933 if (!isLinked(protoUpdateChannelUID)) {
934 eventStorage.put(protoUpdateChannelUID.getId(), csm);
940 * Check correct channel patterns
942 if (csm.hasUomObserver()) {
943 UOMObserver deliveredObserver = csm.getUomObserver();
944 UOMObserver storedObserver = unitStorage.get(channel);
945 boolean change = true;
946 if (storedObserver != null) {
947 change = !storedObserver.equals(deliveredObserver);
949 // Channel adaptions for items with configurable units
950 String pattern = deliveredObserver.getPattern(csm.getGroup(), csm.getChannel());
951 if (pattern != null) {
952 if (pattern.startsWith("%") && change) {
953 mmsop.setStatePattern(cuid, pattern);
955 handleComplexTripPattern(channel, pattern);
958 unitStorage.put(channel, deliveredObserver);
962 * Check if Websocket shall be kept alive during charging or driving
964 if (!UnDefType.UNDEF.equals(csm.getState())) {
965 if (GROUP_VEHICLE.equals(csm.getGroup()) && OH_CHANNEL_IGNITION.equals(csm.getChannel())) {
966 ignitionState = ((DecimalType) csm.getState()).intValue();
967 } else if (GROUP_CHARGE.equals(csm.getGroup()) && OH_CHANNEL_ACTIVE.equals(csm.getChannel())) {
968 chargingState = OnOffType.ON.equals((csm.getState()));
972 if (OH_CHANNEL_ZONE.equals(channel) && !UnDefType.UNDEF.equals(csm.getState())) {
973 activeTemperaturePoint = ((DecimalType) csm.getState()).intValue();
976 updateState(cuid, csm.getState());
979 private void handleComplexTripPattern(String channel, String pattern) {
981 case OH_CHANNEL_CONS_EV:
982 case OH_CHANNEL_CONS_EV_RESET:
983 StringType consumptionUnitEv = StringType.valueOf(pattern);
984 ChannelStateMap csmEv = new ChannelStateMap(OH_CHANNEL_CONS_EV_UNIT, GROUP_TRIP, consumptionUnitEv);
985 updateChannel(csmEv);
987 case OH_CHANNEL_CONS_CONV:
988 case OH_CHANNEL_CONS_CONV_RESET:
989 StringType consumptionUnitFuel = StringType.valueOf(pattern);
990 ChannelStateMap csmFuel = new ChannelStateMap(OH_CHANNEL_CONS_CONV_UNIT, GROUP_TRIP,
991 consumptionUnitFuel);
992 updateChannel(csmFuel);
998 public void updateStatus(ThingStatus ts, ThingStatusDetail tsd, @Nullable String details) {
999 super.updateStatus(ts, tsd, details);
1003 public void updateStatus(ThingStatus ts) {
1004 if (ThingStatus.ONLINE.equals(ts) && !ThingStatus.ONLINE.equals(thing.getStatus())) {
1005 if (accountHandler.isPresent()) {
1006 accountHandler.get().getVehicleCapabilities(config.get().vin);
1009 super.updateStatus(ts);
1012 public void setFeatureCapabilities(@Nullable String capabilities) {
1013 if (capabilities != null) {
1014 ChannelStateMap csm = new ChannelStateMap(MB_KEY_FEATURE_CAPABILITIES, GROUP_VEHICLE,
1015 StringType.valueOf(capabilities));
1020 public void setCommandCapabilities(@Nullable String capabilities) {
1021 if (capabilities != null) {
1022 ChannelStateMap csm = new ChannelStateMap(MB_KEY_COMMAND_CAPABILITIES, GROUP_VEHICLE,
1023 StringType.valueOf(capabilities));
1028 private void setCommandStateOptions() {
1029 List<StateOption> commandTypeOptions = new ArrayList<>();
1030 CommandType[] ctValues = CommandType.values();
1031 for (int i = 0; i < ctValues.length; i++) {
1032 if (!UNRECOGNIZED.equals(ctValues[i].toString())) {
1033 StateOption co = new StateOption(Integer.toString(ctValues[i].getNumber()), ctValues[i].toString());
1034 commandTypeOptions.add(co);
1037 mmsop.setStateOptions(new ChannelUID(thing.getUID(), GROUP_COMMAND, OH_CHANNEL_CMD_NAME), commandTypeOptions);
1038 List<StateOption> commandStateOptions = new ArrayList<>();
1039 CommandState[] csValues = CommandState.values();
1040 for (int j = 0; j < csValues.length; j++) {
1041 if (!UNRECOGNIZED.equals(csValues[j].toString())) {
1042 StateOption so = new StateOption(Integer.toString(csValues[j].getNumber()), csValues[j].toString());
1043 commandStateOptions.add(so);
1046 mmsop.setStateOptions(new ChannelUID(thing.getUID(), GROUP_COMMAND, OH_CHANNEL_CMD_STATE), commandStateOptions);
1053 public Collection<Class<? extends ThingHandlerService>> getServices() {
1054 return Collections.singleton(VehicleActions.class);
1057 public void sendPoi(JSONObject poi) {
1058 accountHandler.get().sendPoi(config.get().vin, poi);