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.livisismarthome.internal.handler;
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;
20 import java.io.IOException;
21 import java.util.HashMap;
22 import java.util.LinkedHashMap;
23 import java.util.List;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.function.Consumer;
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;
59 import ch.qos.logback.classic.Level;
60 import ch.qos.logback.classic.Logger;
63 * @author Sven Strohschein - Initial contribution
66 public class LivisiBridgeHandlerTest {
68 private static final int MAXIMUM_RETRY_EXECUTIONS = 10;
70 private @NonNullByDefault({}) LivisiBridgeHandlerAccessible bridgeHandler;
71 private @NonNullByDefault({}) Bridge bridgeMock;
72 private @NonNullByDefault({}) LivisiWebSocket webSocketMock;
73 private @NonNullByDefault({}) Map<String, State> updatedChannels;
75 private @NonNullByDefault({}) Level previousLoggingLevel;
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
83 updatedChannels = new LinkedHashMap<>();
85 bridgeMock = mock(Bridge.class);
86 when(bridgeMock.getUID()).thenReturn(new ThingUID("livisismarthome", "bridge"));
88 webSocketMock = mock(LivisiWebSocket.class);
90 OAuthClientService oAuthService = mock(OAuthClientService.class);
92 OAuthFactory oAuthFactoryMock = mock(OAuthFactory.class);
93 when(oAuthFactoryMock.createOAuthClientService(any(), any(), any(), any(), any(), any(), any()))
94 .thenReturn(oAuthService);
96 HttpClient httpClientMock = mock(HttpClient.class);
98 bridgeHandler = new LivisiBridgeHandlerAccessible(bridgeMock, oAuthFactoryMock, httpClientMock);
99 bridgeHandler.setCallback(mock(ThingHandlerCallback.class));
103 public void after() {
104 Logger logger = (Logger) LoggerFactory.getLogger(LivisiBridgeHandler.class);
105 logger.setLevel(previousLoggingLevel);
109 public void testInitialize() throws Exception {
110 Configuration bridgeConfig = new Configuration();
112 when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
114 bridgeHandler.initialize();
116 verify(webSocketMock).start();
117 assertEquals(2, bridgeHandler.getDirectExecutionCount());
121 public void testInitializeErrorOnStartingWebSocket() throws Exception {
122 Configuration bridgeConfig = new Configuration();
124 when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
126 doThrow(new WebSocketConnectException("Test-Exception", new IOException())).when(webSocketMock).start();
128 bridgeHandler.initialize();
130 verify(webSocketMock, times(MAXIMUM_RETRY_EXECUTIONS - 1)).start();
131 assertEquals(2, bridgeHandler.getDirectExecutionCount()); // only the first execution should be without a delay
135 public void testConnectionClosed() throws Exception {
136 Configuration bridgeConfig = new Configuration();
138 when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
140 bridgeHandler.initialize();
142 verify(webSocketMock).start();
143 assertEquals(2, bridgeHandler.getDirectExecutionCount());
145 bridgeHandler.connectionClosed();
147 verify(webSocketMock, times(2)).start(); // automatically restarted (with a delay)
148 assertEquals(2, bridgeHandler.getDirectExecutionCount());
150 bridgeHandler.connectionClosed();
152 verify(webSocketMock, times(3)).start(); // automatically restarted (with a delay)
153 assertEquals(2, bridgeHandler.getDirectExecutionCount());
157 public void testConnectionClosedReconnectNotPossible() throws Exception {
158 Configuration bridgeConfig = new Configuration();
160 when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
162 bridgeHandler.initialize();
164 verify(webSocketMock).start();
165 assertEquals(2, bridgeHandler.getDirectExecutionCount());
167 doThrow(new WebSocketConnectException("Connection refused", new IOException())).when(webSocketMock).start();
169 bridgeHandler.connectionClosed();
171 verify(webSocketMock, times(MAXIMUM_RETRY_EXECUTIONS - 1)).start(); // automatic reconnect attempts (with a
173 assertEquals(2, bridgeHandler.getDirectExecutionCount());
177 public void testOnEventDisconnect() throws Exception {
178 final String disconnectEventJSON = "{ type: \"Disconnect\" }";
180 Configuration bridgeConfig = new Configuration();
182 when(bridgeMock.getConfiguration()).thenReturn(bridgeConfig);
184 bridgeHandler.initialize();
186 verify(webSocketMock).start();
187 assertEquals(2, bridgeHandler.getDirectExecutionCount());
189 bridgeHandler.onEvent(disconnectEventJSON);
191 verify(webSocketMock, times(2)).start(); // automatically restarted (with a delay)
192 assertEquals(2, bridgeHandler.getDirectExecutionCount());
194 bridgeHandler.onEvent(disconnectEventJSON);
196 verify(webSocketMock, times(3)).start(); // automatically restarted (with a delay)
197 assertEquals(2, bridgeHandler.getDirectExecutionCount());
201 public void testOnDeviceStateChangedSHCClassic() {
202 DeviceDTO bridgeDevice = createBridgeDevice(true);
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"));
210 DeviceStateDTO deviceState = new DeviceStateDTO();
211 deviceState.setState(state);
212 bridgeDevice.setDeviceState(deviceState);
214 bridgeHandler.onDeviceStateChanged(bridgeDevice);
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")));
223 public void testOnDeviceStateChangedSHCA() {
224 DeviceDTO bridgeDevice = createBridgeDevice(false);
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"));
232 DeviceStateDTO deviceState = new DeviceStateDTO();
233 deviceState.setState(state);
234 bridgeDevice.setDeviceState(deviceState);
236 bridgeHandler.onDeviceStateChanged(bridgeDevice);
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")));
245 public void testOnDeviceStateChangedEventSHCClassic() {
246 DeviceDTO bridgeDevice = createBridgeDevice(true);
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}}
253 EventDTO event = createDeviceEvent(c -> {
254 c.setOSState("Normal");
256 c.setMemoryLoad(67.5);
257 c.setDiskUsage(20.5);
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"));
266 DeviceStateDTO deviceState = new DeviceStateDTO();
267 deviceState.setState(state);
268 bridgeDevice.setDeviceState(deviceState);
270 bridgeHandler.onDeviceStateChanged(bridgeDevice, event);
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")));
279 public void testOnDeviceStateChangedEventSHCA() {
280 DeviceDTO bridgeDevice = createBridgeDevice(false);
282 EventDTO event = createDeviceEvent(c -> {
283 c.setOperationStatus("active");
285 c.setMemoryUsage(67.5);
286 c.setDiskUsage(20.5);
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"));
295 DeviceStateDTO deviceState = new DeviceStateDTO();
296 deviceState.setState(state);
297 bridgeDevice.setDeviceState(deviceState);
299 bridgeHandler.onDeviceStateChanged(bridgeDevice, event);
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")));
307 private static DoubleStateDTO doubleState(double value) {
308 DoubleStateDTO state = new DoubleStateDTO();
309 state.setValue(value);
313 private static StringStateDTO stringState(String value) {
314 StringStateDTO state = new StringStateDTO();
315 state.setValue(value);
319 private static DeviceDTO createBridgeDevice(boolean isSHCClassic) {
320 DeviceDTO device = new DeviceDTO();
322 device.setConfig(new DeviceConfigDTO());
323 device.setCapabilityMap(new HashMap<>());
325 device.setType(DEVICE_SHC);
327 device.setType(DEVICE_SHCA);
332 private boolean isChannelUpdated(String channelUID, State expectedState) {
333 State state = updatedChannels.get(channelUID);
334 return expectedState.equals(state);
337 private static EventDTO createDeviceEvent(Consumer<EventPropertiesDTO> eventPropertiesConsumer) {
338 EventPropertiesDTO eventProperties = new EventPropertiesDTO();
339 eventPropertiesConsumer.accept(eventProperties);
341 EventDTO event = new EventDTO();
342 event.setSource(LINK_TYPE_DEVICE + "deviceId");
343 event.setProperties(eventProperties);
347 private class LivisiBridgeHandlerAccessible extends LivisiBridgeHandler {
349 private final LivisiClient livisiClientMock;
350 private final FullDeviceManager fullDeviceManagerMock;
351 private final ScheduledExecutorService schedulerMock;
352 private int executionCount;
353 private int directExecutionCount;
355 private LivisiBridgeHandlerAccessible(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient)
357 super(bridge, oAuthFactory, httpClient);
359 DeviceDTO bridgeDevice = new DeviceDTO();
360 bridgeDevice.setId("bridgeId");
361 bridgeDevice.setType(DEVICE_SHC);
362 bridgeDevice.setConfig(new DeviceConfigDTO());
364 livisiClientMock = mock(LivisiClient.class);
365 fullDeviceManagerMock = mock(FullDeviceManager.class);
366 when(fullDeviceManagerMock.getFullDevices()).thenReturn(List.of(bridgeDevice));
368 schedulerMock = mock(ScheduledExecutorService.class);
370 doAnswer(invocationOnMock -> {
371 if (executionCount < MAXIMUM_RETRY_EXECUTIONS) {
373 long seconds = invocationOnMock.getArgument(1);
375 directExecutionCount++;
378 invocationOnMock.getArgument(0, Runnable.class).run();
380 return mock(ScheduledFuture.class);
381 }).when(schedulerMock).schedule(any(Runnable.class), anyLong(), any());
384 public int getDirectExecutionCount() {
385 return directExecutionCount;
389 FullDeviceManager createFullDeviceManager(LivisiClient client) {
390 return fullDeviceManagerMock;
394 LivisiClient createClient(OAuthClientService oAuthService) {
395 return livisiClientMock;
400 LivisiWebSocket createAndStartWebSocket(DeviceDTO bridgeDevice) throws WebSocketConnectException {
401 webSocketMock.start();
402 return webSocketMock;
406 ScheduledExecutorService getScheduler() {
407 return schedulerMock;
411 protected void updateState(String channelID, State state) {
412 super.updateState(channelID, state);
413 updatedChannels.put(channelID, state);