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.time.ZonedDateTime;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Locale;
26 import java.util.Optional;
27 import java.util.UUID;
29 import javax.measure.Unit;
30 import javax.measure.quantity.Length;
31 import javax.measure.quantity.Temperature;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.json.JSONObject;
36 import org.openhab.binding.mercedesme.internal.Constants;
37 import org.openhab.binding.mercedesme.internal.MercedesMeCommandOptionProvider;
38 import org.openhab.binding.mercedesme.internal.MercedesMeStateOptionProvider;
39 import org.openhab.binding.mercedesme.internal.actions.VehicleActions;
40 import org.openhab.binding.mercedesme.internal.config.VehicleConfiguration;
41 import org.openhab.binding.mercedesme.internal.utils.ChannelStateMap;
42 import org.openhab.binding.mercedesme.internal.utils.Mapper;
43 import org.openhab.binding.mercedesme.internal.utils.UOMObserver;
44 import org.openhab.binding.mercedesme.internal.utils.Utils;
45 import org.openhab.core.i18n.LocationProvider;
46 import org.openhab.core.library.types.DateTimeType;
47 import org.openhab.core.library.types.DecimalType;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.library.types.PointType;
50 import org.openhab.core.library.types.QuantityType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.library.unit.ImperialUnits;
53 import org.openhab.core.library.unit.SIUnits;
54 import org.openhab.core.library.unit.Units;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseThingHandler;
61 import org.openhab.core.thing.binding.BridgeHandler;
62 import org.openhab.core.thing.binding.ThingHandlerService;
63 import org.openhab.core.types.Command;
64 import org.openhab.core.types.CommandOption;
65 import org.openhab.core.types.RefreshType;
66 import org.openhab.core.types.State;
67 import org.openhab.core.types.StateOption;
68 import org.openhab.core.types.UnDefType;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
72 import com.daimler.mbcarkit.proto.Acp.ACP.CommandType;
73 import com.daimler.mbcarkit.proto.Acp.VehicleAPI.CommandState;
74 import com.daimler.mbcarkit.proto.Client.ClientMessage;
75 import com.daimler.mbcarkit.proto.VehicleCommands.AuxheatStart;
76 import com.daimler.mbcarkit.proto.VehicleCommands.AuxheatStop;
77 import com.daimler.mbcarkit.proto.VehicleCommands.ChargeProgramConfigure;
78 import com.daimler.mbcarkit.proto.VehicleCommands.CommandRequest;
79 import com.daimler.mbcarkit.proto.VehicleCommands.DoorsLock;
80 import com.daimler.mbcarkit.proto.VehicleCommands.DoorsUnlock;
81 import com.daimler.mbcarkit.proto.VehicleCommands.EngineStart;
82 import com.daimler.mbcarkit.proto.VehicleCommands.EngineStop;
83 import com.daimler.mbcarkit.proto.VehicleCommands.SigPosStart;
84 import com.daimler.mbcarkit.proto.VehicleCommands.SigPosStart.HornType;
85 import com.daimler.mbcarkit.proto.VehicleCommands.SigPosStart.LightType;
86 import com.daimler.mbcarkit.proto.VehicleCommands.SigPosStart.SigposType;
87 import com.daimler.mbcarkit.proto.VehicleCommands.SunroofClose;
88 import com.daimler.mbcarkit.proto.VehicleCommands.SunroofLift;
89 import com.daimler.mbcarkit.proto.VehicleCommands.SunroofOpen;
90 import com.daimler.mbcarkit.proto.VehicleCommands.TemperatureConfigure;
91 import com.daimler.mbcarkit.proto.VehicleCommands.TemperatureConfigure.TemperaturePoint;
92 import com.daimler.mbcarkit.proto.VehicleCommands.WindowsClose;
93 import com.daimler.mbcarkit.proto.VehicleCommands.WindowsOpen;
94 import com.daimler.mbcarkit.proto.VehicleCommands.WindowsVentilate;
95 import com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningConfigureSeats;
96 import com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningStart;
97 import com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningStop;
98 import com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningType;
99 import com.daimler.mbcarkit.proto.VehicleEvents;
100 import com.daimler.mbcarkit.proto.VehicleEvents.ChargeProgramParameters;
101 import com.daimler.mbcarkit.proto.VehicleEvents.ChargeProgramsValue;
102 import com.daimler.mbcarkit.proto.VehicleEvents.TemperaturePointsValue;
103 import com.daimler.mbcarkit.proto.VehicleEvents.VEPUpdate;
104 import com.daimler.mbcarkit.proto.VehicleEvents.VehicleAttributeStatus;
105 import com.daimler.mbcarkit.proto.Vehicleapi.AppTwinCommandStatus;
106 import com.daimler.mbcarkit.proto.Vehicleapi.AppTwinCommandStatusUpdatesByPID;
107 import com.google.protobuf.BoolValue;
108 import com.google.protobuf.Int32Value;
111 * {@link VehicleHandler} transform data into state updates and handling of vehicle commands
113 * @author Bernd Weymann - Initial contribution
114 * @author Bernd Weymann - Bugfix https://github.com/openhab/openhab-addons/issues/16932
117 public class VehicleHandler extends BaseThingHandler {
118 private static final List<String> HVAC_SEAT_LIST = Arrays
119 .asList(new String[] { GROUP_HVAC + "#" + OH_CHANNEL_FRONT_LEFT, GROUP_HVAC + "#" + OH_CHANNEL_FRONT_RIGHT,
120 GROUP_HVAC + "#" + OH_CHANNEL_REAR_LEFT, GROUP_HVAC + "#" + OH_CHANNEL_REAR_RIGHT });
122 private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
123 private final LocationProvider locationProvider;
124 private final MercedesMeCommandOptionProvider mmcop;
125 private final MercedesMeStateOptionProvider mmsop;
127 private Map<String, UOMObserver> unitStorage = new HashMap<>();
128 private int ignitionState = -1;
129 private boolean chargingState = false;
130 private int selectedChargeProgram = -1;
131 private int activeTemperaturePoint = -1;
132 private Map<Integer, QuantityType<Temperature>> temperaturePointsStorage = new HashMap<>();
133 private JSONObject chargeGroupValueStorage = new JSONObject();
134 private Map<String, State> hvacGroupValueStorage = new HashMap<>();
135 private String vehicleType = NOT_SET;
137 Map<String, ChannelStateMap> eventStorage = new HashMap<>();
138 Optional<AccountHandler> accountHandler = Optional.empty();
139 Optional<VehicleConfiguration> config = Optional.empty();
141 public VehicleHandler(Thing thing, LocationProvider lp, MercedesMeCommandOptionProvider cop,
142 MercedesMeStateOptionProvider sop) {
144 vehicleType = thing.getThingTypeUID().getId();
145 locationProvider = lp;
151 public void initialize() {
152 config = Optional.of(getConfigAs(VehicleConfiguration.class));
153 Bridge bridge = getBridge();
154 if (bridge != null) {
155 updateStatus(ThingStatus.UNKNOWN);
156 BridgeHandler handler = bridge.getHandler();
157 if (handler != null) {
158 setCommandStateOptions();
159 accountHandler = Optional.of((AccountHandler) handler);
160 accountHandler.get().registerVin(config.get().vin, this);
162 throw new IllegalStateException("BridgeHandler is null");
165 String textKey = Constants.STATUS_TEXT_PREFIX + "vehicle" + Constants.STATUS_BRIDGE_MISSING;
166 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, textKey);
170 private boolean supports(final String propertyName) {
171 String supported = thing.getProperties().get(propertyName);
172 return Boolean.TRUE.toString().equals(supported);
175 private ClientMessage createCM(CommandRequest cr) {
176 return ClientMessage.newBuilder().setCommandRequest(cr).build();
180 public void dispose() {
181 accountHandler.get().unregisterVin(config.get().vin);
186 public void handleCommand(ChannelUID channelUID, Command command) {
188 * Commands shall be not that frequent so trace level for identifying problems should be feasible
190 logger.trace("Received command {} {} for {}", command.getClass(), command, channelUID);
192 if (command instanceof RefreshType) {
193 if (MB_KEY_FEATURE_CAPABILITIES.equals(channelUID.getIdWithoutGroup())
194 || MB_KEY_COMMAND_CAPABILITIES.equals(channelUID.getIdWithoutGroup())) {
195 accountHandler.ifPresent(ah -> {
196 ah.getVehicleCapabilities(config.get().vin);
199 // deliver from event storage
200 ChannelStateMap csm = eventStorage.get(channelUID.getId());
205 // ensure unit update
206 unitStorage.remove(channelUID.getIdWithoutGroup());
209 var crBuilder = CommandRequest.newBuilder().setVin(config.get().vin).setRequestId(UUID.randomUUID().toString());
210 String group = channelUID.getGroupId();
211 String channel = channelUID.getIdWithoutGroup();
212 String pin = accountHandler.get().config.get().pin;
214 logger.trace("No command {} found for {}", command, channel);
220 case OH_CHANNEL_IGNITION:
221 if (!supports(MB_KEY_COMMAND_ENGINE_START)) {
222 logger.trace("Engine Start/Stop not supported");
225 int commandValue = ((DecimalType) command).intValue();
226 if (commandValue == 4) {
227 if (Constants.NOT_SET.equals(pin)) {
228 logger.trace("Security PIN missing in Account bridge");
231 EngineStart eStart = EngineStart.newBuilder().setPin(pin).build();
232 CommandRequest cr = crBuilder.setEngineStart(eStart).build();
233 accountHandler.get().sendCommand(createCM(cr));
234 } else if (commandValue == 0) {
235 EngineStop eStop = EngineStop.newBuilder().build();
236 CommandRequest cr = crBuilder.setEngineStop(eStop).build();
237 accountHandler.get().sendCommand(createCM(cr));
240 case OH_CHANNEL_WINDOWS:
241 if (!supports(MB_KEY_COMMAND_WINDOWS_OPEN)) {
242 logger.trace("Windows control not supported");
246 switch (((DecimalType) command).intValue()) {
248 if (Constants.NOT_SET.equals(pin)) {
249 logger.trace("Security PIN missing in Account bridge");
252 WindowsVentilate wv = WindowsVentilate.newBuilder().setPin(pin).build();
253 cr = crBuilder.setWindowsVentilate(wv).build();
254 accountHandler.get().sendCommand(createCM(cr));
257 WindowsClose wc = WindowsClose.newBuilder().build();
258 cr = crBuilder.setWindowsClose(wc).build();
259 accountHandler.get().sendCommand(createCM(cr));
262 if (Constants.NOT_SET.equals(pin)) {
263 logger.trace("Security PIN missing in Account bridge");
266 WindowsOpen wo = WindowsOpen.newBuilder().setPin(pin).build();
267 cr = crBuilder.setWindowsOpen(wo).build();
268 accountHandler.get().sendCommand(createCM(cr));
271 logger.trace("No Windows movement known for {}", command);
275 case OH_CHANNEL_LOCK:
276 if (!supports(MB_KEY_COMMAND_DOORS_LOCK)) {
277 logger.trace("Door Lock not supported");
280 switch (((DecimalType) command).intValue()) {
282 DoorsLock dl = DoorsLock.newBuilder().build();
283 cr = crBuilder.setDoorsLock(dl).build();
284 accountHandler.get().sendCommand(createCM(cr));
287 if (Constants.NOT_SET.equals(pin)) {
288 logger.trace("Security PIN missing in Account bridge");
291 DoorsUnlock du = DoorsUnlock.newBuilder().setPin(pin).build();
292 cr = crBuilder.setDoorsUnlock(du).build();
293 accountHandler.get().sendCommand(createCM(cr));
296 logger.trace("No lock command mapped to {}", command);
301 logger.trace("No command {} found for {}#{}", command, group, channel);
304 break; // vehicle group
307 case OH_CHANNEL_TEMPERATURE:
308 if (!supports(MB_KEY_COMMAND_ZEV_PRECONDITION_CONFIGURE)) {
309 logger.trace("Air Conditioning Temperature Setting not supported");
312 if (command instanceof QuantityType<?> quantityTypeCommand) {
313 QuantityType<?> targetTempCelsius = quantityTypeCommand.toInvertibleUnit(SIUnits.CELSIUS);
314 if (targetTempCelsius == null) {
315 logger.trace("Cannot handle temperature command {}", quantityTypeCommand);
318 TemperatureConfigure tc = TemperatureConfigure.newBuilder()
319 .addTemperaturePoints(
320 TemperaturePoint.newBuilder().setZoneValue(activeTemperaturePoint)
321 .setTemperatureInCelsius(targetTempCelsius.intValue()).build())
323 CommandRequest cr = crBuilder.setTemperatureConfigure(tc).build();
324 accountHandler.get().sendCommand(createCM(cr));
326 logger.trace("Temperature {} shall be QuantityType with degree Celsius or Fahrenheit",
330 case OH_CHANNEL_ACTIVE:
331 if (!supports(MB_KEY_COMMAND_ZEV_PRECONDITIONING_START)) {
332 logger.trace("Air Conditioning not supported");
335 if (OnOffType.ON.equals(command)) {
336 ZEVPreconditioningStart precondStart = ZEVPreconditioningStart.newBuilder()
337 .setType(ZEVPreconditioningType.NOW).build();
338 CommandRequest cr = crBuilder.setZevPreconditioningStart(precondStart).build();
339 accountHandler.get().sendCommand(createCM(cr));
341 ZEVPreconditioningStop precondStop = ZEVPreconditioningStop.newBuilder()
342 .setType(ZEVPreconditioningType.NOW).build();
343 CommandRequest cr = crBuilder.setZevPreconditioningStop(precondStop).build();
344 accountHandler.get().sendCommand(createCM(cr));
347 case OH_CHANNEL_FRONT_LEFT:
348 case OH_CHANNEL_FRONT_RIGHT:
349 case OH_CHANNEL_REAR_LEFT:
350 case OH_CHANNEL_REAR_RIGHT:
351 configureSeats(channelUID, (State) command);
353 case OH_CHANNEL_AUX_HEAT:
354 if (!supports(MB_KEY_FEATURE_AUX_HEAT)) {
355 logger.trace("Auxiliary Heating not supported");
358 if (OnOffType.ON.equals(command)) {
359 AuxheatStart auxHeatStart = AuxheatStart.newBuilder().build();
360 CommandRequest cr = crBuilder.setAuxheatStart(auxHeatStart).build();
361 accountHandler.get().sendCommand(createCM(cr));
363 AuxheatStop auxHeatStop = AuxheatStop.newBuilder().build();
364 CommandRequest cr = crBuilder.setAuxheatStop(auxHeatStop).build();
365 accountHandler.get().sendCommand(createCM(cr));
368 case OH_CHANNEL_ZONE:
369 int zone = ((DecimalType) command).intValue();
370 if (!temperaturePointsStorage.containsKey(zone)) {
371 logger.trace("No Temperature Zone found for {}", command);
374 ChannelStateMap zoneMap = new ChannelStateMap(OH_CHANNEL_ZONE, GROUP_HVAC,
375 (DecimalType) command);
376 updateChannel(zoneMap);
377 QuantityType<Temperature> selectedTemp = temperaturePointsStorage.get(zone);
378 if (selectedTemp != null) {
379 ChannelStateMap tempCSM = new ChannelStateMap(OH_CHANNEL_TEMPERATURE, GROUP_HVAC,
381 updateChannel(tempCSM);
384 logger.trace("No command {} found for {}#{}", command, group, channel);
390 case OH_CHANNEL_SIGNAL:
391 if (!supports(MB_KEY_COMMAND_SIGPOS_START)) {
392 logger.trace("Signal Position not supported");
397 switch (((DecimalType) command).intValue()) {
399 sps = SigPosStart.newBuilder().setSigposType(SigposType.LIGHT_ONLY)
400 .setLightType(LightType.DIPPED_HEAD_LIGHT).setSigposDuration(10).build();
401 cr = crBuilder.setSigposStart(sps).build();
402 accountHandler.get().sendCommand(createCM(cr));
405 sps = SigPosStart.newBuilder().setSigposType(SigposType.HORN_ONLY).setHornRepeat(3)
406 .setHornType(HornType.HORN_LOW_VOLUME).build();
407 cr = crBuilder.setSigposStart(sps).build();
408 accountHandler.get().sendCommand(createCM(cr));
411 logger.trace("No Positioning known for {}", command);
415 logger.trace("No command {} found for {}#{}", command, group, channel);
418 break; // position group
420 if (!supports(MB_KEY_COMMAND_CHARGE_PROGRAM_CONFIGURE)) {
421 logger.trace("Charge Program Configure not supported");
424 int maxSocToSelect = 80;
425 boolean autoUnlockToSelect = false;
426 boolean sendCommand = false;
428 synchronized (chargeGroupValueStorage) {
430 case OH_CHANNEL_PROGRAM:
431 selectedChargeProgram = ((DecimalType) command).intValue();
432 if (chargeGroupValueStorage.has(Integer.toString(selectedChargeProgram))) {
433 maxSocToSelect = chargeGroupValueStorage
434 .getJSONObject(Integer.toString(selectedChargeProgram))
435 .getInt(Constants.MAX_SOC_KEY);
436 autoUnlockToSelect = chargeGroupValueStorage
437 .getJSONObject(Integer.toString(selectedChargeProgram))
438 .getBoolean(Constants.AUTO_UNLOCK_KEY);
439 updateChannel(new ChannelStateMap(OH_CHANNEL_MAX_SOC, GROUP_CHARGE,
440 QuantityType.valueOf(maxSocToSelect, Units.PERCENT)));
441 updateChannel(new ChannelStateMap(OH_CHANNEL_AUTO_UNLOCK, GROUP_CHARGE,
442 OnOffType.from(autoUnlockToSelect)));
445 logger.trace("No charge program found for {}", selectedChargeProgram);
448 case OH_CHANNEL_AUTO_UNLOCK:
449 autoUnlockToSelect = OnOffType.ON.equals(command);
452 case OH_CHANNEL_MAX_SOC:
453 maxSocToSelect = ((QuantityType<?>) command).intValue();
457 logger.trace("No command {} found for {}#{}", command, group, channel);
461 Int32Value maxSocValue = Int32Value.newBuilder().setValue(maxSocToSelect).build();
462 BoolValue autoUnlockValue = BoolValue.newBuilder().setValue(autoUnlockToSelect).build();
463 ChargeProgramConfigure cpc = ChargeProgramConfigure.newBuilder()
464 .setChargeProgramValue(selectedChargeProgram).setMaxSoc(maxSocValue)
465 .setAutoUnlock(autoUnlockValue).build();
466 CommandRequest cr = crBuilder.setChargeProgramConfigure(cpc).build();
467 accountHandler.get().sendCommand(createCM(cr));
470 break; // charge group
473 case OH_CHANNEL_SUNROOF:
474 if (!supports(MB_KEY_COMMAND_SUNROOF_OPEN)) {
475 logger.trace("Sunroof control not supported");
479 switch (((DecimalType) command).intValue()) {
481 SunroofClose sc = SunroofClose.newBuilder().build();
482 cr = crBuilder.setSunroofClose(sc).build();
483 accountHandler.get().sendCommand(createCM(cr));
486 if (Constants.NOT_SET.equals(pin)) {
487 logger.trace("Security PIN missing in Account bridge");
490 SunroofOpen so = SunroofOpen.newBuilder().setPin(pin).build();
491 cr = crBuilder.setSunroofOpen(so).build();
492 accountHandler.get().sendCommand(createCM(cr));
495 if (Constants.NOT_SET.equals(pin)) {
496 logger.trace("Security PIN missing in Account bridge");
498 SunroofLift sl = SunroofLift.newBuilder().setPin(pin).build();
499 cr = crBuilder.setSunroofLift(sl).build();
500 accountHandler.get().sendCommand(createCM(cr));
503 logger.trace("No Sunroof movement known for {}", command);
506 logger.trace("No command {} found for {}#{}", command, group, channel);
509 break; // doors group
510 default: // no group matching
511 logger.trace("No command {} found for {}#{}", command, group, channel);
516 private void configureSeats(ChannelUID channelUID, State command) {
517 String supported = thing.getProperties().get(MB_KEY_COMMAND_ZEV_PRECONDITION_CONFIGURE_SEATS);
518 if (Boolean.FALSE.toString().equals(supported)) {
519 logger.trace("Seat Conditioning not supported");
521 com.daimler.mbcarkit.proto.VehicleCommands.ZEVPreconditioningConfigureSeats.Builder builder = ZEVPreconditioningConfigureSeats
524 HVAC_SEAT_LIST.forEach(seat -> {
525 ChannelStateMap csm = eventStorage.get(seat);
527 if (csm.getState() != UnDefType.UNDEF && !seat.equals(channelUID.getId())) {
528 OnOffType oot = (OnOffType) csm.getState();
530 case GROUP_HVAC + "#" + OH_CHANNEL_FRONT_LEFT:
531 builder.setFrontLeft(OnOffType.ON.equals(oot));
533 case GROUP_HVAC + "#" + OH_CHANNEL_FRONT_RIGHT:
534 builder.setFrontRight(OnOffType.ON.equals(oot));
536 case GROUP_HVAC + "#" + OH_CHANNEL_REAR_LEFT:
537 builder.setRearLeft(OnOffType.ON.equals(oot));
539 case GROUP_HVAC + "#" + OH_CHANNEL_REAR_RIGHT:
540 builder.setRearRight(OnOffType.ON.equals(oot));
546 ZEVPreconditioningConfigureSeats seats = builder.build();
547 CommandRequest cr = CommandRequest.newBuilder().setVin(config.get().vin)
548 .setRequestId(UUID.randomUUID().toString()).setZevPreconditionConfigureSeats(seats).build();
549 ClientMessage cm = ClientMessage.newBuilder().setCommandRequest(cr).build();
550 accountHandler.get().sendCommand(cm);
554 public void distributeCommandStatus(AppTwinCommandStatusUpdatesByPID cmdUpdates) {
555 Map<Long, AppTwinCommandStatus> updates = cmdUpdates.getUpdatesByPidMap();
556 updates.forEach((key, value) -> {
558 // getting type and state may throw Exception
559 int commandType = value.getType().getNumber();
560 int commandState = value.getState().getNumber();
562 ChannelStateMap csmCommand = new ChannelStateMap(OH_CHANNEL_CMD_NAME, GROUP_COMMAND,
563 new DecimalType(commandType));
564 updateChannel(csmCommand);
566 ChannelStateMap csmState = new ChannelStateMap(OH_CHANNEL_CMD_STATE, GROUP_COMMAND,
567 new DecimalType(commandState));
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,
579 updateChannel(csmUpdated);
580 } catch (IllegalArgumentException iae) {
581 logger.trace("Cannot decode command {} {}", value.getAllFields().toString(), iae.getMessage());
582 // silent ignore update
587 public void distributeContent(VEPUpdate data) {
588 updateStatus(ThingStatus.ONLINE);
589 boolean fullUpdate = data.getFullUpdate();
591 * Deliver proto update
593 String newProto = Utils.proto2Json(data, thing.getThingTypeUID());
594 String combinedProto = newProto;
595 ChannelUID protoUpdateChannelUID = new ChannelUID(thing.getUID(), GROUP_VEHICLE, OH_CHANNEL_PROTO_UPDATE);
596 ChannelStateMap oldProtoMap = eventStorage.get(protoUpdateChannelUID.getId());
597 if (oldProtoMap != null) {
598 String oldProto = ((StringType) oldProtoMap.getState()).toFullString();
599 Map<?, ?> combinedMap = Utils.combineMaps(new JSONObject(oldProto).toMap(),
600 new JSONObject(newProto).toMap());
601 combinedProto = (new JSONObject(combinedMap)).toString();
603 // proto updates causing large printouts in openhab.log
604 // update channel in case of user connected this channel with an item
605 ChannelStateMap dataUpdateMap = new ChannelStateMap(OH_CHANNEL_PROTO_UPDATE, GROUP_VEHICLE,
606 StringType.valueOf(combinedProto));
607 updateChannel(dataUpdateMap);
609 Map<String, VehicleAttributeStatus> atts = data.getAttributesMap();
611 * handle "simple" values
613 atts.forEach((key, value) -> {
614 ChannelStateMap csm = Mapper.getChannelStateMap(key, value);
617 * Store some values and UOM Observer
619 if (GROUP_HVAC.equals(csm.getGroup())) {
620 hvacGroupValueStorage.put(csm.getChannel(), csm.getState());
624 * handle some specific channels
626 String channel = csm.getChannel();
627 // handle range channels very specific regarding to vehicle type
628 boolean block = false;
630 case OH_CHANNEL_RANGE_ELECTRIC:
631 if (!Constants.COMBUSTION.equals(vehicleType)) {
632 ChannelStateMap radiusElectric = new ChannelStateMap(OH_CHANNEL_RADIUS_ELECTRIC,
633 GROUP_RANGE, guessRangeRadius(csm.getState()), csm.getUomObserver());
634 updateChannel(radiusElectric);
639 case OH_CHANNEL_RANGE_FUEL:
640 if (!Constants.BEV.equals(vehicleType)) {
641 ChannelStateMap radiusFuel = new ChannelStateMap(OH_CHANNEL_RADIUS_FUEL, GROUP_RANGE,
642 guessRangeRadius(csm.getState()), csm.getUomObserver());
643 updateChannel(radiusFuel);
648 case OH_CHANNEL_RANGE_HYBRID:
649 if (Constants.HYBRID.equals(vehicleType)) {
650 ChannelStateMap radiusHybrid = new ChannelStateMap(OH_CHANNEL_RADIUS_HYBRID, GROUP_RANGE,
651 guessRangeRadius(csm.getState()), csm.getUomObserver());
652 updateChannel(radiusHybrid);
658 if (!Constants.COMBUSTION.equals(vehicleType)) {
659 if (config.get().batteryCapacity > 0) {
660 float socValue = ((QuantityType<?>) csm.getState()).floatValue();
661 float batteryCapacity = config.get().batteryCapacity;
662 float chargedValue = Math.round(socValue * 1000 * batteryCapacity / 1000) / (float) 100;
663 ChannelStateMap charged = new ChannelStateMap(OH_CHANNEL_CHARGED, GROUP_RANGE,
664 QuantityType.valueOf(chargedValue, Units.KILOWATT_HOUR));
665 updateChannel(charged);
666 float unchargedValue = Math.round((100 - socValue) * 1000 * batteryCapacity / 1000)
668 ChannelStateMap uncharged = new ChannelStateMap(OH_CHANNEL_UNCHARGED, GROUP_RANGE,
669 QuantityType.valueOf(unchargedValue, Units.KILOWATT_HOUR));
670 updateChannel(uncharged);
672 ChannelStateMap charged = new ChannelStateMap(OH_CHANNEL_CHARGED, GROUP_RANGE,
673 QuantityType.valueOf(0, Units.KILOWATT_HOUR));
674 updateChannel(charged);
675 ChannelStateMap uncharged = new ChannelStateMap(OH_CHANNEL_UNCHARGED, GROUP_RANGE,
676 QuantityType.valueOf(0, Units.KILOWATT_HOUR));
677 updateChannel(uncharged);
683 case OH_CHANNEL_FUEL_LEVEL:
684 if (!Constants.BEV.equals(vehicleType)) {
685 if (config.get().fuelCapacity > 0) {
686 float fuelLevelValue = ((QuantityType<?>) csm.getState()).floatValue();
687 float fuelCapacity = config.get().fuelCapacity;
688 float litersInTank = Math.round(fuelLevelValue * 1000 * fuelCapacity / 1000)
690 ChannelStateMap tankFilled = new ChannelStateMap(OH_CHANNEL_TANK_REMAIN, GROUP_RANGE,
691 QuantityType.valueOf(litersInTank, Mapper.defaultVolumeUnit));
692 updateChannel(tankFilled);
693 float litersFree = Math.round((100 - fuelLevelValue) * 1000 * fuelCapacity / 1000)
695 ChannelStateMap tankOpen = new ChannelStateMap(OH_CHANNEL_TANK_OPEN, GROUP_RANGE,
696 QuantityType.valueOf(litersFree, Mapper.defaultVolumeUnit));
697 updateChannel(tankOpen);
699 ChannelStateMap tankFilled = new ChannelStateMap(OH_CHANNEL_TANK_REMAIN, GROUP_RANGE,
700 QuantityType.valueOf(0, Mapper.defaultVolumeUnit));
701 updateChannel(tankFilled);
702 ChannelStateMap tankOpen = new ChannelStateMap(OH_CHANNEL_TANK_OPEN, GROUP_RANGE,
703 QuantityType.valueOf(0, Mapper.defaultVolumeUnit));
704 updateChannel(tankOpen);
710 case OH_CHANNEL_COOLANT_FLUID:
711 case OH_CHANNEL_ENGINE:
712 case OH_CHANNEL_GAS_FLAP:
713 if (Constants.BEV.equals(vehicleType)) {
726 if (atts.containsKey(MB_KEY_POSITION_LAT) && atts.containsKey(MB_KEY_POSITION_LONG)) {
727 double lat = Utils.getDouble(atts.get(MB_KEY_POSITION_LAT));
728 double lon = Utils.getDouble(atts.get(MB_KEY_POSITION_LONG));
729 if (lat != -1 && lon != -1) {
730 PointType pt = new PointType(lat + "," + lon);
731 updateChannel(new ChannelStateMap(OH_CHANNEL_GPS, Constants.GROUP_POSITION, pt));
733 // calculate distance to home
734 PointType homePoint = locationProvider.getLocation();
735 Unit<Length> lengthUnit = KILOMETRE_UNIT;
736 if (homePoint != null) {
737 double distance = Utils.distance(homePoint.getLatitude().doubleValue(), lat,
738 homePoint.getLongitude().doubleValue(), lon, 0.0, 0.0);
739 UOMObserver observer = new UOMObserver(UOMObserver.LENGTH_KM_UNIT);
740 if (Locale.US.getCountry().equals(Utils.getCountry())) {
741 observer = new UOMObserver(UOMObserver.LENGTH_MILES_UNIT);
742 lengthUnit = ImperialUnits.MILE;
744 updateChannel(new ChannelStateMap(OH_CHANNEL_HOME_DISTANCE, Constants.GROUP_RANGE,
745 QuantityType.valueOf(distance / 1000, lengthUnit), observer));
747 logger.trace("No home location found");
752 logger.trace("Either Latitude {} or Longitude {} attribute nil", lat, lon);
753 updateChannel(new ChannelStateMap(OH_CHANNEL_GPS, Constants.GROUP_POSITION, UnDefType.UNDEF));
759 * handle temperature point
761 if (atts.containsKey(MB_KEY_TEMPERATURE_POINTS)) {
762 VehicleAttributeStatus hvacTemperaturePointAttribute = atts.get(MB_KEY_TEMPERATURE_POINTS);
763 if (hvacTemperaturePointAttribute != null) {
764 if (hvacTemperaturePointAttribute.hasTemperaturePointsValue()) {
765 TemperaturePointsValue tpValue = hvacTemperaturePointAttribute.getTemperaturePointsValue();
766 if (tpValue.getTemperaturePointsCount() > 0) {
767 List<VehicleEvents.TemperaturePoint> tPointList = tpValue.getTemperaturePointsList();
768 List<CommandOption> commandOptions = new ArrayList<>();
769 List<StateOption> stateOptions = new ArrayList<>();
770 tPointList.forEach(point -> {
771 String zoneName = point.getZone();
772 int zoneNumber = Utils.getZoneNumber(zoneName);
773 Unit<Temperature> temperatureUnit = Mapper.defaultTemperatureUnit;
774 UOMObserver observer = null;
775 if (hvacTemperaturePointAttribute.hasTemperatureUnit()) {
776 observer = new UOMObserver(
777 hvacTemperaturePointAttribute.getTemperatureUnit().toString());
778 Unit<?> observerUnit = observer.getUnit();
779 if (observerUnit != null) {
780 temperatureUnit = observerUnit.asType(Temperature.class);
783 ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_HVAC, OH_CHANNEL_TEMPERATURE);
784 mmcop.setCommandOptions(cuid, Utils.getTemperatureOptions(temperatureUnit));
785 if (zoneNumber > 0) {
786 if (activeTemperaturePoint == -1) {
787 activeTemperaturePoint = zoneNumber;
789 double temperature = point.getTemperature();
790 if (point.getTemperatureDisplayValue() != null) {
791 if (point.getTemperatureDisplayValue().strip().length() > 0) {
793 temperature = Double.valueOf(point.getTemperatureDisplayValue());
794 } catch (NumberFormatException nfe) {
795 logger.trace("Cannot transform Temperature Display Value {} into Double",
796 point.getTemperatureDisplayValue());
800 QuantityType<Temperature> temperatureState = QuantityType.valueOf(temperature,
802 temperaturePointsStorage.put(zoneNumber, temperatureState);
803 if (activeTemperaturePoint == zoneNumber) {
804 ChannelStateMap zoneCSM = new ChannelStateMap(OH_CHANNEL_ZONE, Constants.GROUP_HVAC,
805 new DecimalType(activeTemperaturePoint));
806 updateChannel(zoneCSM);
807 ChannelStateMap tempCSM = new ChannelStateMap(OH_CHANNEL_TEMPERATURE,
808 Constants.GROUP_HVAC, temperatureState, observer);
809 updateChannel(tempCSM);
812 logger.trace("No Integer mapping found for Temperature Zone {}", zoneName);
814 commandOptions.add(new CommandOption(Integer.toString(zoneNumber), zoneName));
815 stateOptions.add(new StateOption(Integer.toString(zoneNumber), zoneName));
817 ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_HVAC, OH_CHANNEL_ZONE);
818 mmcop.setCommandOptions(cuid, commandOptions);
819 mmsop.setStateOptions(cuid, stateOptions);
821 // don't set to undef - maybe partial update
822 logger.trace("No TemperaturePoints found - list empty");
825 // don't set to undef - maybe partial update
826 logger.trace("No TemperaturePointsValue found");
829 // don't set to undef - maybe partial update
830 logger.trace("No TemperaturePoints found");
833 // full update acknowledged - set to undef
835 ChannelStateMap zoneMap = new ChannelStateMap(OH_CHANNEL_ZONE, Constants.GROUP_HVAC, UnDefType.UNDEF);
836 updateChannel(zoneMap);
837 QuantityType<Temperature> tempState = QuantityType.valueOf(-1, Mapper.defaultTemperatureUnit);
838 ChannelStateMap tempMap = new ChannelStateMap(OH_CHANNEL_TEMPERATURE, Constants.GROUP_HVAC, tempState);
839 updateChannel(tempMap);
844 * handle Charge Program
846 if (Constants.BEV.equals(thing.getThingTypeUID().getId())
847 || Constants.HYBRID.equals(thing.getThingTypeUID().getId())) {
848 VehicleAttributeStatus vas = atts.get(MB_KEY_CHARGE_PROGRAMS);
850 ChargeProgramsValue cpv = vas.getChargeProgramsValue();
851 if (cpv.getChargeProgramParametersCount() > 0) {
852 List<ChargeProgramParameters> chargeProgramParameters = cpv.getChargeProgramParametersList();
853 List<CommandOption> commandOptions = new ArrayList<>();
854 List<StateOption> stateOptions = new ArrayList<>();
855 synchronized (chargeGroupValueStorage) {
856 chargeGroupValueStorage.clear();
857 chargeProgramParameters.forEach(program -> {
858 String programName = program.getChargeProgram().name();
859 int number = Utils.getChargeProgramNumber(programName);
861 JSONObject programValuesJson = new JSONObject();
862 programValuesJson.put(Constants.MAX_SOC_KEY, program.getMaxSoc());
863 programValuesJson.put(Constants.AUTO_UNLOCK_KEY, program.getAutoUnlock());
864 chargeGroupValueStorage.put(Integer.toString(number), programValuesJson);
865 commandOptions.add(new CommandOption(Integer.toString(number), programName));
866 stateOptions.add(new StateOption(Integer.toString(number), programName));
871 ChannelUID cuid = new ChannelUID(thing.getUID(), GROUP_CHARGE, OH_CHANNEL_PROGRAM);
872 mmcop.setCommandOptions(cuid, commandOptions);
873 mmsop.setStateOptions(cuid, stateOptions);
874 vas = atts.get(MB_KEY_SELECTED_CHARGE_PROGRAM);
876 selectedChargeProgram = (int) vas.getIntValue();
877 ChargeProgramParameters cpp = cpv.getChargeProgramParameters(selectedChargeProgram);
878 ChannelStateMap programMap = new ChannelStateMap(OH_CHANNEL_PROGRAM, GROUP_CHARGE,
879 DecimalType.valueOf(Integer.toString(selectedChargeProgram)));
880 updateChannel(programMap);
881 ChannelStateMap maxSocMap = new ChannelStateMap(OH_CHANNEL_MAX_SOC, GROUP_CHARGE,
882 QuantityType.valueOf((double) cpp.getMaxSoc(), Units.PERCENT));
883 updateChannel(maxSocMap);
884 ChannelStateMap autoUnlockMap = new ChannelStateMap(OH_CHANNEL_AUTO_UNLOCK, GROUP_CHARGE,
885 OnOffType.from(cpp.getAutoUnlock()));
886 updateChannel(autoUnlockMap);
889 logger.trace("No Charge Program property available for {}", thing.getThingTypeUID());
893 logger.trace("No Charge Programs found");
899 * handle day of charge end
901 ChannelStateMap chargeTimeEndCSM = eventStorage.get(GROUP_CHARGE + "#" + OH_CHANNEL_END_TIME);
902 if (chargeTimeEndCSM != null) {
903 State entTimeState = chargeTimeEndCSM.getState();
904 if (entTimeState instanceof DateTimeType endDateTimeType) {
905 // we've a valid charged end time
906 VehicleAttributeStatus vas = atts.get(MB_KEY_ENDOFCHARGEDAY);
907 if (vas != null && !Utils.isNil(vas)) {
908 // proto weekday starts with MONDAY=0, java ZonedDateTime starts with MONDAY=1
909 long estimatedWeekday = Utils.getInt(vas) + 1;
910 ZonedDateTime storedZdt = endDateTimeType.getZonedDateTime();
911 long storedWeekday = storedZdt.getDayOfWeek().getValue();
912 // check if estimated weekday is smaller than stored
913 // estimation Monday=1 vs. stored Saturday=6 => (7+1)-6=2 days ahead
914 if (estimatedWeekday < storedWeekday) {
915 estimatedWeekday += 7;
917 if (estimatedWeekday != storedWeekday) {
918 DateTimeType adjustedDtt = new DateTimeType(
919 storedZdt.plusDays(estimatedWeekday - storedWeekday));
920 ChannelStateMap adjustedCsm = new ChannelStateMap(OH_CHANNEL_END_TIME, GROUP_CHARGE,
922 updateChannel(adjustedCsm);
929 * Check if Websocket shall be kept alive
931 accountHandler.get().keepAlive(ignitionState == 4 || chargingState);
935 * Easy function but there's some measures behind:
936 * Guessing the range of the Vehicle on Map. If you can drive x kilometers with your Vehicle it's not feasible to
937 * project this x km Radius on Map. The roads to be taken are causing some overhead because they are not a straight
938 * line from Location A to B.
939 * I've taken some measurements to calculate the overhead factor based on Google Maps
940 * Berlin - Dresden: Road Distance: 193 air-line Distance 167 = Factor 87%
941 * Kassel - Frankfurt: Road Distance: 199 air-line Distance 143 = Factor 72%
942 * After measuring more distances you'll find out that the outcome is between 70% and 90%. So
944 * This depends also on the roads of a concrete route but this is only a guess without any Route Navigation behind
947 * @return mapping from air-line distance to "real road" distance
949 public static State guessRangeRadius(State state) {
950 if (state instanceof QuantityType<?> qt) {
951 double radius = qt.intValue() * 0.8;
952 return QuantityType.valueOf(Math.round(radius), qt.getUnit());
954 return QuantityType.valueOf(-1, Units.ONE);
957 protected void updateChannel(ChannelStateMap csm) {
958 String channel = csm.getChannel();
959 ChannelUID cuid = new ChannelUID(thing.getUID(), csm.getGroup(), channel);
960 eventStorage.put(cuid.getId(), csm);
963 * proto updates causing large printouts in openhab.log
964 * only log in case of channel is connected to an item
966 if (OH_CHANNEL_PROTO_UPDATE.equals(csm.getChannel())) {
967 ChannelUID protoUpdateChannelUID = new ChannelUID(thing.getUID(), GROUP_VEHICLE, OH_CHANNEL_PROTO_UPDATE);
968 if (!isLinked(protoUpdateChannelUID)) {
969 eventStorage.put(protoUpdateChannelUID.getId(), csm);
975 * Check correct channel patterns
977 if (csm.hasUomObserver()) {
978 UOMObserver deliveredObserver = csm.getUomObserver();
979 UOMObserver storedObserver = unitStorage.get(channel);
980 boolean change = true;
981 if (storedObserver != null) {
982 change = !storedObserver.equals(deliveredObserver);
984 // Channel adaptions for items with configurable units
985 String pattern = deliveredObserver.getPattern(csm.getGroup(), csm.getChannel());
986 if (pattern != null) {
987 if (pattern.startsWith("%") && change) {
988 mmsop.setStatePattern(cuid, pattern);
990 handleComplexTripPattern(channel, pattern);
993 unitStorage.put(channel, deliveredObserver);
997 * Check if Websocket shall be kept alive during charging or driving
999 if (!UnDefType.UNDEF.equals(csm.getState())) {
1000 if (GROUP_VEHICLE.equals(csm.getGroup()) && OH_CHANNEL_IGNITION.equals(csm.getChannel())) {
1001 ignitionState = ((DecimalType) csm.getState()).intValue();
1002 } else if (GROUP_CHARGE.equals(csm.getGroup()) && OH_CHANNEL_ACTIVE.equals(csm.getChannel())) {
1003 chargingState = OnOffType.ON.equals((csm.getState()));
1007 if (OH_CHANNEL_ZONE.equals(channel) && !UnDefType.UNDEF.equals(csm.getState())) {
1008 activeTemperaturePoint = ((DecimalType) csm.getState()).intValue();
1011 updateState(cuid, csm.getState());
1014 private void handleComplexTripPattern(String channel, String pattern) {
1016 case OH_CHANNEL_CONS_EV:
1017 case OH_CHANNEL_CONS_EV_RESET:
1018 StringType consumptionUnitEv = StringType.valueOf(pattern);
1019 ChannelStateMap csmEv = new ChannelStateMap(OH_CHANNEL_CONS_EV_UNIT, GROUP_TRIP, consumptionUnitEv);
1020 updateChannel(csmEv);
1022 case OH_CHANNEL_CONS_CONV:
1023 case OH_CHANNEL_CONS_CONV_RESET:
1024 StringType consumptionUnitFuel = StringType.valueOf(pattern);
1025 ChannelStateMap csmFuel = new ChannelStateMap(OH_CHANNEL_CONS_CONV_UNIT, GROUP_TRIP,
1026 consumptionUnitFuel);
1027 updateChannel(csmFuel);
1033 public void updateStatus(ThingStatus ts, ThingStatusDetail tsd, @Nullable String details) {
1034 super.updateStatus(ts, tsd, details);
1038 public void updateStatus(ThingStatus ts) {
1039 if (ThingStatus.ONLINE.equals(ts) && !ThingStatus.ONLINE.equals(thing.getStatus())) {
1040 if (accountHandler.isPresent()) {
1041 accountHandler.get().getVehicleCapabilities(config.get().vin);
1044 super.updateStatus(ts);
1047 public void setFeatureCapabilities(@Nullable String capabilities) {
1048 if (capabilities != null) {
1049 ChannelStateMap csm = new ChannelStateMap(MB_KEY_FEATURE_CAPABILITIES, GROUP_VEHICLE,
1050 StringType.valueOf(capabilities));
1055 public void setCommandCapabilities(@Nullable String capabilities) {
1056 if (capabilities != null) {
1057 ChannelStateMap csm = new ChannelStateMap(MB_KEY_COMMAND_CAPABILITIES, GROUP_VEHICLE,
1058 StringType.valueOf(capabilities));
1063 private void setCommandStateOptions() {
1064 List<StateOption> commandTypeOptions = new ArrayList<>();
1065 CommandType[] ctValues = CommandType.values();
1066 for (int i = 0; i < ctValues.length; i++) {
1067 if (!UNRECOGNIZED.equals(ctValues[i].toString())) {
1068 StateOption co = new StateOption(Integer.toString(ctValues[i].getNumber()), ctValues[i].toString());
1069 commandTypeOptions.add(co);
1072 mmsop.setStateOptions(new ChannelUID(thing.getUID(), GROUP_COMMAND, OH_CHANNEL_CMD_NAME), commandTypeOptions);
1073 List<StateOption> commandStateOptions = new ArrayList<>();
1074 CommandState[] csValues = CommandState.values();
1075 for (int j = 0; j < csValues.length; j++) {
1076 if (!UNRECOGNIZED.equals(csValues[j].toString())) {
1077 StateOption so = new StateOption(Integer.toString(csValues[j].getNumber()), csValues[j].toString());
1078 commandStateOptions.add(so);
1081 mmsop.setStateOptions(new ChannelUID(thing.getUID(), GROUP_COMMAND, OH_CHANNEL_CMD_STATE), commandStateOptions);
1088 public Collection<Class<? extends ThingHandlerService>> getServices() {
1089 return Collections.singleton(VehicleActions.class);
1092 public void sendPoi(JSONObject poi) {
1093 accountHandler.get().sendPoi(config.get().vin, poi);