]> git.basschouten.com Git - openhab-addons.git/blob
7f52423900d681d502648d2e463ffc0233a18a01
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mybmw.internal.handler;
14
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;
24
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;
30 import java.util.Map;
31 import java.util.Optional;
32
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;
56
57 /**
58  * The {@link VehicleHandlerTest} is responsible for handling commands, which are
59  * sent to one of the channels.
60  *
61  * @author Bernd Weymann - Initial contribution
62  */
63 @NonNullByDefault
64 public class VehicleHandlerTest {
65     private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
66
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");
81
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<>();
89
90     String driveTrain = Constants.EMPTY;
91
92     /**
93      * Prepare environment for Vehicle Status Updates
94      */
95     private void setup(String type, String vin) {
96         driveTrain = type;
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);
107
108         setVehicleConfigurationToVehicleHandler(vehicleHandler, vehicleConfiguration);
109         thingHandlerCallback = mock(ThingHandlerCallback.class);
110         try {
111             vehicleHandler.setCallback(thingHandlerCallback);
112         } catch (Exception e) {
113             logger.error(e.getMessage(), e);
114         }
115         channelCaptor = ArgumentCaptor.forClass(ChannelUID.class);
116         stateCaptor = ArgumentCaptor.forClass(State.class);
117     }
118
119     private void setVehicleConfigurationToVehicleHandler(@Nullable VehicleHandler vehicleHandler,
120             MyBMWVehicleConfiguration vehicleConfiguration) {
121         try {
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);
128         }
129     }
130
131     private boolean testVehicle(String statusContent, int callbacksExpected,
132             Optional<Map<String, State>> concreteChecks) {
133         assertNotNull(statusContent);
134
135         try {
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);
144         }
145
146         verify(thingHandlerCallback, times(callbacksExpected)).stateUpdated(channelCaptor.capture(),
147                 stateCaptor.capture());
148         allChannels = channelCaptor.getAllValues();
149         allStates = stateCaptor.getAllValues();
150
151         assertNotNull(driveTrain);
152         StatusWrapper checker = new StatusWrapper(driveTrain, statusContent);
153         trace();
154         if (concreteChecks.isPresent()) {
155             return checker.append(concreteChecks.get()).checkResults(allChannels, allStates);
156         } else {
157             return checker.checkResults(allChannels, allStates);
158         }
159     }
160
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));
165         }
166     }
167
168     @Test
169     public void testPressureConversion() {
170         try {
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());
178
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);
184         }
185     }
186
187     /**
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.
192      *
193      * With the given output the updated Channels are visible.
194      * Example:
195      *
196      * testi3Rex
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
274      */
275
276     @Test
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()));
283     }
284
285     @Test
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()));
292     }
293
294     @Test
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()));
301     }
302
303     @Test
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()));
310     }
311
312     @Test
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()));
319     }
320
321     @Test
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()));
328     }
329
330     @Test
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()));
337     }
338
339     @Test
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()));
346     }
347
348     @Test
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()));
355     }
356
357     @Test
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()));
364     }
365
366     @Test
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()));
373     }
374
375     @Test
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()));
382     }
383 }