result = RadoneyeDataParser.parseRd200Data(data);
+
+ assertEquals(99, result.get(RadoneyeDataParser.RADON).intValue());
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
index 52786a248253a..f97100d3ea82e 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
@@ -28,7 +28,7 @@
@NonNullByDefault
public class BoschSHCBindingConstants {
- private static final String BINDING_ID = "boschshc";
+ public static final String BINDING_ID = "boschshc";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_SHC = new ThingTypeUID(BINDING_ID, "shc");
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
index d8dab396c25c4..5856316b45c0b 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
@@ -46,6 +46,7 @@
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
+import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -95,8 +96,10 @@ public BridgeHandler(Bridge bridge) {
@Override
public void initialize() {
- logger.debug("Initialize {} Version {}", FrameworkUtil.getBundle(getClass()).getSymbolicName(),
- FrameworkUtil.getBundle(getClass()).getVersion());
+ Bundle bundle = FrameworkUtil.getBundle(getClass());
+ if (bundle != null) {
+ logger.debug("Initialize {} Version {}", bundle.getSymbolicName(), bundle.getVersion());
+ }
// Read configuration
BridgeConfiguration config = getConfigAs(BridgeConfiguration.class);
@@ -190,8 +193,10 @@ private void scheduleInitialAccess(BoschHttpClient httpClient) {
* to check if access if possible
* pairs this Bosch SHC Bridge with the SHC if necessary
* and starts the first log poll.
+ *
+ * This method is package-protected to enable unit testing.
*/
- private void initialAccess(BoschHttpClient httpClient) {
+ /* package */ void initialAccess(BoschHttpClient httpClient) {
logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient);
try {
@@ -482,7 +487,7 @@ public Device getDeviceInfo(String deviceId)
deviceId, errorResponse.statusCode, errorResponse.errorCode));
}
} else {
- return new BoschSHCException(String.format("Request for info for device %s failed with status code %d",
+ return new BoschSHCException(String.format("Request for info of device %s failed with status code %d",
deviceId, statusCode));
}
});
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java
index 1a243671c56dc..6e85cefdf6bcd 100644
--- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java
@@ -12,13 +12,21 @@
*/
package org.openhab.binding.boschshc.internal.devices;
-import static org.mockito.Mockito.verify;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import com.google.gson.JsonElement;
@@ -35,8 +43,20 @@
public abstract class AbstractBatteryPoweredDeviceHandlerTest
extends AbstractBoschSHCDeviceHandlerTest {
+ @BeforeEach
+ @Override
+ public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ super.beforeEach();
+
+ DeviceServiceData deviceServiceData = new DeviceServiceData();
+ deviceServiceData.path = "/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel";
+ deviceServiceData.id = "BatteryLevel";
+ deviceServiceData.deviceId = "hdm:ZigBee:000d6f0004b93361";
+ lenient().when(bridgeHandler.getServiceData(anyString(), anyString())).thenReturn(deviceServiceData);
+ }
+
@Test
- public void testProcessUpdate_BatteryLevel_LowBattery() {
+ public void testProcessUpdateBatteryLevelLowBattery() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
@@ -44,15 +64,13 @@ public void testProcessUpdate_BatteryLevel_LowBattery() {
+ " \"type\":\"LOW_BATTERY\",\n" + " \"category\":\"WARNING\"\n" + " }\n"
+ " ]\n" + " }\n" + "}");
getFixture().processUpdate("BatteryLevel", deviceServiceData);
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
new DecimalType(10));
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
}
@Test
- public void testProcessUpdate_BatteryLevel_CriticalLow() {
+ public void testProcessUpdateBatteryLevelCriticalLow() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
@@ -60,15 +78,13 @@ public void testProcessUpdate_BatteryLevel_CriticalLow() {
+ " \"type\":\"CRITICAL_LOW\",\n" + " \"category\":\"WARNING\"\n"
+ " }\n" + " ]\n" + " }\n" + "}");
getFixture().processUpdate("BatteryLevel", deviceServiceData);
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
new DecimalType(1));
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
}
@Test
- public void testProcessUpdate_BatteryLevel_CriticallyLowBattery() {
+ public void testProcessUpdateBatteryLevelCriticallyLowBattery() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
@@ -76,15 +92,13 @@ public void testProcessUpdate_BatteryLevel_CriticallyLowBattery() {
+ " \"type\":\"CRITICALLY_LOW_BATTERY\",\n" + " \"category\":\"WARNING\"\n"
+ " }\n" + " ]\n" + " }\n" + "}");
getFixture().processUpdate("BatteryLevel", deviceServiceData);
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
new DecimalType(1));
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
}
@Test
- public void testProcessUpdate_BatteryLevel_OK() {
+ public void testProcessUpdateBatteryLevelOK() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\" }");
@@ -97,7 +111,7 @@ public void testProcessUpdate_BatteryLevel_OK() {
}
@Test
- public void testProcessUpdate_BatteryLevel_NotAvailable() {
+ public void testProcessUpdateBatteryLevelNotAvailable() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
@@ -105,9 +119,21 @@ public void testProcessUpdate_BatteryLevel_NotAvailable() {
+ " \"type\":\"NOT_AVAILABLE\",\n" + " \"category\":\"WARNING\"\n"
+ " }\n" + " ]\n" + " }\n" + "}");
getFixture().processUpdate("BatteryLevel", deviceServiceData);
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), UnDefType.UNDEF);
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+ UnDefType.UNDEF);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
+ }
+
+ @Test
+ public void testHandleCommandRefreshBatteryLevelChannel() {
+ getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), RefreshType.REFRESH);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+ new DecimalType(100));
+ }
+
+ @Test
+ public void testHandleCommandRefreshLowBatteryChannel() {
+ getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), RefreshType.REFRESH);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
}
}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java
index 5f104b8219f71..ba11a32b005b9 100644
--- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java
@@ -12,6 +12,7 @@
*/
package org.openhab.binding.boschshc.internal.devices;
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.Configuration;
/**
@@ -21,8 +22,9 @@
*
* @param type of the device handler to be tested
*/
+@NonNullByDefault
public abstract class AbstractBoschSHCDeviceHandlerTest
- extends AbstractSHCHandlerTest {
+ extends AbstractBoschSHCHandlerTest {
@Override
protected Configuration getConfiguration() {
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSHCHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCHandlerTest.java
similarity index 69%
rename from bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSHCHandlerTest.java
rename to bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCHandlerTest.java
index 18fe7ff95c2ff..a3ccf28d93426 100644
--- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSHCHandlerTest.java
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCHandlerTest.java
@@ -15,14 +15,20 @@
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
@@ -38,32 +44,33 @@
*
* @param type of the handler to be tested
*/
+@NonNullByDefault
@ExtendWith(MockitoExtension.class)
-public abstract class AbstractSHCHandlerTest {
+public abstract class AbstractBoschSHCHandlerTest {
private T fixture;
- @Mock
- private Thing thing;
+ private @Mock @NonNullByDefault({}) Thing thing;
+
+ private @Mock @NonNullByDefault({}) Bridge bridge;
- @Mock
- private Bridge bridge;
+ protected @Mock @NonNullByDefault({}) BridgeHandler bridgeHandler;
- @Mock
- private BridgeHandler bridgeHandler;
+ private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
- @Mock
- private ThingHandlerCallback callback;
+ protected AbstractBoschSHCHandlerTest() {
+ this.fixture = createFixture();
+ }
@BeforeEach
- public void beforeEach() {
+ void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
fixture = createFixture();
lenient().when(thing.getUID()).thenReturn(getThingUID());
when(thing.getBridgeUID()).thenReturn(new ThingUID("boschshc", "shc", "myBridgeUID"));
when(callback.getBridge(any())).thenReturn(bridge);
fixture.setCallback(callback);
when(bridge.getHandler()).thenReturn(bridgeHandler);
- when(thing.getConfiguration()).thenReturn(getConfiguration());
+ lenient().when(thing.getConfiguration()).thenReturn(getConfiguration());
fixture.initialize();
}
@@ -80,6 +87,10 @@ protected ThingUID getThingUID() {
protected abstract ThingTypeUID getThingTypeUID();
+ protected ChannelUID getChannelUID(String channelID) {
+ return new ChannelUID(getThingUID(), channelID);
+ }
+
protected Configuration getConfiguration() {
return new Configuration();
}
@@ -88,11 +99,11 @@ protected Thing getThing() {
return thing;
}
- public BridgeHandler getBridgeHandler() {
+ protected BridgeHandler getBridgeHandler() {
return bridgeHandler;
}
- public ThingHandlerCallback getCallback() {
+ protected ThingHandlerCallback getCallback() {
return callback;
}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java
index 1dc632bc6cc48..6ec8f4b40dce7 100644
--- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java
@@ -13,7 +13,7 @@
package org.openhab.binding.boschshc.internal.devices;
import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.concurrent.ExecutionException;
@@ -22,16 +22,19 @@
import javax.measure.quantity.Energy;
import javax.measure.quantity.Power;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.ThingUID;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
@@ -43,30 +46,42 @@
*
* @param type of the handler to be tested
*/
+@NonNullByDefault
public abstract class AbstractPowerSwitchHandlerTest
extends AbstractBoschSHCDeviceHandlerTest {
- @Captor
- private ArgumentCaptor serviceStateCaptor;
+ private @Captor @NonNullByDefault({}) ArgumentCaptor serviceStateCaptor;
- @Captor
- private ArgumentCaptor> powerCaptor;
+ private @Captor @NonNullByDefault({}) ArgumentCaptor> powerCaptor;
- @Captor
- private ArgumentCaptor> energyCaptor;
+ private @Captor @NonNullByDefault({}) ArgumentCaptor> energyCaptor;
+
+ @BeforeEach
+ @Override
+ public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ super.beforeEach();
+
+ PowerSwitchServiceState powerSwitchServiceState = new PowerSwitchServiceState();
+ powerSwitchServiceState.switchState = PowerSwitchState.ON;
+ lenient().when(bridgeHandler.getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
+ .thenReturn(powerSwitchServiceState);
+
+ PowerMeterServiceState powerMeterServiceState = new PowerMeterServiceState();
+ powerMeterServiceState.powerConsumption = 12.34d;
+ powerMeterServiceState.energyConsumption = 56.78d;
+ lenient().when(bridgeHandler.getState(anyString(), eq("PowerMeter"), same(PowerMeterServiceState.class)))
+ .thenReturn(powerMeterServiceState);
+ }
@Test
- public void testHandleCommand()
+ public void testHandleCommandPowerSwitchChannel()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
-
- getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH),
- OnOffType.ON);
+ getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
verify(getBridgeHandler()).putState(eq(getDeviceID()), eq("PowerSwitch"), serviceStateCaptor.capture());
PowerSwitchServiceState state = serviceStateCaptor.getValue();
assertSame(PowerSwitchState.ON, state.switchState);
- getFixture().handleCommand(new ChannelUID(new ThingUID(getThingTypeUID(), "abcdef"),
- BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF);
+ getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF);
verify(getBridgeHandler(), times(2)).putState(eq(getDeviceID()), eq("PowerSwitch"),
serviceStateCaptor.capture());
state = serviceStateCaptor.getValue();
@@ -74,36 +89,54 @@ public void testHandleCommand()
}
@Test
- public void testUpdateChannel_PowerSwitchState() {
+ public void testUpdateChannelPowerSwitchState() {
JsonElement jsonObject = JsonParser
.parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"ON\"\n" + "}");
getFixture().processUpdate("PowerSwitch", jsonObject);
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
jsonObject = JsonParser
.parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"OFF\"\n" + "}");
getFixture().processUpdate("PowerSwitch", jsonObject);
- verify(getCallback()).stateUpdated(
- new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF);
}
@Test
- public void testUpdateChannel_PowerMeterServiceState() {
+ public void testUpdateChannelPowerMeterServiceState() {
JsonElement jsonObject = JsonParser.parseString("{\n" + " \"@type\": \"powerMeterState\",\n"
+ " \"powerConsumption\": \"23\",\n" + " \"energyConsumption\": 42\n" + "}");
getFixture().processUpdate("PowerMeter", jsonObject);
- verify(getCallback()).stateUpdated(
- eq(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
+ verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
powerCaptor.capture());
QuantityType powerValue = powerCaptor.getValue();
assertEquals(23, powerValue.intValue());
- verify(getCallback()).stateUpdated(
- eq(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
+ verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
energyCaptor.capture());
QuantityType energyValue = energyCaptor.getValue();
assertEquals(42, energyValue.intValue());
}
+
+ @Test
+ public void testHandleCommandRefreshPowerSwitchChannel() {
+ getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), RefreshType.REFRESH);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
+ }
+
+ @Test
+ public void testHandleCommandRefreshPowerConsumptionChannel() {
+ getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
+ RefreshType.REFRESH);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
+ new QuantityType(12.34d, Units.WATT));
+ }
+
+ @Test
+ public void testHandleCommandRefreshEnergyConsumptionChannel() {
+ getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
+ RefreshType.REFRESH);
+ verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
+ new QuantityType(56.78d, Units.WATT_HOUR));
+ }
}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSmokeDetectorHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSmokeDetectorHandlerTest.java
index a04452bdf4b15..76898ffaeeb2f 100644
--- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSmokeDetectorHandlerTest.java
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSmokeDetectorHandlerTest.java
@@ -14,8 +14,7 @@
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
@@ -28,9 +27,14 @@
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.smokedetectorcheck.SmokeDetectorCheckState;
import org.openhab.binding.boschshc.internal.services.smokedetectorcheck.dto.SmokeDetectorCheckServiceState;
+import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
@@ -51,7 +55,6 @@ public abstract class AbstractSmokeDetectorHandlerTest fields = ReflectionSupport.findFields(BoschHttpClient.class,
+ f -> f.getName().equalsIgnoreCase("logger"), HierarchyTraversalMode.TOP_DOWN);
+ Field field = fields.iterator().next();
+ field.setAccessible(true);
+ field.set(mockedHttpClient, mockedLogger);
+
+ Request request = mock(Request.class);
+ when(mockedHttpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+ when(request.send()).thenReturn(response);
+ when(response.getStatus()).thenReturn(500);
+ assertFalse(mockedHttpClient.isOnline());
+ }
+
+ @Test
+ void isOnlineMockedResponse() throws InterruptedException, TimeoutException, ExecutionException,
+ IllegalArgumentException, IllegalAccessException {
+ BoschHttpClient mockedHttpClient = mock(BoschHttpClient.class);
+ when(mockedHttpClient.isOnline()).thenCallRealMethod();
+ when(mockedHttpClient.getPublicInformationUrl()).thenCallRealMethod();
+
+ // mock a logger using reflection to avoid NPEs during logger calls
+ Logger mockedLogger = mock(Logger.class);
+ List fields = ReflectionSupport.findFields(BoschHttpClient.class,
+ f -> f.getName().equalsIgnoreCase("logger"), HierarchyTraversalMode.TOP_DOWN);
+ Field field = fields.iterator().next();
+ field.setAccessible(true);
+ field.set(mockedHttpClient, mockedLogger);
+
+ Request request = mock(Request.class);
+ when(mockedHttpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+ when(request.send()).thenReturn(response);
+ when(response.getStatus()).thenReturn(200);
+ when(response.getContentAsString()).thenReturn("response");
+ assertTrue(mockedHttpClient.isOnline());
+ }
+
@Test
void doPairing() throws InterruptedException {
assertFalse(httpClient.doPairing());
@@ -104,16 +164,103 @@ void createRequest() {
@Test
void createRequestWithObject() {
- Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET, "someData");
+ BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
+ binarySwitchState.on = true;
+ Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET, binarySwitchState);
assertNotNull(request);
+ assertEquals("{\"on\":true,\"stateType\":\"binarySwitchState\",\"@type\":\"binarySwitchState\"}",
+ StandardCharsets.UTF_8.decode(request.getContent().iterator().next()).toString());
}
@Test
- void sendRequest() {
- Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET);
- // Null pointer exception is expected, because localhost will not answer request
- assertThrows(NullPointerException.class, () -> {
- httpClient.sendRequest(request, SubscribeResult.class, SubscribeResult::isValid, null);
- });
+ void sendRequest() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ Request request = mock(Request.class);
+ ContentResponse response = mock(ContentResponse.class);
+ when(request.send()).thenReturn(response);
+ when(response.getStatus()).thenReturn(200);
+ when(response.getContentAsString()).thenReturn("{\"jsonrpc\": \"2.0\", \"result\": \"test result\"}");
+
+ SubscribeResult subscribeResult = httpClient.sendRequest(request, SubscribeResult.class,
+ SubscribeResult::isValid, null);
+ assertEquals("2.0", subscribeResult.getJsonrpc());
+ assertEquals("test result", subscribeResult.getResult());
+ }
+
+ @Test
+ void sendRequestResponseError()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ Request request = mock(Request.class);
+ ContentResponse response = mock(ContentResponse.class);
+ when(request.send()).thenReturn(response);
+ when(response.getStatus()).thenReturn(500);
+ ExecutionException e = assertThrows(ExecutionException.class,
+ () -> httpClient.sendRequest(request, SubscribeResult.class, SubscribeResult::isValid, null));
+ assertEquals("Request failed with status code 500", e.getMessage());
+ }
+
+ @Test
+ void sendRequestResponseErrorWithErrorHandler()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ Request request = mock(Request.class);
+ ContentResponse response = mock(ContentResponse.class);
+ when(request.send()).thenReturn(response);
+ when(response.getStatus()).thenReturn(500);
+ when(response.getContentAsString()).thenReturn(
+ "{\"@type\": \"JsonRestExceptionResponseEntity\", \"errorCode\": \"500\", \"statusCode\": \"500\"}");
+
+ BoschSHCException e = assertThrows(BoschSHCException.class, () -> httpClient.sendRequest(request, Device.class,
+ Device::isValid, (Integer statusCode, String content) -> {
+ return new BoschSHCException("test exception");
+ }));
+ assertEquals("test exception", e.getMessage());
+ }
+
+ @Test
+ void sendRequestEmptyResponse()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ Request request = mock(Request.class);
+ ContentResponse response = mock(ContentResponse.class);
+ when(request.send()).thenReturn(response);
+ when(response.getStatus()).thenReturn(200);
+ ExecutionException e = assertThrows(ExecutionException.class,
+ () -> httpClient.sendRequest(request, SubscribeResult.class, SubscribeResult::isValid, null));
+ assertEquals(
+ "Received no content in response, expected type org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult",
+ e.getMessage());
+ }
+
+ @Test
+ void sendRequestInvalidResponse()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ Request request = mock(Request.class);
+ ContentResponse response = mock(ContentResponse.class);
+ when(request.send()).thenReturn(response);
+ when(response.getStatus()).thenReturn(200);
+ when(response.getContentAsString()).thenReturn(
+ "{\"@type\": \"JsonRestExceptionResponseEntity\", \"errorCode\": \"500\", \"statusCode\": \"500\"}");
+ ExecutionException e = assertThrows(ExecutionException.class,
+ () -> httpClient.sendRequest(request, SubscribeResult.class, sr -> {
+ return false;
+ }, null));
+ String actualMessage = e.getMessage();
+ assertTrue(actualMessage.contains(
+ "Received invalid content for type org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult:"));
+ }
+
+ @Test
+ void sendRequestInvalidSyntaxInResponse()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ Request request = mock(Request.class);
+ ContentResponse response = mock(ContentResponse.class);
+ when(request.send()).thenReturn(response);
+ when(response.getStatus()).thenReturn(200);
+ when(response.getContentAsString()).thenReturn("{\"@type\": \"JsonRestExceptionResponseEntity}");
+ ExecutionException e = assertThrows(ExecutionException.class,
+ () -> httpClient.sendRequest(request, SubscribeResult.class, sr -> {
+ return false;
+ }, null));
+ assertEquals(
+ "Received invalid content in response, expected type org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult: com.google.gson.stream.MalformedJsonException: Unterminated string at line 1 column 44 path $.@type",
+ e.getMessage());
}
}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeConfigurationTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeConfigurationTest.java
new file mode 100644
index 0000000000000..4a6a8d38f33c5
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeConfigurationTest.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.bridge;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link BridgeConfiguration}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class BridgeConfigurationTest {
+
+ @Test
+ void testConstructor() {
+ BridgeConfiguration fixture = new BridgeConfiguration();
+ assertEquals("", fixture.ipAddress);
+ assertEquals("", fixture.password);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java
index 3c61340c1fa64..1f77e17416b2b 100644
--- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java
@@ -12,45 +12,101 @@
*/
package org.openhab.binding.boschshc.internal.devices.bridge;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.same;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
+import java.util.function.BiFunction;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.binaryswitch.dto.BinarySwitchServiceState;
import org.openhab.binding.boschshc.internal.services.intrusion.actions.arm.dto.ArmActionRequest;
+import org.openhab.binding.boschshc.internal.services.intrusion.dto.AlarmState;
+import org.openhab.binding.boschshc.internal.services.intrusion.dto.ArmingState;
+import org.openhab.binding.boschshc.internal.services.intrusion.dto.IntrusionDetectionSystemState;
+import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
+import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
+import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
/**
* Unit tests for the {@link BridgeHandler}.
- *
+ *
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
class BridgeHandlerTest {
- @Nullable
- private BridgeHandler fixture;
+ private @NonNullByDefault({}) BridgeHandler fixture;
+
+ private @NonNullByDefault({}) BoschHttpClient httpClient;
- @Nullable
- private BoschHttpClient httpClient;
+ private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
+
+ @BeforeAll
+ static void beforeAll() throws IOException {
+ Path mavenTargetFolder = Paths.get("target");
+ assertTrue(Files.exists(mavenTargetFolder), "Maven target folder does not exist.");
+ System.setProperty("openhab.userdata", mavenTargetFolder.toFile().getAbsolutePath());
+ Path etc = mavenTargetFolder.resolve("etc");
+ if (!Files.exists(etc)) {
+ Files.createDirectory(etc);
+ }
+ }
@BeforeEach
- void beforeEach() {
+ void beforeEach() throws Exception {
Bridge bridge = mock(Bridge.class);
fixture = new BridgeHandler(bridge);
+
+ thingHandlerCallback = mock(ThingHandlerCallback.class);
+ fixture.setCallback(thingHandlerCallback);
+
+ Configuration bridgeConfiguration = new Configuration();
+ Map properties = new HashMap<>();
+ properties.put("ipAddress", "localhost");
+ properties.put("password", "test");
+ bridgeConfiguration.setProperties(properties);
+
+ Thing thing = mock(Bridge.class);
+ when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
+ // this calls initialize() as well
+ fixture.thingUpdated(thing);
+
+ // shut down the real HTTP client
+ if (fixture.httpClient != null) {
+ fixture.httpClient.stop();
+ }
+
+ // use a mocked HTTP client
httpClient = mock(BoschHttpClient.class);
fixture.httpClient = httpClient;
}
@@ -69,4 +125,265 @@ void postAction() throws InterruptedException, TimeoutException, ExecutionExcept
verify(httpClient).createRequest(eq(url), same(HttpMethod.POST), same(request));
verify(mockRequest).send();
}
+
+ @Test
+ void initialAccessHttpClientOffline() {
+ fixture.initialAccess(httpClient);
+ }
+
+ @Test
+ void initialAccessHttpClientOnline() throws InterruptedException {
+ when(httpClient.isOnline()).thenReturn(true);
+ fixture.initialAccess(httpClient);
+ }
+
+ @Test
+ void initialAccessAccessPossible()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ when(httpClient.isOnline()).thenReturn(true);
+ when(httpClient.isAccessPossible()).thenReturn(true);
+ when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+
+ // mock a request and response to obtain rooms
+ Request roomsRequest = mock(Request.class);
+ ContentResponse roomsResponse = mock(ContentResponse.class);
+ when(roomsResponse.getStatus()).thenReturn(200);
+ when(roomsResponse.getContentAsString()).thenReturn(
+ "[{\"@type\":\"room\",\"id\":\"hz_1\",\"iconId\":\"icon_room_bedroom\",\"name\":\"Bedroom\"}]");
+ when(roomsRequest.send()).thenReturn(roomsResponse);
+ when(httpClient.createRequest(contains("/rooms"), same(HttpMethod.GET))).thenReturn(roomsRequest);
+
+ // mock a request and response to obtain devices
+ Request devicesRequest = mock(Request.class);
+ ContentResponse devicesResponse = mock(ContentResponse.class);
+ when(devicesResponse.getStatus()).thenReturn(200);
+ when(devicesResponse.getContentAsString()).thenReturn("[{\"@type\":\"device\",\r\n"
+ + " \"rootDeviceId\":\"64-da-a0-02-14-9b\",\r\n"
+ + " \"id\":\"hdm:HomeMaticIP:3014F711A00004953859F31B\",\r\n"
+ + " \"deviceServiceIds\":[\"PowerMeter\",\"PowerSwitch\",\"PowerSwitchProgram\",\"Routing\"],\r\n"
+ + " \"manufacturer\":\"BOSCH\",\r\n" + " \"roomId\":\"hz_3\",\r\n" + " \"deviceModel\":\"PSM\",\r\n"
+ + " \"serial\":\"3014F711A00004953859F31B\",\r\n" + " \"profile\":\"GENERIC\",\r\n"
+ + " \"name\":\"Coffee Machine\",\r\n" + " \"status\":\"AVAILABLE\",\r\n" + " \"childDeviceIds\":[]\r\n"
+ + " }]");
+ when(devicesRequest.send()).thenReturn(devicesResponse);
+ when(httpClient.createRequest(contains("/devices"), same(HttpMethod.GET))).thenReturn(devicesRequest);
+
+ SubscribeResult subscribeResult = new SubscribeResult();
+ when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
+
+ Request longPollRequest = mock(Request.class);
+ when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
+ argThat((JsonRpcRequest r) -> r.method.equals("RE/longPoll")))).thenReturn(longPollRequest);
+
+ fixture.initialAccess(httpClient);
+ verify(thingHandlerCallback).statusUpdated(any(),
+ eq(ThingStatusInfoBuilder.create(ThingStatus.ONLINE, ThingStatusDetail.NONE).build()));
+ }
+
+ @Test
+ void getState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+ Request request = mock(Request.class);
+ when(request.header(anyString(), anyString())).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+ when(response.getStatus()).thenReturn(200);
+ when(response.getContentAsString()).thenReturn("{\r\n" + " \"@type\": \"systemState\",\r\n"
+ + " \"systemAvailability\": {\r\n" + " \"@type\": \"systemAvailabilityState\",\r\n"
+ + " \"available\": true,\r\n" + " \"deleted\": false\r\n" + " },\r\n"
+ + " \"armingState\": {\r\n" + " \"@type\": \"armingState\",\r\n"
+ + " \"state\": \"SYSTEM_DISARMED\",\r\n" + " \"deleted\": false\r\n" + " },\r\n"
+ + " \"alarmState\": {\r\n" + " \"@type\": \"alarmState\",\r\n"
+ + " \"value\": \"ALARM_OFF\",\r\n" + " \"incidents\": [],\r\n"
+ + " \"deleted\": false\r\n" + " },\r\n" + " \"activeConfigurationProfile\": {\r\n"
+ + " \"@type\": \"activeConfigurationProfile\",\r\n" + " \"deleted\": false\r\n"
+ + " },\r\n" + " \"securityGapState\": {\r\n" + " \"@type\": \"securityGapState\",\r\n"
+ + " \"securityGaps\": [],\r\n" + " \"deleted\": false\r\n" + " },\r\n"
+ + " \"deleted\": false\r\n" + " }");
+ when(request.send()).thenReturn(response);
+ when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+
+ IntrusionDetectionSystemState state = fixture.getState("intrusion/states/system",
+ IntrusionDetectionSystemState.class);
+ assertNotNull(state);
+ assertTrue(state.systemAvailability.available);
+ assertSame(AlarmState.ALARM_OFF, state.alarmState.value);
+ assertSame(ArmingState.SYSTEM_DISARMED, state.armingState.state);
+ }
+
+ @Test
+ void getDeviceState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
+
+ Request request = mock(Request.class);
+ when(request.header(anyString(), anyString())).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+ when(response.getStatus()).thenReturn(200);
+ when(response.getContentAsString())
+ .thenReturn("{\n" + " \"@type\": \"shutterContactState\",\n" + " \"value\": \"OPEN\"\n" + " }");
+ when(request.send()).thenReturn(response);
+ when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+
+ ShutterContactServiceState state = fixture.getState("hdm:HomeMaticIP:3014D711A000009D545DEB39D",
+ "ShutterContact", ShutterContactServiceState.class);
+ assertNotNull(state);
+ assertSame(ShutterContactState.OPEN, state.value);
+ }
+
+ @Test
+ void getDeviceInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+
+ Request request = mock(Request.class);
+ when(request.header(anyString(), anyString())).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+ when(response.getStatus()).thenReturn(200);
+ when(request.send()).thenReturn(response);
+ when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+ when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
+ .thenReturn(DeviceTest.createTestDevice());
+
+ String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
+ Device device = fixture.getDeviceInfo(deviceId);
+ assertEquals(deviceId, device.id);
+ }
+
+ @Test
+ void getDeviceInfoErrorCases()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+
+ Request request = mock(Request.class);
+ when(request.header(anyString(), anyString())).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+ when(response.getStatus()).thenReturn(200);
+ when(request.send()).thenReturn(response);
+ when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> errorResponseHandlerCaptor = ArgumentCaptor
+ .forClass(BiFunction.class);
+
+ when(httpClient.sendRequest(same(request), same(Device.class), any(), errorResponseHandlerCaptor.capture()))
+ .thenReturn(DeviceTest.createTestDevice());
+
+ String deviceId = "hdm:HomeMaticIP:3014F711A00004953859F31B";
+ fixture.getDeviceInfo(deviceId);
+
+ BiFunction errorResponseHandler = errorResponseHandlerCaptor.getValue();
+ Exception e = errorResponseHandler.apply(500,
+ "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
+ assertEquals(
+ "Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500 and error code testErrorCode",
+ e.getMessage());
+
+ e = errorResponseHandler.apply(404,
+ "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"ENTITY_NOT_FOUND\",\"statusCode\": 404}");
+ assertNotNull(e);
+
+ e = errorResponseHandler.apply(500, "");
+ assertEquals("Request for info of device hdm:HomeMaticIP:3014F711A00004953859F31B failed with status code 500",
+ e.getMessage());
+ }
+
+ @Test
+ void getServiceData() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
+
+ Request request = mock(Request.class);
+ when(request.header(anyString(), anyString())).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+ when(response.getStatus()).thenReturn(200);
+ when(response.getContentAsString()).thenReturn("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ + " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ + " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
+ + " \"faults\":{ \n" + " \"entries\":[\n" + " {\n"
+ + " \"type\":\"LOW_BATTERY\",\n" + " \"category\":\"WARNING\"\n" + " }\n"
+ + " ]\n" + " }\n" + "}");
+ when(request.send()).thenReturn(response);
+ when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+
+ DeviceServiceData serviceData = fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel");
+ assertNotNull(serviceData);
+ Faults faults = serviceData.faults;
+ assertNotNull(faults);
+ assertEquals("LOW_BATTERY", faults.entries.get(0).type);
+ }
+
+ @Test
+ void getServiceDataError() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
+
+ Request request = mock(Request.class);
+ when(request.header(anyString(), anyString())).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+ when(response.getStatus()).thenReturn(500);
+ when(response.getContentAsString()).thenReturn(
+ "{\"@type\":\"JsonRestExceptionResponseEntity\",\"errorCode\": \"testErrorCode\",\"statusCode\": 500}");
+ when(request.send()).thenReturn(response);
+ when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+ when(httpClient.sendRequest(same(request), same(Device.class), any(), any()))
+ .thenReturn(DeviceTest.createTestDevice());
+
+ BoschSHCException e = assertThrows(BoschSHCException.class,
+ () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
+ assertEquals(
+ "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500 and error code testErrorCode",
+ e.getMessage());
+ }
+
+ @Test
+ void getServiceDataErrorNoRestExceptionResponse()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getServiceUrl(anyString(), anyString())).thenCallRealMethod();
+
+ Request request = mock(Request.class);
+ when(request.header(anyString(), anyString())).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+ when(response.getStatus()).thenReturn(500);
+ when(response.getContentAsString()).thenReturn("");
+ when(request.send()).thenReturn(response);
+ when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request);
+
+ BoschSHCException e = assertThrows(BoschSHCException.class,
+ () -> fixture.getServiceData("hdm:ZigBee:000d6f0004b93361", "BatteryLevel"));
+ assertEquals(
+ "State request with URL https://null:8444/smarthome/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel failed with status code 500",
+ e.getMessage());
+ }
+
+ @Test
+ void putState() throws InterruptedException, TimeoutException, ExecutionException {
+ when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+ when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod();
+
+ Request request = mock(Request.class);
+ when(request.header(anyString(), anyString())).thenReturn(request);
+ ContentResponse response = mock(ContentResponse.class);
+
+ when(httpClient.createRequest(anyString(), same(HttpMethod.PUT), any(BinarySwitchServiceState.class)))
+ .thenReturn(request);
+ when(request.send()).thenReturn(response);
+
+ BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState();
+ binarySwitchState.on = true;
+ fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState);
+ }
+
+ @AfterEach
+ void afterEach() throws Exception {
+ fixture.dispose();
+ }
}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequestTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequestTest.java
new file mode 100644
index 0000000000000..7d3b43b39b7b4
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequestTest.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.bridge;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link JsonRpcRequest}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class JsonRpcRequestTest {
+
+ private @NonNullByDefault({}) JsonRpcRequest fixture;
+
+ @BeforeEach
+ protected void setUp() throws Exception {
+ fixture = new JsonRpcRequest("2.0", "RE/longPoll", new String[] { "subscriptionId", "20" });
+ }
+
+ @Test
+ public void testConstructor() {
+ assertEquals("2.0", fixture.getJsonrpc());
+ assertEquals("RE/longPoll", fixture.getMethod());
+ assertArrayEquals(new String[] { "subscriptionId", "20" }, fixture.getParams());
+ }
+
+ @Test
+ public void testNoArgConstructor() {
+ fixture = new JsonRpcRequest();
+ assertEquals("", fixture.getJsonrpc());
+ assertEquals("", fixture.getMethod());
+ assertArrayEquals(new String[0], fixture.getParams());
+ }
+
+ @Test
+ public void testSetJsonrpc() {
+ fixture.setJsonrpc("test");
+ assertEquals("test", fixture.getJsonrpc());
+ }
+
+ @Test
+ public void testSetMethod() {
+ fixture.setMethod("RE/subscribe");
+ assertEquals("RE/subscribe", fixture.getMethod());
+ }
+
+ @Test
+ public void testSetParams() {
+ fixture.setParams(new String[] { "com/bosch/sh/remote/*", null });
+ assertArrayEquals(new String[] { "com/bosch/sh/remote/*", null }, fixture.getParams());
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java
new file mode 100644
index 0000000000000..524d23fc111b8
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java
@@ -0,0 +1,327 @@
+/**
+ * Copyright (c) 2010-2023 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.bridge;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Response.CompleteListener;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.eclipse.jetty.http.HttpMethod;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Unit tests for {@link LongPolling}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+public class LongPollingTest {
+
+ /**
+ * A dummy implementation of {@link ScheduledFuture}.
+ *
+ * This is required because we can not return null
in the executor service test implementation (see
+ * below).
+ *
+ * @author David Pace - Initial contribution
+ *
+ * @param The result type returned by this Future
+ */
+ private static class NullScheduledFuture implements ScheduledFuture {
+
+ @Override
+ public long getDelay(@Nullable TimeUnit unit) {
+ return 0;
+ }
+
+ @Override
+ public int compareTo(@Nullable Delayed o) {
+ return 0;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return false;
+ }
+
+ @Override
+ public T get() throws InterruptedException, ExecutionException {
+ return null;
+ }
+
+ @Override
+ public T get(long timeout, @Nullable TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return null;
+ }
+ }
+
+ /**
+ * Executor service implementation that runs all runnables in the same thread in order to enable deterministic
+ * testing.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+ private static class SameThreadExecutorService extends AbstractExecutorService implements ScheduledExecutorService {
+
+ private volatile boolean terminated;
+
+ @Override
+ public void shutdown() {
+ terminated = true;
+ }
+
+ @NonNullByDefault({})
+ @Override
+ public List shutdownNow() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isShutdown() {
+ return terminated;
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return terminated;
+ }
+
+ @Override
+ public boolean awaitTermination(long timeout, @Nullable TimeUnit unit) throws InterruptedException {
+ shutdown();
+ return terminated;
+ }
+
+ @Override
+ public void execute(@Nullable Runnable command) {
+ if (command != null) {
+ // execute in the same thread in unit tests
+ command.run();
+ }
+ }
+
+ @Override
+ public ScheduledFuture> schedule(@Nullable Runnable command, long delay, @Nullable TimeUnit unit) {
+ // not used in this tests
+ return new NullScheduledFuture