]> git.basschouten.com Git - openhab-addons.git/blob
90eccc7e5bbc59d624278b45bd778454a3ea134b
[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.livisismarthome.internal.handler;
14
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.*;
17 import static org.openhab.binding.livisismarthome.internal.LivisiBindingConstants.*;
18 import static org.openhab.binding.livisismarthome.internal.client.api.entity.link.LinkDTO.LINK_TYPE_DEVICE;
19
20 import java.io.IOException;
21 import java.util.HashMap;
22 import java.util.LinkedHashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.function.Consumer;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.junit.jupiter.api.AfterEach;
33 import org.junit.jupiter.api.BeforeEach;
34 import org.junit.jupiter.api.Test;
35 import org.openhab.binding.livisismarthome.internal.LivisiWebSocket;
36 import org.openhab.binding.livisismarthome.internal.client.LivisiClient;
37 import org.openhab.binding.livisismarthome.internal.client.api.entity.device.DeviceConfigDTO;
38 import org.openhab.binding.livisismarthome.internal.client.api.entity.device.DeviceDTO;
39 import org.openhab.binding.livisismarthome.internal.client.api.entity.device.DeviceStateDTO;
40 import org.openhab.binding.livisismarthome.internal.client.api.entity.device.StateDTO;
41 import org.openhab.binding.livisismarthome.internal.client.api.entity.event.EventDTO;
42 import org.openhab.binding.livisismarthome.internal.client.api.entity.event.EventPropertiesDTO;
43 import org.openhab.binding.livisismarthome.internal.client.api.entity.state.DoubleStateDTO;
44 import org.openhab.binding.livisismarthome.internal.client.api.entity.state.StringStateDTO;
45 import org.openhab.binding.livisismarthome.internal.client.exception.WebSocketConnectException;
46 import org.openhab.binding.livisismarthome.internal.manager.FullDeviceManager;
47 import org.openhab.core.auth.client.oauth2.OAuthClientService;
48 import org.openhab.core.auth.client.oauth2.OAuthFactory;
49 import org.openhab.core.config.core.Configuration;
50 import org.openhab.core.library.types.QuantityType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.library.unit.Units;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.ThingUID;
55 import org.openhab.core.thing.binding.ThingHandlerCallback;
56 import org.openhab.core.types.State;
57 import org.slf4j.LoggerFactory;
58
59 import ch.qos.logback.classic.Level;
60 import ch.qos.logback.classic.Logger;
61
62 /**
63  * @author Sven Strohschein - Initial contribution
64  */
65 @NonNullByDefault
66 public class LivisiBridgeHandlerTest {
67
68     private static final int MAXIMUM_RETRY_EXECUTIONS = 10;
69
70     private @NonNullByDefault({}) LivisiBridgeHandlerAccessible bridgeHandler;
71     private @NonNullByDefault({}) Bridge bridgeMock;
72     private @NonNullByDefault({}) LivisiWebSocket webSocketMock;
73     private @NonNullByDefault({}) Map<String, State> updatedChannels;
74
75     private @NonNullByDefault({}) Level previousLoggingLevel;
76
77     @BeforeEach
78     public void before() throws Exception {
79         Logger logger = (Logger) LoggerFactory.getLogger(LivisiBridgeHandler.class);
80         previousLoggingLevel = logger.getLevel();
81         logger.setLevel(Level.OFF); // avoid (test) exception logs which occur within these reconnect tests
82
83         updatedChannels = new LinkedHashMap<>();
84
85         bridgeMock = mock(Bridge.class);
86         when(bridgeMock.getUID()).thenReturn(new ThingUID("livisismarthome", "bridge"));
87
88         webSocketMock = mock(LivisiWebSocket.class);
89
90         OAuthClientService oAuthService = mock(OAuthClientService.class);
91
92         OAuthFactory oAuthFactoryMock = mock(OAuthFactory.class);
93         when(oAuthFactoryMock.createOAuthClientService(any(), any(), any(), any(), any(), any(), any()))
94                 .thenReturn(oAuthService);
95
96         HttpClient httpClientMock = mock(HttpClient.class);
97
98         bridgeHandler = new LivisiBridgeHandlerAccessible(bridgeMock, oAuthFactoryMock, httpClientMock);
99         bridgeHandler.setCallback(mock(ThingHandlerCallback.class));
100     }
101
102     @AfterEach
103     public void after() {
104         Logger logger = (Logger) LoggerFactory.getLogger(LivisiBridgeHandler.class);
105         logger.setLevel(previousLoggingLevel);
106     }
107
108     @Test
109     public void testInitialize() throws Exception {
110         Configuration bridgeConfig = new Configuration();
111
112         when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
113
114         bridgeHandler.initialize();
115
116         verify(webSocketMock).start();
117         assertEquals(2, bridgeHandler.getDirectExecutionCount());
118     }
119
120     @Test
121     public void testInitializeErrorOnStartingWebSocket() throws Exception {
122         Configuration bridgeConfig = new Configuration();
123
124         when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
125
126         doThrow(new WebSocketConnectException("Test-Exception", new IOException())).when(webSocketMock).start();
127
128         bridgeHandler.initialize();
129
130         verify(webSocketMock, times(MAXIMUM_RETRY_EXECUTIONS - 1)).start();
131         assertEquals(2, bridgeHandler.getDirectExecutionCount()); // only the first execution should be without a delay
132     }
133
134     @Test
135     public void testConnectionClosed() throws Exception {
136         Configuration bridgeConfig = new Configuration();
137
138         when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
139
140         bridgeHandler.initialize();
141
142         verify(webSocketMock).start();
143         assertEquals(2, bridgeHandler.getDirectExecutionCount());
144
145         bridgeHandler.connectionClosed();
146
147         verify(webSocketMock, times(2)).start(); // automatically restarted (with a delay)
148         assertEquals(2, bridgeHandler.getDirectExecutionCount());
149
150         bridgeHandler.connectionClosed();
151
152         verify(webSocketMock, times(3)).start(); // automatically restarted (with a delay)
153         assertEquals(2, bridgeHandler.getDirectExecutionCount());
154     }
155
156     @Test
157     public void testConnectionClosedReconnectNotPossible() throws Exception {
158         Configuration bridgeConfig = new Configuration();
159
160         when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
161
162         bridgeHandler.initialize();
163
164         verify(webSocketMock).start();
165         assertEquals(2, bridgeHandler.getDirectExecutionCount());
166
167         doThrow(new WebSocketConnectException("Connection refused", new IOException())).when(webSocketMock).start();
168
169         bridgeHandler.connectionClosed();
170
171         verify(webSocketMock, times(MAXIMUM_RETRY_EXECUTIONS - 1)).start(); // automatic reconnect attempts (with a
172                                                                             // delay)
173         assertEquals(2, bridgeHandler.getDirectExecutionCount());
174     }
175
176     @Test
177     public void testOnEventDisconnect() throws Exception {
178         final String disconnectEventJSON = "{ type: \"Disconnect\" }";
179
180         Configuration bridgeConfig = new Configuration();
181
182         when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
183
184         bridgeHandler.initialize();
185
186         verify(webSocketMock).start();
187         assertEquals(2, bridgeHandler.getDirectExecutionCount());
188
189         bridgeHandler.onEvent(disconnectEventJSON);
190
191         verify(webSocketMock, times(2)).start(); // automatically restarted (with a delay)
192         assertEquals(2, bridgeHandler.getDirectExecutionCount());
193
194         bridgeHandler.onEvent(disconnectEventJSON);
195
196         verify(webSocketMock, times(3)).start(); // automatically restarted (with a delay)
197         assertEquals(2, bridgeHandler.getDirectExecutionCount());
198     }
199
200     @Test
201     public void testOnDeviceStateChangedSHCClassic() {
202         DeviceDTO bridgeDevice = createBridgeDevice(true);
203
204         StateDTO state = new StateDTO();
205         state.setCPULoad(doubleState(30.5));
206         state.setMemoryLoad(doubleState(60.5));
207         state.setDiskUsage(doubleState(70.5));
208         state.setOSState(stringState("Normal"));
209
210         DeviceStateDTO deviceState = new DeviceStateDTO();
211         deviceState.setState(state);
212         bridgeDevice.setDeviceState(deviceState);
213
214         bridgeHandler.onDeviceStateChanged(bridgeDevice);
215
216         assertTrue(isChannelUpdated(CHANNEL_CPU, QuantityType.valueOf(30.5, Units.PERCENT)));
217         assertTrue(isChannelUpdated(CHANNEL_MEMORY, QuantityType.valueOf(60.5, Units.PERCENT)));
218         assertTrue(isChannelUpdated(CHANNEL_DISK, QuantityType.valueOf(70.5, Units.PERCENT)));
219         assertTrue(isChannelUpdated(CHANNEL_OPERATION_STATUS, StringType.valueOf("NORMAL")));
220     }
221
222     @Test
223     public void testOnDeviceStateChangedSHCA() {
224         DeviceDTO bridgeDevice = createBridgeDevice(false);
225
226         StateDTO state = new StateDTO();
227         state.setCpuUsage(doubleState(30.5));
228         state.setMemoryUsage(doubleState(60.5));
229         state.setDiskUsage(doubleState(70.5));
230         state.setOperationStatus(stringState("active"));
231
232         DeviceStateDTO deviceState = new DeviceStateDTO();
233         deviceState.setState(state);
234         bridgeDevice.setDeviceState(deviceState);
235
236         bridgeHandler.onDeviceStateChanged(bridgeDevice);
237
238         assertTrue(isChannelUpdated(CHANNEL_CPU, QuantityType.valueOf(30.5, Units.PERCENT)));
239         assertTrue(isChannelUpdated(CHANNEL_MEMORY, QuantityType.valueOf(60.5, Units.PERCENT)));
240         assertTrue(isChannelUpdated(CHANNEL_DISK, QuantityType.valueOf(70.5, Units.PERCENT)));
241         assertTrue(isChannelUpdated(CHANNEL_OPERATION_STATUS, StringType.valueOf("ACTIVE")));
242     }
243
244     @Test
245     public void testOnDeviceStateChangedEventSHCClassic() {
246         DeviceDTO bridgeDevice = createBridgeDevice(true);
247
248         // Example SHC-Classic-Event
249         // {"sequenceNumber":709,"type":"StateChanged","namespace":"core.RWE","timestamp":"2022-03-17T11:13:20.1100000Z","source":"/device/812ae7233697408378943e5d943a450x","properties":{"updateAvailable":"","lastReboot":"2022-03-17T
250         // 10:10:14.3530000Z","MBusDongleAttached":false,"LBDongleAttached":false,"configVersion":649,"discoveryActive":false,"IPAddress":"192.168.178.12","currentUTCOffset":
251         // 60,"productsHash":"xy","OSState":"Normal","memoryLoad":67,"CPULoad":39,"diskUsage":20}}
252
253         EventDTO event = createDeviceEvent(c -> {
254             c.setOSState("Normal");
255             c.setCPULoad(39.5);
256             c.setMemoryLoad(67.5);
257             c.setDiskUsage(20.5);
258         });
259
260         StateDTO state = new StateDTO();
261         state.setCPULoad(doubleState(30.5));
262         state.setMemoryLoad(doubleState(60.5));
263         state.setDiskUsage(doubleState(70.5));
264         state.setOSState(stringState("active"));
265
266         DeviceStateDTO deviceState = new DeviceStateDTO();
267         deviceState.setState(state);
268         bridgeDevice.setDeviceState(deviceState);
269
270         bridgeHandler.onDeviceStateChanged(bridgeDevice, event);
271
272         assertTrue(isChannelUpdated(CHANNEL_CPU, QuantityType.valueOf(39.5, Units.PERCENT)));
273         assertTrue(isChannelUpdated(CHANNEL_MEMORY, QuantityType.valueOf(67.5, Units.PERCENT)));
274         assertTrue(isChannelUpdated(CHANNEL_DISK, QuantityType.valueOf(20.5, Units.PERCENT)));
275         assertTrue(isChannelUpdated(CHANNEL_OPERATION_STATUS, StringType.valueOf("NORMAL")));
276     }
277
278     @Test
279     public void testOnDeviceStateChangedEventSHCA() {
280         DeviceDTO bridgeDevice = createBridgeDevice(false);
281
282         EventDTO event = createDeviceEvent(c -> {
283             c.setOperationStatus("active");
284             c.setCpuUsage(39.5);
285             c.setMemoryUsage(67.5);
286             c.setDiskUsage(20.5);
287         });
288
289         StateDTO state = new StateDTO();
290         state.setCpuUsage(doubleState(30.5));
291         state.setMemoryUsage(doubleState(60.5));
292         state.setDiskUsage(doubleState(70.5));
293         state.setOperationStatus(stringState("shuttingdown"));
294
295         DeviceStateDTO deviceState = new DeviceStateDTO();
296         deviceState.setState(state);
297         bridgeDevice.setDeviceState(deviceState);
298
299         bridgeHandler.onDeviceStateChanged(bridgeDevice, event);
300
301         assertTrue(isChannelUpdated(CHANNEL_CPU, QuantityType.valueOf(39.5, Units.PERCENT)));
302         assertTrue(isChannelUpdated(CHANNEL_MEMORY, QuantityType.valueOf(67.5, Units.PERCENT)));
303         assertTrue(isChannelUpdated(CHANNEL_DISK, QuantityType.valueOf(20.5, Units.PERCENT)));
304         assertTrue(isChannelUpdated(CHANNEL_OPERATION_STATUS, StringType.valueOf("ACTIVE")));
305     }
306
307     private static DoubleStateDTO doubleState(double value) {
308         DoubleStateDTO state = new DoubleStateDTO();
309         state.setValue(value);
310         return state;
311     }
312
313     private static StringStateDTO stringState(String value) {
314         StringStateDTO state = new StringStateDTO();
315         state.setValue(value);
316         return state;
317     }
318
319     private static DeviceDTO createBridgeDevice(boolean isSHCClassic) {
320         DeviceDTO device = new DeviceDTO();
321         device.setId("id");
322         device.setConfig(new DeviceConfigDTO());
323         device.setCapabilityMap(new HashMap<>());
324         if (isSHCClassic) {
325             device.setType(DEVICE_SHC);
326         } else {
327             device.setType(DEVICE_SHCA);
328         }
329         return device;
330     }
331
332     private boolean isChannelUpdated(String channelUID, State expectedState) {
333         State state = updatedChannels.get(channelUID);
334         return expectedState.equals(state);
335     }
336
337     private static EventDTO createDeviceEvent(Consumer<EventPropertiesDTO> eventPropertiesConsumer) {
338         EventPropertiesDTO eventProperties = new EventPropertiesDTO();
339         eventPropertiesConsumer.accept(eventProperties);
340
341         EventDTO event = new EventDTO();
342         event.setSource(LINK_TYPE_DEVICE + "deviceId");
343         event.setProperties(eventProperties);
344         return event;
345     }
346
347     private class LivisiBridgeHandlerAccessible extends LivisiBridgeHandler {
348
349         private final LivisiClient livisiClientMock;
350         private final FullDeviceManager fullDeviceManagerMock;
351         private final ScheduledExecutorService schedulerMock;
352         private int executionCount;
353         private int directExecutionCount;
354
355         private LivisiBridgeHandlerAccessible(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient)
356                 throws Exception {
357             super(bridge, oAuthFactory, httpClient);
358
359             DeviceDTO bridgeDevice = new DeviceDTO();
360             bridgeDevice.setId("bridgeId");
361             bridgeDevice.setType(DEVICE_SHC);
362             bridgeDevice.setConfig(new DeviceConfigDTO());
363
364             livisiClientMock = mock(LivisiClient.class);
365             fullDeviceManagerMock = mock(FullDeviceManager.class);
366             when(fullDeviceManagerMock.getFullDevices()).thenReturn(List.of(bridgeDevice));
367
368             schedulerMock = mock(ScheduledExecutorService.class);
369
370             doAnswer(invocationOnMock -> {
371                 if (executionCount < MAXIMUM_RETRY_EXECUTIONS) {
372                     executionCount++;
373                     long seconds = invocationOnMock.getArgument(1);
374                     if (seconds <= 0) {
375                         directExecutionCount++;
376                     }
377
378                     invocationOnMock.getArgument(0, Runnable.class).run();
379                 }
380                 return mock(ScheduledFuture.class);
381             }).when(schedulerMock).schedule(any(Runnable.class), anyLong(), any());
382         }
383
384         public int getDirectExecutionCount() {
385             return directExecutionCount;
386         }
387
388         @Override
389         FullDeviceManager createFullDeviceManager(LivisiClient client) {
390             return fullDeviceManagerMock;
391         }
392
393         @Override
394         LivisiClient createClient(OAuthClientService oAuthService) {
395             return livisiClientMock;
396         }
397
398         @Override
399         @Nullable
400         LivisiWebSocket createAndStartWebSocket(DeviceDTO bridgeDevice) throws WebSocketConnectException {
401             webSocketMock.start();
402             return webSocketMock;
403         }
404
405         @Override
406         ScheduledExecutorService getScheduler() {
407             return schedulerMock;
408         }
409
410         @Override
411         protected void updateState(String channelID, State state) {
412             super.updateState(channelID, state);
413             updatedChannels.put(channelID, state);
414         }
415     }
416 }