2 * Copyright (c) 2010-2023 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.mybmw.internal.dto;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.openhab.binding.mybmw.internal.MyBMWConstants.*;
18 import java.util.HashMap;
19 import java.util.List;
22 import javax.measure.Unit;
23 import javax.measure.quantity.Length;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType;
28 import org.openhab.binding.mybmw.internal.dto.properties.CBS;
29 import org.openhab.binding.mybmw.internal.dto.vehicle.Vehicle;
30 import org.openhab.binding.mybmw.internal.handler.VehicleTests;
31 import org.openhab.binding.mybmw.internal.utils.Constants;
32 import org.openhab.binding.mybmw.internal.utils.Converter;
33 import org.openhab.binding.mybmw.internal.utils.VehicleStatusUtils;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.PointType;
37 import org.openhab.core.library.types.QuantityType;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.library.unit.ImperialUnits;
40 import org.openhab.core.library.unit.SIUnits;
41 import org.openhab.core.library.unit.Units;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.types.State;
44 import org.openhab.core.types.UnDefType;
47 * The {@link StatusWrapper} tests stored fingerprint responses from BMW API
49 * @author Bernd Weymann - Initial contribution
52 @SuppressWarnings("null")
53 public class StatusWrapper {
54 private static final Unit<Length> KILOMETRE = Constants.KILOMETRE_UNIT;
56 private Vehicle vehicle;
57 private boolean isElectric;
58 private boolean hasFuel;
59 private boolean isHybrid;
61 private Map<String, State> specialHandlingMap = new HashMap<String, State>();
63 public StatusWrapper(String type, String statusJson) {
64 hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
65 || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.MILD_HYBRID.toString());
66 isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
67 || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
68 isHybrid = hasFuel && isElectric;
69 List<Vehicle> vl = Converter.getVehicleList(statusJson);
70 assertEquals(1, vl.size(), "Vehciles found");
71 vehicle = Converter.getConsistentVehcile(vl.get(0));
75 * Test results auctomatically against json values
81 public boolean checkResults(@Nullable List<ChannelUID> channels, @Nullable List<State> states) {
82 assertNotNull(channels);
83 assertNotNull(states);
84 assertTrue(channels.size() == states.size(), "Same list sizes");
85 for (int i = 0; i < channels.size(); i++) {
86 checkResult(channels.get(i), states.get(i));
92 * Add a specific check for a value e.g. hard coded "Upcoming Service" in order to check the right ordering
97 public StatusWrapper append(Map<String, State> compareMap) {
98 specialHandlingMap.putAll(compareMap);
102 @SuppressWarnings({ "unchecked", "rawtypes" })
103 private void checkResult(ChannelUID channelUID, State state) {
104 String cUid = channelUID.getIdWithoutGroup();
105 String gUid = channelUID.getGroupId();
106 QuantityType<Length> qt;
112 Unit<Length> wantedUnit;
116 case CHANNEL_GROUP_RANGE:
117 if (!state.equals(UnDefType.UNDEF)) {
118 assertTrue(state instanceof QuantityType);
119 qt = ((QuantityType) state);
120 if (Constants.KM_JSON.equals(vehicle.status.currentMileage.units)) {
121 assertEquals(KILOMETRE, qt.getUnit(), "KM");
123 assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
125 assertEquals(qt.intValue(), vehicle.status.currentMileage.mileage, "Mileage");
127 assertEquals(Constants.INT_UNDEF, vehicle.status.currentMileage.mileage,
128 "Mileage undefined");
131 case CHANNEL_GROUP_SERVICE:
132 State wantedMileage = QuantityType.valueOf(Constants.INT_UNDEF, Constants.KILOMETRE_UNIT);
133 if (!vehicle.properties.serviceRequired.isEmpty()) {
134 if (vehicle.properties.serviceRequired.get(0).distance != null) {
135 if (vehicle.properties.serviceRequired.get(0).distance.units
136 .equals(Constants.KILOMETERS_JSON)) {
137 wantedMileage = QuantityType.valueOf(
138 vehicle.properties.serviceRequired.get(0).distance.value,
139 Constants.KILOMETRE_UNIT);
141 wantedMileage = QuantityType.valueOf(
142 vehicle.properties.serviceRequired.get(0).distance.value,
147 assertEquals(wantedMileage, state, "Service Mileage");
150 assertFalse(true, "Channel " + channelUID + " " + state + " not found");
155 assertTrue(isElectric, "Is Electric");
156 assertTrue(state instanceof QuantityType);
157 qt = ((QuantityType) state);
158 wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
159 assertEquals(wantedUnit, qt.getUnit());
160 assertEquals(VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle), qt.intValue(),
164 assertTrue(hasFuel, "Has Fuel");
165 assertTrue(state instanceof QuantityType);
166 qt = ((QuantityType) state);
167 wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
168 assertEquals(wantedUnit, qt.getUnit());
169 assertEquals(VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle), qt.intValue(),
173 assertTrue(isHybrid, "Is Hybrid");
174 assertTrue(state instanceof QuantityType);
175 qt = ((QuantityType) state);
176 wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
177 assertEquals(wantedUnit, qt.getUnit());
178 assertEquals(VehicleStatusUtils.getRange(Constants.PHEV, vehicle), qt.intValue(), "Range Combined");
181 assertTrue(hasFuel, "Has Fuel");
182 assertTrue(state instanceof QuantityType);
183 qt = ((QuantityType) state);
184 assertEquals(Units.LITRE, qt.getUnit(), "Liter Unit");
185 assertEquals(vehicle.properties.fuelLevel.value, qt.intValue(), "Fuel Level");
188 assertTrue(isElectric, "Is Ee<lctric");
189 assertTrue(state instanceof QuantityType);
190 qt = ((QuantityType) state);
191 assertEquals(Units.PERCENT, qt.getUnit(), "Percent");
192 assertEquals(vehicle.properties.chargingState.chargePercentage, qt.intValue(), "Charge Level");
195 assertTrue(state instanceof StringType);
196 st = (StringType) state;
197 assertEquals(Converter.getLockState(vehicle.properties.areDoorsLocked), st, "Vehicle locked");
200 assertTrue(state instanceof StringType);
201 st = (StringType) state;
202 assertEquals(Converter.getClosedState(vehicle.properties.areDoorsClosed), st, "Doors Closed");
205 assertTrue(state instanceof StringType);
206 st = (StringType) state;
207 if (specialHandlingMap.containsKey(WINDOWS)) {
208 assertEquals(specialHandlingMap.get(WINDOWS).toString(), st.toString(), "Windows");
210 assertEquals(Converter.getClosedState(vehicle.properties.areWindowsClosed), st, "Windows");
215 assertTrue(state instanceof StringType);
216 st = (StringType) state;
217 if (specialHandlingMap.containsKey(CHECK_CONTROL)) {
218 assertEquals(specialHandlingMap.get(CHECK_CONTROL).toString(), st.toString(), "Check Control");
220 assertEquals(vehicle.status.checkControlMessagesGeneralState, st.toString(), "Check Control");
224 assertTrue(isElectric, "Is Electric");
225 assertTrue(state instanceof StringType);
226 st = (StringType) state;
227 assertEquals(Converter.getLocalTime(VehicleStatusUtils.getChargeInfo(vehicle)), st.toString(),
231 assertTrue(isElectric, "Is Electric");
232 assertTrue(state instanceof StringType);
233 st = (StringType) state;
234 assertEquals(Converter.toTitleCase(VehicleStatusUtils.getChargStatus(vehicle)), st.toString(),
237 case PLUG_CONNECTION:
238 assertTrue(state instanceof StringType);
239 st = (StringType) state;
240 assertEquals(Converter.getConnectionState(vehicle.properties.chargingState.isChargerConnected), st,
241 "Plug Connection State");
244 assertTrue(state instanceof DateTimeType);
245 dtt = (DateTimeType) state;
246 DateTimeType expected = DateTimeType
247 .valueOf(Converter.zonedToLocalDateTime(vehicle.properties.lastUpdatedAt));
248 assertEquals(expected.toString(), dtt.toString(), "Last Update");
251 if (state instanceof PointType) {
252 pt = (PointType) state;
253 assertNotNull(vehicle.properties.vehicleLocation);
255 PointType.valueOf(Double.toString(vehicle.properties.vehicleLocation.coordinates.latitude)
256 + "," + Double.toString(vehicle.properties.vehicleLocation.coordinates.longitude)),
258 } // else no check needed
261 if (state instanceof QuantityType) {
262 qt = ((QuantityType) state);
263 assertEquals(Units.DEGREE_ANGLE, qt.getUnit(), "Angle Unit");
264 assertNotNull(vehicle.properties.vehicleLocation);
265 assertEquals(vehicle.properties.vehicleLocation.heading, qt.intValue(), 0.01, "Heading");
266 } // else no check needed
268 case RANGE_RADIUS_ELECTRIC:
269 assertTrue(state instanceof QuantityType);
270 assertTrue(isElectric);
271 qt = ((QuantityType) state);
272 wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
273 assertEquals(wantedUnit, qt.getUnit());
275 Converter.guessRangeRadius(VehicleStatusUtils.getRange(Constants.UNIT_PRECENT_JSON, vehicle)),
276 qt.intValue(), "Range Radius Electric");
278 case RANGE_RADIUS_FUEL:
279 assertTrue(state instanceof QuantityType);
281 qt = (QuantityType) state;
282 wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
283 assertEquals(wantedUnit, qt.getUnit());
285 Converter.guessRangeRadius(VehicleStatusUtils.getRange(Constants.UNIT_LITER_JSON, vehicle)),
286 qt.intValue(), "Range Radius Fuel");
288 case RANGE_RADIUS_HYBRID:
289 assertTrue(state instanceof QuantityType);
290 assertTrue(isHybrid);
291 qt = (QuantityType) state;
292 wantedUnit = VehicleStatusUtils.getLengthUnit(vehicle.status.fuelIndicators);
293 assertEquals(wantedUnit, qt.getUnit());
294 assertEquals(Converter.guessRangeRadius(VehicleStatusUtils.getRange(Constants.PHEV, vehicle)),
295 qt.intValue(), "Range Radius Combined");
297 case DOOR_DRIVER_FRONT:
298 assertTrue(state instanceof StringType);
299 st = (StringType) state;
301 .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.doors.driverFront));
302 assertEquals(wanted.toString(), st.toString(), "Door");
304 case DOOR_DRIVER_REAR:
305 assertTrue(state instanceof StringType);
306 st = (StringType) state;
307 wanted = StringType.valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.doors.driverRear));
308 assertEquals(wanted.toString(), st.toString(), "Door");
310 case DOOR_PASSENGER_FRONT:
311 assertTrue(state instanceof StringType);
312 st = (StringType) state;
314 .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.doors.passengerFront));
315 assertEquals(wanted.toString(), st.toString(), "Door");
317 case DOOR_PASSENGER_REAR:
318 assertTrue(state instanceof StringType);
319 st = (StringType) state;
321 .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.doors.passengerRear));
322 assertEquals(wanted.toString(), st.toString(), "Door");
325 assertTrue(state instanceof StringType);
326 st = (StringType) state;
327 wanted = StringType.valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.trunk));
328 assertEquals(wanted.toString(), st.toString(), "Door");
331 assertTrue(state instanceof StringType);
332 st = (StringType) state;
333 wanted = StringType.valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.hood));
334 assertEquals(wanted.toString(), st.toString(), "Door");
336 case WINDOW_DOOR_DRIVER_FRONT:
337 assertTrue(state instanceof StringType);
338 st = (StringType) state;
340 .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.windows.driverFront));
341 assertEquals(wanted.toString(), st.toString(), "Window");
343 case WINDOW_DOOR_DRIVER_REAR:
344 assertTrue(state instanceof StringType);
345 st = (StringType) state;
347 .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.windows.driverRear));
348 assertEquals(wanted.toString(), st.toString(), "Window");
350 case WINDOW_DOOR_PASSENGER_FRONT:
351 assertTrue(state instanceof StringType);
352 st = (StringType) state;
354 .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.windows.passengerFront));
355 assertEquals(wanted.toString(), st.toString(), "Window");
357 case WINDOW_DOOR_PASSENGER_REAR:
358 assertTrue(state instanceof StringType);
359 st = (StringType) state;
361 .valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.windows.passengerRear));
362 assertEquals(wanted.toString(), st.toString(), "Window");
365 assertTrue(state instanceof StringType);
366 st = (StringType) state;
367 wanted = StringType.valueOf(Converter.toTitleCase(vehicle.properties.doorsAndWindows.moonroof));
368 assertEquals(wanted.toString(), st.toString(), "Window");
371 if (!state.equals(UnDefType.UNDEF)) {
372 assertTrue(state instanceof DateTimeType);
373 dtt = (DateTimeType) state;
374 if (gUid.contentEquals(CHANNEL_GROUP_STATUS)) {
375 if (specialHandlingMap.containsKey(SERVICE_DATE)) {
376 assertEquals(specialHandlingMap.get(SERVICE_DATE).toString(), dtt.toString(),
379 String dueDateString = VehicleStatusUtils
380 .getNextServiceDate(vehicle.properties.serviceRequired).toString();
381 DateTimeType expectedDTT = DateTimeType.valueOf(dueDateString);
382 assertEquals(expectedDTT.toString(), dtt.toString(), "Next Service");
384 } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
385 String dueDateString = vehicle.properties.serviceRequired.get(0).dateTime;
386 DateTimeType expectedDTT = DateTimeType.valueOf(Converter.zonedToLocalDateTime(dueDateString));
387 assertEquals(expectedDTT.toString(), dtt.toString(), "First Service Date");
391 case SERVICE_MILEAGE:
392 if (!state.equals(UnDefType.UNDEF)) {
393 qt = ((QuantityType) state);
394 if (gUid.contentEquals(CHANNEL_GROUP_STATUS)) {
395 QuantityType<Length> wantedQt = (QuantityType) VehicleStatusUtils
396 .getNextServiceMileage(vehicle.properties.serviceRequired);
397 assertEquals(wantedQt.getUnit(), qt.getUnit(), "Next Service Miles");
398 assertEquals(wantedQt.intValue(), qt.intValue(), "Mileage");
399 } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
400 assertEquals(vehicle.properties.serviceRequired.get(0).distance.units, qt.getUnit(),
401 "First Service Unit");
402 assertEquals(vehicle.properties.serviceRequired.get(0).distance.value, qt.intValue(),
403 "First Service Mileage");
408 assertTrue(state instanceof StringType);
409 st = (StringType) state;
411 case CHANNEL_GROUP_SERVICE:
412 wanted = StringType.valueOf(Constants.NO_ENTRIES);
413 if (!vehicle.properties.serviceRequired.isEmpty()) {
415 .valueOf(Converter.toTitleCase(vehicle.properties.serviceRequired.get(0).type));
417 assertEquals(wanted.toString(), st.toString(), "Service Name");
419 case CHANNEL_GROUP_CHECK_CONTROL:
420 wanted = StringType.valueOf(Constants.NO_ENTRIES);
421 if (!vehicle.status.checkControlMessages.isEmpty()) {
422 wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).title);
424 assertEquals(wanted.toString(), st.toString(), "CheckControl Name");
427 assertFalse(true, "Channel " + channelUID + " " + state + " not found");
432 assertTrue(state instanceof StringType);
433 st = (StringType) state;
435 case CHANNEL_GROUP_SERVICE:
436 wanted = StringType.valueOf(Converter.toTitleCase(Constants.NO_ENTRIES));
437 if (!vehicle.properties.serviceRequired.isEmpty()) {
439 .valueOf(Converter.toTitleCase(vehicle.properties.serviceRequired.get(0).type));
441 assertEquals(wanted.toString(), st.toString(), "Service Details");
443 case CHANNEL_GROUP_CHECK_CONTROL:
444 wanted = StringType.valueOf(Constants.NO_ENTRIES);
445 if (!vehicle.status.checkControlMessages.isEmpty()) {
446 wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).longDescription);
448 assertEquals(wanted.toString(), st.toString(), "CheckControl Details");
451 assertFalse(true, "Channel " + channelUID + " " + state + " not found");
456 assertTrue(state instanceof StringType);
457 st = (StringType) state;
458 wanted = StringType.valueOf(Constants.NO_ENTRIES);
459 if (!vehicle.status.checkControlMessages.isEmpty()) {
460 wanted = StringType.valueOf(vehicle.status.checkControlMessages.get(0).state);
462 assertEquals(wanted.toString(), st.toString(), "CheckControl Details");
465 if (state.equals(UnDefType.UNDEF)) {
466 for (CBS serviceEntry : vehicle.properties.serviceRequired) {
467 assertTrue(serviceEntry.dateTime == null, "No Service Date available");
470 assertTrue(state instanceof DateTimeType);
471 dtt = (DateTimeType) state;
473 case CHANNEL_GROUP_SERVICE:
474 String dueDateString = vehicle.properties.serviceRequired.get(0).dateTime;
475 DateTimeType expectedDTT = DateTimeType
476 .valueOf(Converter.zonedToLocalDateTime(dueDateString));
477 assertEquals(expectedDTT.toString(), dtt.toString(), "ServiceSate");
480 assertFalse(true, "Channel " + channelUID + " " + state + " not found");
485 case FRONT_LEFT_CURRENT:
486 if (vehicle.properties.tires != null) {
487 assertTrue(state instanceof QuantityType);
488 qt = (QuantityType) state;
489 assertEquals(vehicle.properties.tires.frontLeft.status.currentPressure / 100, qt.doubleValue(),
490 "Fron Left Current");
492 assertTrue(state.equals(UnDefType.UNDEF));
495 case FRONT_LEFT_TARGET:
496 if (vehicle.properties.tires != null) {
497 assertTrue(state instanceof QuantityType);
498 qt = (QuantityType) state;
499 assertEquals(vehicle.properties.tires.frontLeft.status.targetPressure / 100, qt.doubleValue(),
500 "Fron Left Current");
502 assertTrue(state.equals(UnDefType.UNDEF));
505 case FRONT_RIGHT_CURRENT:
506 if (vehicle.properties.tires != null) {
507 assertTrue(state instanceof QuantityType);
508 qt = (QuantityType) state;
509 assertEquals(vehicle.properties.tires.frontRight.status.currentPressure / 100, qt.doubleValue(),
510 "Fron Left Current");
512 assertTrue(state.equals(UnDefType.UNDEF));
515 case FRONT_RIGHT_TARGET:
516 if (vehicle.properties.tires != null) {
517 assertTrue(state instanceof QuantityType);
518 qt = (QuantityType) state;
519 assertEquals(vehicle.properties.tires.frontRight.status.targetPressure / 100, qt.doubleValue(),
520 "Fron Left Current");
522 assertTrue(state.equals(UnDefType.UNDEF));
525 case REAR_LEFT_CURRENT:
526 if (vehicle.properties.tires != null) {
527 assertTrue(state instanceof QuantityType);
528 qt = (QuantityType) state;
529 assertEquals(vehicle.properties.tires.rearLeft.status.currentPressure / 100, qt.doubleValue(),
530 "Fron Left Current");
532 assertTrue(state.equals(UnDefType.UNDEF));
535 case REAR_LEFT_TARGET:
536 if (vehicle.properties.tires != null) {
537 assertTrue(state instanceof QuantityType);
538 qt = (QuantityType) state;
539 assertEquals(vehicle.properties.tires.rearLeft.status.targetPressure / 100, qt.doubleValue(),
540 "Fron Left Current");
542 assertTrue(state.equals(UnDefType.UNDEF));
545 case REAR_RIGHT_CURRENT:
546 if (vehicle.properties.tires != null) {
547 assertTrue(state instanceof QuantityType);
548 qt = (QuantityType) state;
549 assertEquals(vehicle.properties.tires.rearRight.status.currentPressure / 100, qt.doubleValue(),
550 "Fron Left Current");
552 assertTrue(state.equals(UnDefType.UNDEF));
555 case REAR_RIGHT_TARGET:
556 if (vehicle.properties.tires != null) {
557 assertTrue(state instanceof QuantityType);
558 qt = (QuantityType) state;
559 assertEquals(vehicle.properties.tires.rearRight.status.targetPressure / 100, qt.doubleValue(),
560 "Fron Left Current");
562 assertTrue(state.equals(UnDefType.UNDEF));
566 assertTrue(state instanceof OnOffType);
567 oot = (OnOffType) state;
568 if (vehicle.properties.inMotion) {
569 assertEquals(oot.toFullString(), OnOffType.ON.toFullString(), "Vehicle Driving");
571 assertEquals(oot.toFullString(), OnOffType.OFF.toFullString(), "Vehicle Stationary");
575 if (state instanceof StringType) {
576 st = (StringType) state;
577 assertEquals(st.toFullString(), vehicle.properties.vehicleLocation.address.formatted,
579 } // else no check needed
582 if (state instanceof QuantityType) {
583 qt = (QuantityType) state;
584 PointType vehicleLocation = PointType
585 .valueOf(Double.toString(vehicle.properties.vehicleLocation.coordinates.latitude) + ","
586 + Double.toString(vehicle.properties.vehicleLocation.coordinates.longitude));
587 int distance = vehicleLocation.distanceFrom(VehicleTests.HOME_LOCATION).intValue();
588 assertEquals(qt.intValue(), distance, "Distance from Home");
589 assertEquals(qt.getUnit(), SIUnits.METRE, "Distance from Home Unit");
590 } // else no check needed
593 // don't assert raw channel
596 if (!gUid.equals(CHANNEL_GROUP_CHARGE_PROFILE)) {
597 // fail in case of unknown update
598 assertFalse(true, "Channel " + channelUID + " " + state + " not found");