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.handler;
15 import static org.junit.jupiter.api.Assertions.assertEquals;
16 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
17 import static org.junit.jupiter.api.Assertions.assertNotNull;
18 import static org.junit.jupiter.api.Assertions.assertTrue;
19 import static org.junit.jupiter.api.Assertions.fail;
20 import static org.mockito.Mockito.mock;
21 import static org.mockito.Mockito.times;
22 import static org.mockito.Mockito.verify;
23 import static org.mockito.Mockito.when;
25 import java.lang.reflect.Field;
26 import java.lang.reflect.Method;
27 import java.time.ZoneId;
28 import java.util.ArrayList;
29 import java.util.List;
31 import java.util.Optional;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.junit.jupiter.api.Test;
36 import org.mockito.ArgumentCaptor;
37 import org.openhab.binding.mybmw.internal.MyBMWConstants.VehicleType;
38 import org.openhab.binding.mybmw.internal.MyBMWVehicleConfiguration;
39 import org.openhab.binding.mybmw.internal.dto.StatusWrapper;
40 import org.openhab.binding.mybmw.internal.dto.vehicle.VehicleStateContainer;
41 import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer;
42 import org.openhab.binding.mybmw.internal.util.FileReader;
43 import org.openhab.binding.mybmw.internal.utils.Constants;
44 import org.openhab.core.i18n.LocationProvider;
45 import org.openhab.core.i18n.TimeZoneProvider;
46 import org.openhab.core.library.types.PointType;
47 import org.openhab.core.library.types.QuantityType;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingUID;
51 import org.openhab.core.thing.binding.ThingHandlerCallback;
52 import org.openhab.core.types.State;
53 import org.openhab.core.types.UnDefType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * The {@link VehicleHandlerTest} is responsible for handling commands, which are
59 * sent to one of the channels.
61 * @author Bernd Weymann - Initial contribution
64 public class VehicleHandlerTest {
65 private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
67 // counters for the number of properties per section
68 private static final int STATUS_ELECTRIC = 12;
69 private static final int STATUS_CONV = 9;
70 private static final int RANGE_HYBRID = 11;
71 private static final int RANGE_CONV = 6;
72 private static final int RANGE_ELECTRIC = 4;
73 private static final int DOORS = 11;
74 private static final int CHECK_EMPTY = 3;
75 private static final int SERVICE_AVAILABLE = 4;
76 private static final int SERVICE_EMPTY = 4;
77 private static final int LOCATION = 4;
78 private static final int CHARGE_PROFILE = 44;
79 private static final int TIRES = 8;
80 public static final PointType HOME_LOCATION = new PointType("54.321,9.876");
82 // I couldn't resolve all NonNull compile errors, hence I'm initializing the values here...
83 ArgumentCaptor<ChannelUID> channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
84 ArgumentCaptor<State> stateCaptor = ArgumentCaptor.forClass(State.class);
85 ThingHandlerCallback thingHandlerCallback = mock(ThingHandlerCallback.class);
86 VehicleHandler vehicleHandler = mock(VehicleHandler.class);
87 List<ChannelUID> allChannels = new ArrayList<>();
88 List<State> allStates = new ArrayList<>();
90 String driveTrain = Constants.EMPTY;
93 * Prepare environment for Vehicle Status Updates
95 private void setup(String type, String vin) {
97 Thing thing = mock(Thing.class);
98 when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test"));
99 MyBMWCommandOptionProvider cop = mock(MyBMWCommandOptionProvider.class);
100 LocationProvider locationProvider = mock(LocationProvider.class);
101 when(locationProvider.getLocation()).thenReturn(HOME_LOCATION);
102 TimeZoneProvider timeZoneProvider = mock(TimeZoneProvider.class);
103 when(timeZoneProvider.getTimeZone()).thenReturn(ZoneId.systemDefault());
104 vehicleHandler = new VehicleHandler(thing, cop, locationProvider, timeZoneProvider, type);
105 MyBMWVehicleConfiguration vehicleConfiguration = new MyBMWVehicleConfiguration();
106 vehicleConfiguration.setVin(vin);
108 setVehicleConfigurationToVehicleHandler(vehicleHandler, vehicleConfiguration);
109 thingHandlerCallback = mock(ThingHandlerCallback.class);
111 vehicleHandler.setCallback(thingHandlerCallback);
112 } catch (Exception e) {
113 logger.error(e.getMessage(), e);
115 channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
116 stateCaptor = ArgumentCaptor.forClass(State.class);
119 private void setVehicleConfigurationToVehicleHandler(@Nullable VehicleHandler vehicleHandler,
120 MyBMWVehicleConfiguration vehicleConfiguration) {
122 Field vehicleConfigurationField = VehicleHandler.class.getDeclaredField("vehicleConfiguration");
123 vehicleConfigurationField.setAccessible(true);
124 vehicleConfigurationField.set(vehicleHandler, Optional.of(vehicleConfiguration));
125 } catch (Exception e) {
126 logger.error("vehicleConfiguration could not be set", e);
127 fail("vehicleConfiguration could not be set", e);
131 private boolean testVehicle(String statusContent, int callbacksExpected,
132 Optional<Map<String, State>> concreteChecks) {
133 assertNotNull(statusContent);
136 Method triggerVehicleStatusUpdateMethod = VehicleHandler.class
137 .getDeclaredMethod("triggerVehicleStatusUpdate", VehicleStateContainer.class, String.class);
138 triggerVehicleStatusUpdateMethod.setAccessible(true);
139 triggerVehicleStatusUpdateMethod.invoke(vehicleHandler,
140 JsonStringDeserializer.getVehicleState(statusContent), null);
141 } catch (Exception e) {
142 logger.error("vehicleState could not be set", e);
143 fail("vehicleState could not be set", e);
146 verify(thingHandlerCallback, times(callbacksExpected)).stateUpdated(channelCaptor.capture(),
147 stateCaptor.capture());
148 allChannels = channelCaptor.getAllValues();
149 allStates = stateCaptor.getAllValues();
151 assertNotNull(driveTrain);
152 StatusWrapper checker = new StatusWrapper(driveTrain, statusContent);
154 if (concreteChecks.isPresent()) {
155 return checker.append(concreteChecks.get()).checkResults(allChannels, allStates);
157 return checker.checkResults(allChannels, allStates);
161 private void trace() {
162 for (int i = 0; i < allChannels.size(); i++) {
163 // change to info for debugging channel updates
164 logger.debug("Channel {} {}", allChannels.get(i), allStates.get(i));
169 public void testPressureConversion() {
171 Method calculatePressureMethod = VehicleHandler.class.getDeclaredMethod("calculatePressure", int.class);
172 calculatePressureMethod.setAccessible(true);
173 State state = (State) calculatePressureMethod.invoke(vehicleHandler, 110);
174 assertInstanceOf(QuantityType.class, state);
175 assertEquals(1.1, ((QuantityType) state).doubleValue());
176 state = (State) calculatePressureMethod.invoke(vehicleHandler, 280);
177 assertEquals(2.8, ((QuantityType) state).doubleValue());
179 state = (State) calculatePressureMethod.invoke(vehicleHandler, -1);
180 assertInstanceOf(UnDefType.class, state);
181 } catch (Exception e) {
182 logger.error("vehicleState could not be set", e);
183 fail("vehicleState could not be set", e);
188 * Test various Vehicles from users which delivered their fingerprint.
189 * The tests are checking the chain from "JSON to Channel update".
190 * Checks are done in an automated way cross checking the data from JSON and data delivered via Channel.
191 * Also important the updates are counted in order to check if code changes are affecting Channel Updates.
193 * With the given output the updated Channels are visible.
197 * Channel testbinding::test:status#lock Locked
198 * Channel testbinding::test:status#service-date 2023-11-01T00:00:00.000+0100
199 * Channel testbinding::test:status#check-control No Issues
200 * Channel testbinding::test:status#last-update 2021-12-21T16:46:02.000+0100
201 * Channel testbinding::test:status#doors Closed
202 * Channel testbinding::test:status#windows Closed
203 * Channel testbinding::test:status#plug-connection Not connected
204 * Channel testbinding::test:status#charge Not Charging
205 * Channel testbinding::test:status#charge-type Not Available
206 * Channel testbinding::test:range#electric 76 km
207 * Channel testbinding::test:range#radius-electric 60.800000000000004 km
208 * Channel testbinding::test:range#fuel 31 km
209 * Channel testbinding::test:range#radius-fuel 24.8 km
210 * Channel testbinding::test:range#hybrid 31 km
211 * Channel testbinding::test:range#radius-hybrid 24.8 km
212 * Channel testbinding::test:range#mileage 31537 km
213 * Channel testbinding::test:range#soc 74 %
214 * Channel testbinding::test:range#remaining-fuel 4 l
215 * Channel testbinding::test:doors#driver-front Closed
216 * Channel testbinding::test:doors#driver-rear Closed
217 * Channel testbinding::test:doors#passenger-front Closed
218 * Channel testbinding::test:doors#passenger-rear Closed
219 * Channel testbinding::test:doors#trunk Closed
220 * Channel testbinding::test:doors#hood Closed
221 * Channel testbinding::test:doors#win-driver-front Closed
222 * Channel testbinding::test:doors#win-driver-rear Undef
223 * Channel testbinding::test:doors#win-passenger-front Closed
224 * Channel testbinding::test:doors#win-passenger-rear Undef
225 * Channel testbinding::test:doors#sunroof Closed
226 * Channel testbinding::test:location#gps 1.2345,6.789
227 * Channel testbinding::test:location#heading 222 °
228 * Channel testbinding::test:service#name Brake Fluid
229 * Channel testbinding::test:service#date 2023-11-01T00:00:00.000+0100
230 * Channel testbinding::test:profile#prefs Chargingwindow
231 * Channel testbinding::test:profile#mode Immediatecharging
232 * Channel testbinding::test:profile#control Weeklyplanner
233 * Channel testbinding::test:profile#target 100
234 * Channel testbinding::test:profile#limit OFF
235 * Channel testbinding::test:profile#climate OFF
236 * Channel testbinding::test:profile#window-start 1970-01-01T11:00:00.000+0100
237 * Channel testbinding::test:profile#window-end 1970-01-01T14:30:00.000+0100
238 * Channel testbinding::test:profile#timer1-departure 1970-01-01T16:00:00.000+0100
239 * Channel testbinding::test:profile#timer1-enabled OFF
240 * Channel testbinding::test:profile#timer1-day-mon ON
241 * Channel testbinding::test:profile#timer1-day-tue ON
242 * Channel testbinding::test:profile#timer1-day-wed ON
243 * Channel testbinding::test:profile#timer1-day-thu ON
244 * Channel testbinding::test:profile#timer1-day-fri ON
245 * Channel testbinding::test:profile#timer1-day-sat ON
246 * Channel testbinding::test:profile#timer1-day-sun ON
247 * Channel testbinding::test:profile#timer2-departure 1970-01-01T12:02:00.000+0100
248 * Channel testbinding::test:profile#timer2-enabled ON
249 * Channel testbinding::test:profile#timer2-day-mon OFF
250 * Channel testbinding::test:profile#timer2-day-tue OFF
251 * Channel testbinding::test:profile#timer2-day-wed OFF
252 * Channel testbinding::test:profile#timer2-day-thu OFF
253 * Channel testbinding::test:profile#timer2-day-fri OFF
254 * Channel testbinding::test:profile#timer2-day-sat OFF
255 * Channel testbinding::test:profile#timer2-day-sun ON
256 * Channel testbinding::test:profile#timer3-departure 1970-01-01T13:03:00.000+0100
257 * Channel testbinding::test:profile#timer3-enabled OFF
258 * Channel testbinding::test:profile#timer3-day-mon OFF
259 * Channel testbinding::test:profile#timer3-day-tue OFF
260 * Channel testbinding::test:profile#timer3-day-wed OFF
261 * Channel testbinding::test:profile#timer3-day-thu OFF
262 * Channel testbinding::test:profile#timer3-day-fri OFF
263 * Channel testbinding::test:profile#timer3-day-sat ON
264 * Channel testbinding::test:profile#timer3-day-sun OFF
265 * Channel testbinding::test:profile#timer4-departure 1970-01-01T12:02:00.000+0100
266 * Channel testbinding::test:profile#timer4-enabled OFF
267 * Channel testbinding::test:profile#timer4-day-mon OFF
268 * Channel testbinding::test:profile#timer4-day-tue OFF
269 * Channel testbinding::test:profile#timer4-day-wed OFF
270 * Channel testbinding::test:profile#timer4-day-thu OFF
271 * Channel testbinding::test:profile#timer4-day-fri OFF
272 * Channel testbinding::test:profile#timer4-day-sat OFF
273 * Channel testbinding::test:profile#timer4-day-sun ON
277 public void testBevIx() {
278 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
279 setup(VehicleType.ELECTRIC.toString(), "anonymous");
280 String content = FileReader.fileToString("responses/BEV/vehicles_state.json");
281 assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY
282 + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
286 public void testBevI3() {
287 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
288 setup(VehicleType.ELECTRIC.toString(), "anonymous");
289 String content = FileReader.fileToString("responses/BEV2/vehicles_state.json");
290 assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY
291 + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
295 public void testBevIX3() {
296 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
297 setup(VehicleType.ELECTRIC.toString(), "anonymous");
298 String content = FileReader.fileToString("responses/BEV3/vehicles_state.json");
299 assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY
300 + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
304 public void testBevI4() {
305 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
306 setup(VehicleType.ELECTRIC.toString(), "anonymous");
307 String content = FileReader.fileToString("responses/BEV4/vehicles_state.json");
308 assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY
309 + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
313 public void testBevI7() {
314 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
315 setup(VehicleType.ELECTRIC.toString(), "anonymous");
316 String content = FileReader.fileToString("responses/BEV5/vehicles_state.json");
317 assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_ELECTRIC + SERVICE_AVAILABLE + CHECK_EMPTY
318 + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
322 public void testIceMiniCooper() {
323 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
324 setup(VehicleType.CONVENTIONAL.toString(), "anonymous");
325 String content = FileReader.fileToString("responses/ICE/vehicles_state.json");
326 assertTrue(testVehicle(content,
327 STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty()));
331 public void testIceX320d() {
332 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
333 setup(VehicleType.CONVENTIONAL.toString(), "anonymous");
334 String content = FileReader.fileToString("responses/ICE2/vehicles_state.json");
335 assertTrue(testVehicle(content,
336 STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty()));
340 public void testIce530d() {
341 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
342 setup(VehicleType.CONVENTIONAL.toString(), "anonymous");
343 String content = FileReader.fileToString("responses/ICE3/vehicles_state.json");
344 assertTrue(testVehicle(content,
345 STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty()));
349 public void testIce435i() {
350 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
351 setup(VehicleType.CONVENTIONAL.toString(), "anonymous");
352 String content = FileReader.fileToString("responses/ICE4/vehicles_state.json");
353 assertTrue(testVehicle(content,
354 STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty()));
358 public void testMildHybrid340i() {
359 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
360 setup(VehicleType.MILD_HYBRID.toString(), "anonymous");
361 String content = FileReader.fileToString("responses/MILD_HYBRID/vehicles_state.json");
362 assertTrue(testVehicle(content,
363 STATUS_CONV + DOORS + RANGE_CONV + LOCATION + SERVICE_EMPTY + CHECK_EMPTY + TIRES, Optional.empty()));
367 public void testPhev530e() {
368 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
369 setup(VehicleType.PLUGIN_HYBRID.toString(), "anonymous");
370 String content = FileReader.fileToString("responses/PHEV/vehicles_state.json");
371 assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY
372 + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));
376 public void testPhev330e() {
377 logger.info("{}", Thread.currentThread().getStackTrace()[1].getMethodName());
378 setup(VehicleType.PLUGIN_HYBRID.toString(), "anonymous");
379 String content = FileReader.fileToString("responses/PHEV2/vehicles_state.json");
380 assertTrue(testVehicle(content, STATUS_ELECTRIC + DOORS + RANGE_HYBRID + SERVICE_AVAILABLE + CHECK_EMPTY
381 + LOCATION + CHARGE_PROFILE + TIRES, Optional.empty()));