2 * Copyright (c) 2010-2022 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.bmwconnecteddrive.internal.dto;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*;
18 import java.util.HashMap;
19 import java.util.List;
22 import javax.measure.Unit;
23 import javax.measure.quantity.Length;
24 import javax.measure.quantity.Time;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.VehicleType;
29 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Doors;
30 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatus;
31 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.VehicleStatusContainer;
32 import org.openhab.binding.bmwconnecteddrive.internal.dto.status.Windows;
33 import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants;
34 import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter;
35 import org.openhab.binding.bmwconnecteddrive.internal.utils.VehicleStatusUtils;
36 import org.openhab.core.library.types.DateTimeType;
37 import org.openhab.core.library.types.PointType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.library.unit.ImperialUnits;
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;
46 import com.google.gson.Gson;
49 * The {@link StatusWrapper} Test json responses from ConnectedDrive Portal
51 * @author Bernd Weymann - Initial contribution
54 @SuppressWarnings("null")
55 public class StatusWrapper {
56 private static final Gson GSON = new Gson();
57 private static final Unit<Length> KILOMETRE = Constants.KILOMETRE_UNIT;
58 private static final double ALLOWED_MILE_CONVERSION_DEVIATION = 1.5;
59 private static final double ALLOWED_KM_ROUND_DEVIATION = 0.1;
61 private VehicleStatus vStatus;
62 private boolean imperial;
63 private boolean isElectric;
64 private boolean hasFuel;
65 private boolean isHybrid;
67 private Map<String, State> specialHandlingMap = new HashMap<String, State>();
69 public StatusWrapper(String type, boolean imperial, String statusJson) {
70 this.imperial = imperial;
71 hasFuel = type.equals(VehicleType.CONVENTIONAL.toString()) || type.equals(VehicleType.PLUGIN_HYBRID.toString())
72 || type.equals(VehicleType.ELECTRIC_REX.toString());
73 isElectric = type.equals(VehicleType.PLUGIN_HYBRID.toString())
74 || type.equals(VehicleType.ELECTRIC_REX.toString()) || type.equals(VehicleType.ELECTRIC.toString());
75 isHybrid = hasFuel && isElectric;
76 VehicleStatusContainer container = GSON.fromJson(statusJson, VehicleStatusContainer.class);
77 assertNotNull(container);
78 assertNotNull(container.vehicleStatus);
79 vStatus = container.vehicleStatus;
83 * Test results auctomatically against json values
89 public boolean checkResults(@Nullable List<ChannelUID> channels, @Nullable List<State> states) {
90 assertNotNull(channels);
91 assertNotNull(states);
92 assertTrue(channels.size() == states.size(), "Same list sizes");
93 for (int i = 0; i < channels.size(); i++) {
94 checkResult(channels.get(i), states.get(i));
100 * Add a specific check for a value e.g. hard coded "Upcoming Service" in order to check the right ordering
105 public StatusWrapper append(Map<String, State> compareMap) {
106 specialHandlingMap.putAll(compareMap);
110 @SuppressWarnings({ "unchecked", "rawtypes" })
111 private void checkResult(ChannelUID channelUID, State state) {
112 String cUid = channelUID.getIdWithoutGroup();
113 String gUid = channelUID.getGroupId();
114 QuantityType<Length> qt;
115 QuantityType<Time> qtt;
122 assertTrue(state instanceof QuantityType);
123 qt = ((QuantityType) state);
125 assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
127 assertEquals(KILOMETRE, qt.getUnit(), "KM");
130 case CHANNEL_GROUP_RANGE:
131 assertEquals(qt.intValue(), vStatus.mileage, "Mileage");
133 case CHANNEL_GROUP_SERVICE:
134 if (vStatus.cbsData.isEmpty()) {
135 assertEquals(qt.intValue(), -1, "Service Mileage");
137 assertEquals(qt.intValue(), vStatus.cbsData.get(0).cbsRemainingMileage, "Service Mileage");
140 case CHANNEL_GROUP_CHECK_CONTROL:
141 if (vStatus.checkControlMessages.isEmpty()) {
142 assertEquals(qt.intValue(), -1, "CheckControl Mileage");
144 assertEquals(qt.intValue(), vStatus.checkControlMessages.get(0).ccmMileage,
145 "CheckControl Mileage");
149 assertFalse(true, "Channel " + channelUID + " " + state + " not found");
154 assertTrue(isElectric, "Is Eelctric");
155 assertTrue(state instanceof QuantityType);
156 qt = ((QuantityType) state);
158 assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
159 assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeElectricMls),
160 ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
162 assertEquals(KILOMETRE, qt.getUnit(), "KM");
163 assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeElectric),
164 ALLOWED_KM_ROUND_DEVIATION, "Mileage");
167 case RANGE_ELECTRIC_MAX:
168 assertTrue(isElectric, "Is Eelctric");
169 assertTrue(state instanceof QuantityType);
170 qt = ((QuantityType) state);
172 assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
173 assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.maxRangeElectricMls),
174 ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
176 assertEquals(KILOMETRE, qt.getUnit(), "KM");
177 assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.maxRangeElectric),
178 ALLOWED_KM_ROUND_DEVIATION, "Mileage");
182 assertTrue(hasFuel, "Has Fuel");
183 if (!(state instanceof UnDefType)) {
184 assertTrue(state instanceof QuantityType);
185 qt = ((QuantityType) state);
187 assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
188 assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeFuelMls),
189 ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
191 assertEquals(KILOMETRE, qt.getUnit(), "KM");
192 assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.remainingRangeFuel),
193 ALLOWED_KM_ROUND_DEVIATION, "Mileage");
198 assertTrue(isHybrid, "Is Hybrid");
199 assertTrue(state instanceof QuantityType);
200 qt = ((QuantityType) state);
202 assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
203 assertEquals(Converter.round(qt.floatValue()),
204 Converter.round(vStatus.remainingRangeElectricMls + vStatus.remainingRangeFuelMls),
205 ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
207 assertEquals(KILOMETRE, qt.getUnit(), "KM");
208 assertEquals(Converter.round(qt.floatValue()),
209 Converter.round(vStatus.remainingRangeElectric + vStatus.remainingRangeFuel),
210 ALLOWED_KM_ROUND_DEVIATION, "Mileage");
213 case RANGE_HYBRID_MAX:
214 assertTrue(isHybrid, "Is Hybrid");
215 assertTrue(state instanceof QuantityType);
216 qt = ((QuantityType) state);
218 assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles");
219 assertEquals(Converter.round(qt.floatValue()),
220 Converter.round(vStatus.maxRangeElectricMls + vStatus.remainingRangeFuelMls),
221 ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage");
223 assertEquals(KILOMETRE, qt.getUnit(), "KM");
224 assertEquals(Converter.round(qt.floatValue()),
225 Converter.round(vStatus.maxRangeElectric + vStatus.remainingRangeFuel),
226 ALLOWED_KM_ROUND_DEVIATION, "Mileage");
230 assertTrue(hasFuel, "Has Fuel");
231 assertTrue(state instanceof QuantityType);
232 qt = ((QuantityType) state);
233 assertEquals(Units.LITRE, qt.getUnit(), "Liter Unit");
234 assertEquals(Converter.round(vStatus.remainingFuel), Converter.round(qt.floatValue()), 0.01,
238 assertTrue(isElectric, "Is Eelctric");
239 assertTrue(state instanceof QuantityType);
240 qt = ((QuantityType) state);
241 assertEquals(Units.PERCENT, qt.getUnit(), "Percent");
242 assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01,
246 assertTrue(isElectric, "Is Eelctric");
247 assertTrue(state instanceof QuantityType);
248 qt = ((QuantityType) state);
249 assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h");
250 assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01,
254 assertTrue(state instanceof StringType);
255 st = (StringType) state;
256 assertEquals(Converter.toTitleCase(vStatus.doorLockState), st.toString(), "Vehicle locked");
259 assertTrue(state instanceof StringType);
260 st = (StringType) state;
261 Doors doorState = GSON.fromJson(GSON.toJson(vStatus), Doors.class);
262 if (doorState != null) {
263 assertEquals(VehicleStatusUtils.checkClosed(doorState), st.toString(), "Doors Closed");
270 assertTrue(state instanceof StringType);
271 st = (StringType) state;
272 Windows windowState = GSON.fromJson(GSON.toJson(vStatus), Windows.class);
273 if (windowState != null) {
274 if (specialHandlingMap.containsKey(WINDOWS)) {
275 assertEquals(specialHandlingMap.get(WINDOWS).toString(), st.toString(), "Windows");
277 assertEquals(VehicleStatusUtils.checkClosed(windowState), st.toString(), "Windows");
285 assertTrue(state instanceof StringType);
286 st = (StringType) state;
287 if (specialHandlingMap.containsKey(CHECK_CONTROL)) {
288 assertEquals(specialHandlingMap.get(CHECK_CONTROL).toString(), st.toString(), "Check Control");
290 assertEquals(Converter.toTitleCase(VehicleStatusUtils.checkControlActive(vStatus)), st.toString(),
295 assertTrue(isElectric, "Is Electric");
296 assertTrue(state instanceof StringType);
297 st = (StringType) state;
298 if (vStatus.chargingStatus.contentEquals(Constants.INVALID)) {
299 assertEquals(Converter.toTitleCase(vStatus.lastChargingEndReason), st.toString(), "Charge Status");
301 assertEquals(Converter.toTitleCase(vStatus.chargingStatus), st.toString(), "Charge Status");
304 case CHARGE_REMAINING:
305 assertTrue(isElectric, "Is Electric");
306 if (vStatus.chargingTimeRemaining == null) {
307 assertTrue(state instanceof UnDefType, "expected UndefType");
309 assertTrue(state instanceof QuantityType);
310 qtt = ((QuantityType) state);
311 assertEquals(qtt.doubleValue(), vStatus.chargingTimeRemaining);
312 assertEquals(Units.MINUTE, qtt.getUnit(), "Minutes");
315 case PLUG_CONNECTION:
316 assertTrue(state instanceof StringType);
317 st = (StringType) state;
318 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.connectionStatus));
319 assertEquals(wanted.toString(), st.toString(), "Plug Connection State");
322 assertTrue(state instanceof DateTimeType);
323 dtt = (DateTimeType) state;
324 DateTimeType expected = DateTimeType
325 .valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus)));
326 assertEquals(expected.toString(), dtt.toString(), "Last Update");
328 case LAST_UPDATE_REASON:
329 assertTrue(state instanceof StringType);
330 st = (StringType) state;
331 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.updateReason));
332 assertEquals(wanted.toString(), st.toString(), "Last Update");
335 assertTrue(state instanceof PointType);
336 pt = (PointType) state;
337 assertNotNull(vStatus.position);
338 assertEquals(vStatus.position.getCoordinates(), pt.toString(), "Coordinates");
341 assertTrue(state instanceof QuantityType);
342 qt = ((QuantityType) state);
343 assertEquals(Units.DEGREE_ANGLE, qt.getUnit(), "Angle Unit");
344 assertNotNull(vStatus.position);
345 assertEquals(vStatus.position.heading, qt.intValue(), 0.01, "Heading");
347 case RANGE_RADIUS_ELECTRIC:
348 assertTrue(state instanceof QuantityType);
349 assertTrue(isElectric);
350 qt = (QuantityType) state;
352 assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeElectricMls), qt.floatValue(), 1,
353 "Range Radius Electric mi");
355 assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeElectric), qt.floatValue(), 0.1,
356 "Range Radius Electric km");
359 case RANGE_RADIUS_ELECTRIC_MAX:
360 assertTrue(state instanceof QuantityType);
361 assertTrue(isElectric);
362 qt = (QuantityType) state;
364 assertEquals(Converter.guessRangeRadius(vStatus.maxRangeElectricMls), qt.floatValue(), 1,
365 "Range Radius Electric mi");
367 assertEquals(Converter.guessRangeRadius(vStatus.maxRangeElectric), qt.floatValue(), 0.1,
368 "Range Radius Electric km");
371 case RANGE_RADIUS_FUEL:
372 assertTrue(state instanceof QuantityType);
374 qt = (QuantityType) state;
376 assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeFuelMls), qt.floatValue(), 1,
377 "Range Radius Fuel mi");
379 assertEquals(Converter.guessRangeRadius(vStatus.remainingRangeFuel), qt.floatValue(), 0.1,
380 "Range Radius Fuel km");
383 case RANGE_RADIUS_HYBRID:
384 assertTrue(state instanceof QuantityType);
385 assertTrue(isHybrid);
386 qt = (QuantityType) state;
389 Converter.guessRangeRadius(
390 vStatus.remainingRangeElectricMls + vStatus.remainingRangeFuelMls),
391 qt.floatValue(), ALLOWED_MILE_CONVERSION_DEVIATION, "Range Radius Hybrid mi");
394 Converter.guessRangeRadius(vStatus.remainingRangeElectric + vStatus.remainingRangeFuel),
395 qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid km");
398 case RANGE_RADIUS_HYBRID_MAX:
399 assertTrue(state instanceof QuantityType);
400 assertTrue(isHybrid);
401 qt = (QuantityType) state;
404 Converter.guessRangeRadius(vStatus.maxRangeElectricMls + vStatus.remainingRangeFuelMls),
405 qt.floatValue(), ALLOWED_MILE_CONVERSION_DEVIATION, "Range Radius Hybrid Max mi");
407 assertEquals(Converter.guessRangeRadius(vStatus.maxRangeElectric + vStatus.remainingRangeFuel),
408 qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid Max km");
411 case DOOR_DRIVER_FRONT:
412 assertTrue(state instanceof StringType);
413 st = (StringType) state;
414 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorDriverFront));
415 assertEquals(wanted.toString(), st.toString(), "Door");
417 case DOOR_DRIVER_REAR:
418 assertTrue(state instanceof StringType);
419 st = (StringType) state;
420 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorDriverRear));
421 assertEquals(wanted.toString(), st.toString(), "Door");
423 case DOOR_PASSENGER_FRONT:
424 assertTrue(state instanceof StringType);
425 st = (StringType) state;
426 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorPassengerFront));
427 assertEquals(wanted.toString(), st.toString(), "Door");
429 case DOOR_PASSENGER_REAR:
430 assertTrue(state instanceof StringType);
431 st = (StringType) state;
432 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.doorPassengerRear));
433 assertEquals(wanted.toString(), st.toString(), "Door");
436 assertTrue(state instanceof StringType);
437 st = (StringType) state;
438 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.trunk));
439 assertEquals(wanted.toString(), st.toString(), "Door");
442 assertTrue(state instanceof StringType);
443 st = (StringType) state;
444 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.hood));
445 assertEquals(wanted.toString(), st.toString(), "Door");
447 case WINDOW_DOOR_DRIVER_FRONT:
448 assertTrue(state instanceof StringType);
449 st = (StringType) state;
450 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowDriverFront));
451 assertEquals(wanted.toString(), st.toString(), "Window");
453 case WINDOW_DOOR_DRIVER_REAR:
454 assertTrue(state instanceof StringType);
455 st = (StringType) state;
456 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowDriverRear));
457 assertEquals(wanted.toString(), st.toString(), "Window");
459 case WINDOW_DOOR_PASSENGER_FRONT:
460 assertTrue(state instanceof StringType);
461 st = (StringType) state;
462 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowPassengerFront));
463 assertEquals(wanted.toString(), st.toString(), "Window");
465 case WINDOW_DOOR_PASSENGER_REAR:
466 assertTrue(state instanceof StringType);
467 st = (StringType) state;
468 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.windowPassengerRear));
469 assertEquals(wanted.toString(), st.toString(), "Window");
472 assertTrue(state instanceof StringType);
473 st = (StringType) state;
474 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.rearWindow));
475 assertEquals(wanted.toString(), st.toString(), "Window");
478 assertTrue(state instanceof StringType);
479 st = (StringType) state;
480 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.sunroof));
481 assertEquals(wanted.toString(), st.toString(), "Window");
484 assertTrue(state instanceof DateTimeType);
485 dtt = (DateTimeType) state;
486 if (gUid.contentEquals(CHANNEL_GROUP_STATUS)) {
487 if (specialHandlingMap.containsKey(SERVICE_DATE)) {
488 assertEquals(specialHandlingMap.get(SERVICE_DATE).toString(), dtt.toString(), "Next Service");
490 String dueDateString = VehicleStatusUtils.getNextServiceDate(vStatus);
491 DateTimeType expectedDTT = DateTimeType.valueOf(Converter.getLocalDateTime(dueDateString));
492 assertEquals(expectedDTT.toString(), dtt.toString(), "Next Service");
494 } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
495 String dueDateString = vStatus.cbsData.get(0).getDueDate();
496 DateTimeType expectedDTT = DateTimeType.valueOf(Converter.getLocalDateTime(dueDateString));
497 assertEquals(expectedDTT.toString(), dtt.toString(), "First Service Date");
500 case SERVICE_MILEAGE:
501 assertTrue(state instanceof QuantityType);
502 qt = ((QuantityType) state);
503 if (gUid.contentEquals(CHANNEL_GROUP_STATUS)) {
505 assertEquals(ImperialUnits.MILE, qt.getUnit(), "Next Service Miles");
506 assertEquals(VehicleStatusUtils.getNextServiceMileage(vStatus), qt.intValue(), "Mileage");
508 assertEquals(KILOMETRE, qt.getUnit(), "Next Service KM");
509 assertEquals(VehicleStatusUtils.getNextServiceMileage(vStatus), qt.intValue(), "Mileage");
511 } else if (gUid.equals(CHANNEL_GROUP_SERVICE)) {
513 assertEquals(ImperialUnits.MILE, qt.getUnit(), "First Service Miles");
514 assertEquals(vStatus.cbsData.get(0).cbsRemainingMileage, qt.intValue(),
515 "First Service Mileage");
517 assertEquals(KILOMETRE, qt.getUnit(), "First Service KM");
518 assertEquals(vStatus.cbsData.get(0).cbsRemainingMileage, qt.intValue(),
519 "First Service Mileage");
524 assertTrue(state instanceof StringType);
525 st = (StringType) state;
527 case CHANNEL_GROUP_SERVICE:
528 wanted = StringType.valueOf(Converter.toTitleCase(Constants.NO_ENTRIES));
529 if (!vStatus.cbsData.isEmpty()) {
530 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.cbsData.get(0).getType()));
532 assertEquals(wanted.toString(), st.toString(), "Service Name");
534 case CHANNEL_GROUP_CHECK_CONTROL:
535 wanted = StringType.valueOf(Constants.NO_ENTRIES);
536 if (!vStatus.checkControlMessages.isEmpty()) {
537 wanted = StringType.valueOf(vStatus.checkControlMessages.get(0).ccmDescriptionShort);
539 assertEquals(wanted.toString(), st.toString(), "CheckControl Name");
542 assertFalse(true, "Channel " + channelUID + " " + state + " not found");
547 assertTrue(state instanceof StringType);
548 st = (StringType) state;
550 case CHANNEL_GROUP_SERVICE:
551 wanted = StringType.valueOf(Converter.toTitleCase(Constants.NO_ENTRIES));
552 if (!vStatus.cbsData.isEmpty()) {
553 wanted = StringType.valueOf(Converter.toTitleCase(vStatus.cbsData.get(0).getDescription()));
555 assertEquals(wanted.toString(), st.toString(), "Service Details");
557 case CHANNEL_GROUP_CHECK_CONTROL:
558 wanted = StringType.valueOf(Constants.NO_ENTRIES);
559 if (!vStatus.checkControlMessages.isEmpty()) {
560 wanted = StringType.valueOf(vStatus.checkControlMessages.get(0).ccmDescriptionLong);
562 assertEquals(wanted.toString(), st.toString(), "CheckControl Details");
565 assertFalse(true, "Channel " + channelUID + " " + state + " not found");
570 assertTrue(state instanceof DateTimeType);
571 dtt = (DateTimeType) state;
573 case CHANNEL_GROUP_SERVICE:
574 String dueDateString = Constants.NULL_DATE;
575 if (!vStatus.cbsData.isEmpty()) {
576 dueDateString = vStatus.cbsData.get(0).getDueDate();
578 DateTimeType expectedDTT = DateTimeType.valueOf(Converter.getLocalDateTime(dueDateString));
579 assertEquals(expectedDTT.toString(), dtt.toString(), "ServiceSate");
582 assertFalse(true, "Channel " + channelUID + " " + state + " not found");
587 // fail in case of unknown update
588 assertFalse(true, "Channel " + channelUID + " " + state + " not found");