| child-protection | Switch | ☑ | Indicates whether the child protection is active. |
| power-switch | Switch | ☑ | Switches the relay on or off. Only available if the relay is in power switch mode. |
| impulse-switch | Switch | ☑ | Channel to send impulses by means of `ON` events. After the time specified by `impulse-length`, the relay will switch off automatically and the state will be reset to `OFF`. Only available if the relay is in impulse switch mode. |
-| impulse-length | Number:Time | ☑ | Channel to configure how long the relay will stay on after receiving an impulse switch event. The time is specified in tenth seconds (deciseconds), e.g. 15 means 1.5 seconds. Only available if the relay is in impulse switch mode. |
+| impulse-length | Number:Time | ☑ | Channel to configure how long the relay will stay on after receiving an impulse switch event. If raw numbers (without time unit) are provided, the default unit is tenth seconds (deciseconds), e.g. 15 means 1.5 seconds. If quantities with time units are provided, the quantity will be converted to deciseconds internally, discarding any fraction digits that are more precise than expressible in whole deciseconds (e.g. 1.58 seconds will be converted to 15 ds). Only available if the relay is in impulse switch mode. |
| instant-of-last-impulse | DateTime | ☐ | Timestamp indicating when the last impulse was triggered. Only available if the relay is in impulse switch mode. |
+If the device mode is changed from power switch to impulse switch mode or vice versa, the corresponding thing has to be deleted and re-added in openHAB.
+
### Security Camera 360
Indoor security camera with 360° view and motion detection.
*/
package org.openhab.binding.boschshc.internal.devices.relay;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.BINDING_ID;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import javax.inject.Provider;
+import javax.measure.Unit;
+import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.services.communicationquality.dto.CommunicationQualityServiceState;
import org.openhab.binding.boschshc.internal.services.impulseswitch.ImpulseSwitchService;
import org.openhab.binding.boschshc.internal.services.impulseswitch.dto.ImpulseSwitchServiceState;
-import org.openhab.core.library.CoreItemFactory;
+import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.MetricPrefix;
+import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.binding.builder.ChannelBuilder;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.builder.ThingBuilder;
-import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
private final Logger logger = LoggerFactory.getLogger(RelayHandler.class);
+ protected static final String PROPERTY_MODE = "mode";
+
+ /**
+ * Unit for the impulse length, which is specified in deciseconds (tenth seconds)
+ */
+ private static final Unit<Time> UNIT_DECISECOND = MetricPrefix.DECI(Units.SECOND);
+
private ChildProtectionService childProtectionService;
private ImpulseSwitchService impulseSwitchService;
@Override
protected boolean processDeviceInfo(Device deviceInfo) {
this.isInImpulseSwitchMode = isRelayInImpulseSwitchMode(deviceInfo);
- configureChannels();
+ boolean isChannelConfigurationValid = configureChannels();
+ if (!isChannelConfigurationValid) {
+ return false;
+ }
+
+ updateModePropertyIfApplicable();
return super.processDeviceInfo(deviceInfo);
}
+ private void updateModePropertyIfApplicable() {
+ String modePropertyValue = isInImpulseSwitchMode ? ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME
+ : PowerSwitchService.POWER_SWITCH_SERVICE_NAME;
+ updateProperty(PROPERTY_MODE, modePropertyValue);
+ }
+
/**
* Dynamically configures the channels according to the device mode.
* <p>
* then switches off automatically)</li>
* </ul>
*/
- private void configureChannels() {
- if (isInImpulseSwitchMode) {
- configureImpulseSwitchModeChannels();
- } else {
- configurePowerSwitchModeChannels();
- }
+ private boolean configureChannels() {
+ return isInImpulseSwitchMode ? configureImpulseSwitchModeChannels() : configurePowerSwitchModeChannels();
}
- private void configureImpulseSwitchModeChannels() {
+ private boolean configureImpulseSwitchModeChannels() {
List<String> channelsToBePresent = List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH,
CHANNEL_INSTANT_OF_LAST_IMPULSE);
List<String> channelsToBeAbsent = List.of(CHANNEL_POWER_SWITCH);
- configureChannels(channelsToBePresent, channelsToBeAbsent);
+ return configureChannels(channelsToBePresent, channelsToBeAbsent);
}
- private void configurePowerSwitchModeChannels() {
+ private boolean configurePowerSwitchModeChannels() {
List<String> channelsToBePresent = List.of(CHANNEL_POWER_SWITCH);
List<String> channelsToBeAbsent = List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH,
CHANNEL_INSTANT_OF_LAST_IMPULSE);
- configureChannels(channelsToBePresent, channelsToBeAbsent);
+ return configureChannels(channelsToBePresent, channelsToBeAbsent);
}
/**
* Re-configures the channels of the associated thing, if applicable.
*
- * @param channelsToBePresent channels to be added, if not present already
+ * @param channelsToBePresent channels expected to be present according to the current device mode
* @param channelsToBeAbsent channels to be removed, if present
+ *
+ * @return <code>true</code> if the channels were reconfigured or no re-configuration is necessary,
+ * <code>false</code> if the thing has to be re-created manually
*/
- private void configureChannels(List<String> channelsToBePresent, List<String> channelsToBeAbsent) {
- List<String> channelsToAdd = channelsToBePresent.stream().filter(c -> getThing().getChannel(c) == null)
- .toList();
+ private boolean configureChannels(List<String> channelsToBePresent, List<String> channelsToBeAbsent) {
+ Optional<String> anyChannelMissing = channelsToBePresent.stream().filter(c -> getThing().getChannel(c) == null)
+ .findAny();
+
+ if (anyChannelMissing.isPresent()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.conf-error.relay-recreation-required");
+ return false;
+ }
+
List<Channel> channelsToRemove = channelsToBeAbsent.stream().map(c -> getThing().getChannel(c))
.filter(Objects::nonNull).map(Objects::requireNonNull).toList();
- if (channelsToAdd.isEmpty() && channelsToRemove.isEmpty()) {
- return;
+ if (channelsToRemove.isEmpty()) {
+ return true;
}
ThingBuilder thingBuilder = editThing();
- if (!channelsToAdd.isEmpty()) {
- addChannels(channelsToAdd, thingBuilder);
- }
- if (!channelsToRemove.isEmpty()) {
- thingBuilder.withoutChannels(channelsToRemove);
- }
-
+ thingBuilder.withoutChannels(channelsToRemove);
updateThing(thingBuilder.build());
- }
-
- private void addChannels(List<String> channelsToAdd, ThingBuilder thingBuilder) {
- for (String channelToAdd : channelsToAdd) {
- Channel channel = createChannel(channelToAdd);
- thingBuilder.withChannel(channel);
- }
- }
-
- private Channel createChannel(String channelId) {
- ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelId);
- ChannelTypeUID channelTypeUID = getChannelTypeUID(channelId);
- @Nullable
- String itemType = getItemType(channelId);
- return ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID).build();
- }
-
- private ChannelTypeUID getChannelTypeUID(String channelId) {
- switch (channelId) {
- case CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH, CHANNEL_INSTANT_OF_LAST_IMPULSE:
- return new ChannelTypeUID(BINDING_ID, channelId);
- case CHANNEL_POWER_SWITCH:
- return DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_POWER;
- default:
- throw new UnsupportedOperationException(
- "Cannot determine channel type UID to create channel " + channelId + " dynamically.");
- }
- }
-
- private @Nullable String getItemType(String channelId) {
- switch (channelId) {
- case CHANNEL_POWER_SWITCH, CHANNEL_IMPULSE_SWITCH:
- return CoreItemFactory.SWITCH;
- case CHANNEL_IMPULSE_LENGTH:
- return CoreItemFactory.NUMBER + ":Time";
- case CHANNEL_INSTANT_OF_LAST_IMPULSE:
- return CoreItemFactory.DATETIME;
- default:
- throw new UnsupportedOperationException(
- "Cannot determine item type to create channel " + channelId + " dynamically.");
- }
+ return true;
}
private boolean isRelayInImpulseSwitchMode(Device deviceInfo) {
updateChildProtectionState(onOffCommand);
} else if (CHANNEL_IMPULSE_SWITCH.equals(channelUID.getId()) && command instanceof OnOffType onOffCommand) {
triggerImpulse(onOffCommand);
- } else if (CHANNEL_IMPULSE_LENGTH.equals(channelUID.getId()) && command instanceof DecimalType number) {
- updateImpulseLength(number);
+ } else if (CHANNEL_IMPULSE_LENGTH.equals(channelUID.getId())) {
+ updateImpulseLength(command);
}
}
}
}
- private void updateImpulseLength(DecimalType number) {
+ private void updateImpulseLength(Command command) {
+ Integer impulseLength = getImpulseLength(command);
+ if (impulseLength == null) {
+ return;
+ }
+
ImpulseSwitchServiceState newState = cloneCurrentImpulseSwitchServiceState();
if (newState != null) {
- newState.impulseLength = number.intValue();
+ newState.impulseLength = impulseLength;
this.currentImpulseSwitchServiceState = newState;
logger.debug("New impulse length setting for relay: {} deciseconds", newState.impulseLength);
}
}
+ private @Nullable Integer getImpulseLength(Command command) {
+ if (command instanceof DecimalType decimalCommand) {
+ return decimalCommand.intValue();
+ } else if (command instanceof QuantityType<?> quantityCommand) {
+ @Nullable
+ QuantityType<?> convertedQuantity = quantityCommand.toUnit(UNIT_DECISECOND);
+ return convertedQuantity != null ? convertedQuantity.intValue() : null;
+ } else {
+ return null;
+ }
+ }
+
private @Nullable ImpulseSwitchServiceState cloneCurrentImpulseSwitchServiceState() {
if (currentImpulseSwitchServiceState != null) {
ImpulseSwitchServiceState clonedState = new ImpulseSwitchServiceState();
offline.conf-error.empty-state-id = No ID set.
offline.conf-error.invalid-state-id = ID is invalid.
offline.conf-error.child-device-ids-not-obtainable = Could not obtain child device IDs.
+offline.conf-error.relay-recreation-required = Relay mode (power/impulse switch) change detected. Please delete and re-create this Thing.
package org.openhab.binding.boschshc.internal.devices;
import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
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.TestInfo;
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;
@BeforeEach
@Override
- public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ public void beforeEach(TestInfo testInfo)
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
DeviceServiceData deviceServiceData = new DeviceServiceData();
deviceServiceData.path = "/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel";
deviceServiceData.id = "BatteryLevel";
deviceServiceData.deviceId = "hdm:ZigBee:000d6f0004b93361";
when(getBridgeHandler().getServiceData(anyString(), anyString())).thenReturn(deviceServiceData);
- super.beforeEach();
+ super.beforeEach(testInfo);
}
@Test
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
this.fixture = createFixture();
}
+ /**
+ * Initializes the fixture and all required mocks around the handler.
+ *
+ * @param testInfo used in subclasses where initializing the handler differently in individual tests is required.
+ *
+ * @throws InterruptedException
+ * @throws TimeoutException
+ * @throws ExecutionException
+ * @throws BoschSHCException
+ */
@BeforeEach
- void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ void beforeEach(TestInfo testInfo)
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
fixture = createFixture();
lenient().when(thing.getUID()).thenReturn(getThingUID());
when(thing.getBridgeUID()).thenReturn(new ThingUID("boschshc", "shc", "myBridgeUID"));
configureDevice(device);
lenient().when(bridgeHandler.getDeviceInfo(anyString())).thenReturn(device);
+ beforeHandlerInitialization(testInfo);
+
fixture.initialize();
+
+ afterHandlerInitialization(testInfo);
+ }
+
+ /**
+ * Hook to allow tests to add custom setup code before the handler initialization.
+ *
+ * @param testInfo provides metadata related to the current test being executed
+ */
+ protected void beforeHandlerInitialization(TestInfo testInfo) {
+ // default implementation is empty, subclasses may override
+ }
+
+ /**
+ * Hook to allow tests to add custom setup code after the handler initialization.
+ *
+ * @param testInfo provides metadata related to the current test being executed
+ */
+ protected void afterHandlerInitialization(TestInfo testInfo) {
+ // default implementation is empty, subclasses may override
}
protected abstract T createFixture();
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
@BeforeEach
@Override
- public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ public void beforeEach(TestInfo testInfo)
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
PowerSwitchServiceState powerSwitchServiceState = new PowerSwitchServiceState();
powerSwitchServiceState.switchState = PowerSwitchState.ON;
- when(getBridgeHandler().getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
+ lenient().when(getBridgeHandler().getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
.thenReturn(powerSwitchServiceState);
- super.beforeEach();
+ super.beforeEach(testInfo);
}
@Test
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
@BeforeEach
- public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ public void beforeEach(TestInfo testInfo)
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
PowerMeterServiceState powerMeterServiceState = new PowerMeterServiceState();
powerMeterServiceState.powerConsumption = 12.34d;
powerMeterServiceState.energyConsumption = 56.78d;
lenient().when(getBridgeHandler().getState(anyString(), eq("PowerMeter"), same(PowerMeterServiceState.class)))
.thenReturn(powerMeterServiceState);
- super.beforeEach();
+ super.beforeEach(testInfo);
}
@Test
*/
package org.openhab.binding.boschshc.internal.devices.relay;
-import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
+import javax.measure.quantity.Time;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerTest;
import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
import org.openhab.binding.boschshc.internal.services.impulseswitch.ImpulseSwitchService;
import org.openhab.binding.boschshc.internal.services.impulseswitch.dto.ImpulseSwitchServiceState;
+import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.types.UnDefType;
import com.google.gson.JsonElement;
private @Captor @NonNullByDefault({}) ArgumentCaptor<ImpulseSwitchServiceState> impulseSwitchServiceStateCaptor;
+ @Override
+ protected void beforeHandlerInitialization(TestInfo testInfo) {
+ super.beforeHandlerInitialization(testInfo);
+
+ Channel signalStrengthChannel = ChannelBuilder
+ .create(new ChannelUID(getThingUID(), BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH)).build();
+ Channel childProtectionChannel = ChannelBuilder
+ .create(new ChannelUID(getThingUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION)).build();
+ Channel powerSwitchChannel = ChannelBuilder
+ .create(new ChannelUID(getThingUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH)).build();
+ Channel impulseSwitchChannel = ChannelBuilder
+ .create(new ChannelUID(getThingUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH)).build();
+ Channel impulseLengthChannel = ChannelBuilder
+ .create(new ChannelUID(getThingUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH)).build();
+ Channel instantOfLastImpulseChannel = ChannelBuilder
+ .create(new ChannelUID(getThingUID(), BoschSHCBindingConstants.CHANNEL_INSTANT_OF_LAST_IMPULSE))
+ .build();
+
+ when(getThing().getChannels()).thenReturn(List.of(signalStrengthChannel, childProtectionChannel,
+ powerSwitchChannel, impulseSwitchChannel, impulseLengthChannel, instantOfLastImpulseChannel));
+
+ lenient().when(getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH))
+ .thenReturn(signalStrengthChannel);
+ lenient().when(getThing().getChannel(BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION))
+ .thenReturn(childProtectionChannel);
+ lenient().when(getThing().getChannel(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH))
+ .thenReturn(powerSwitchChannel);
+ lenient().when(getThing().getChannel(BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH))
+ .thenReturn(impulseSwitchChannel);
+ lenient().when(getThing().getChannel(BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH))
+ .thenReturn(impulseLengthChannel);
+ lenient().when(getThing().getChannel(BoschSHCBindingConstants.CHANNEL_INSTANT_OF_LAST_IMPULSE))
+ .thenReturn(instantOfLastImpulseChannel);
+
+ if (testInfo.getTags().contains(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME)) {
+ getDevice().deviceServiceIds = List.of(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME);
+ }
+ }
+
+ @Override
+ protected void afterHandlerInitialization(TestInfo testInfo) {
+ super.afterHandlerInitialization(testInfo);
+
+ @Nullable
+ JsonElement impulseSwitchServiceState = JsonParser.parseString("""
+ {
+ "@type": "ImpulseSwitchState",
+ "impulseState": false,
+ "impulseLength": 100,
+ "instantOfLastImpulse": "2024-04-14T15:52:31.677366Z"
+ }
+ """);
+ getFixture().processUpdate(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME, impulseSwitchServiceState);
+ }
+
@Override
protected RelayHandler createFixture() {
return new RelayHandler(getThing());
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION), OnOffType.ON);
}
+ @Tag(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME)
@Test
void testUpdateChannelsImpulseSwitchService()
throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
- configureImpulseSwitchMode();
String json = """
{
"@type": "ImpulseSwitchState",
JsonElement jsonObject = JsonParser.parseString(json);
getFixture().processUpdate("ImpulseSwitch", jsonObject);
+
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH), OnOffType.ON);
verify(getCallback(), times(2)).stateUpdated(
new DateTimeType("2024-04-14T15:52:31.677366Z"));
}
+ @Tag(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME)
@Test
void testUpdateChannelsImpulseSwitchServiceNoInstantOfLastImpulse()
throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
- configureImpulseSwitchMode();
String json = """
{
"@type": "ImpulseSwitchState",
UnDefType.NULL);
}
- private void configureImpulseSwitchMode()
- throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
+ @Test
+ void testDeviceModeChanged() throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
getDevice().deviceServiceIds = List.of(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME);
+
+ // initialize again to check whether mode change is detected
getFixture().initialize();
- assertThat(getFixture().getThing().getChannel(BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH),
- is(notNullValue()));
- assertThat(getFixture().getThing().getChannel(BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH),
- is(notNullValue()));
- assertThat(getFixture().getThing().getChannel(BoschSHCBindingConstants.CHANNEL_INSTANT_OF_LAST_IMPULSE),
- is(notNullValue()));
+ verify(getCallback()).statusUpdated(any(Thing.class),
+ argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+ && status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
- @Nullable
- JsonElement impulseSwitchServiceState = JsonParser.parseString("""
- {
- "@type": "ImpulseSwitchState",
- "impulseState": false,
- "impulseLength": 100,
- "instantOfLastImpulse": "2024-04-14T15:52:31.677366Z"
- }
- """);
- getFixture().processUpdate(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME, impulseSwitchServiceState);
+ verify(getCallback(), times(1)).statusUpdated(any(Thing.class),
+ argThat(status -> status.getStatus().equals(ThingStatus.ONLINE)));
+
+ verify(getCallback(), times(0)).thingUpdated(
+ argThat(t -> ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME.equals(t.getProperties().get("mode"))));
+ }
+
+ @Test
+ void testDeviceModeUnchanged()
+ throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
+ // initialize again without mode change
+ getFixture().initialize();
+
+ verify(getCallback(), times(0)).statusUpdated(any(Thing.class),
+ argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+ && status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
}
@Test
verify(getBridgeHandler(), times(0)).putState(eq(getDeviceID()), eq("ChildProtection"), any());
}
+ @Tag(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME)
@Test
void testHandleCommandImpulseStateOn()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
- configureImpulseSwitchMode();
-
Instant testDate = Instant.now();
getFixture().setCurrentDateTimeProvider(() -> testDate);
assertThat(state.instantOfLastImpulse, is(testDate.toString()));
}
+ @Tag(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME)
@Test
- void testHandleCommandImpulseLength()
+ void testHandleCommandImpulseLengthDecimalType()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
- configureImpulseSwitchMode();
-
Instant testDate = Instant.now();
getFixture().setCurrentDateTimeProvider(() -> testDate);
assertThat(state.instantOfLastImpulse, is("2024-04-14T15:52:31.677366Z"));
}
+ @Tag(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME)
+ @Test
+ void testHandleCommandImpulseLengthQuantityType()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ Instant testDate = Instant.now();
+ getFixture().setCurrentDateTimeProvider(() -> testDate);
+
+ getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH),
+ new QuantityType<Time>(1.5, Units.SECOND));
+ verify(getBridgeHandler()).putState(eq(getDeviceID()), eq(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME),
+ impulseSwitchServiceStateCaptor.capture());
+ ImpulseSwitchServiceState state = impulseSwitchServiceStateCaptor.getValue();
+ assertThat(state.impulseState, is(false));
+ assertThat(state.impulseLength, is(15));
+ assertThat(state.instantOfLastImpulse, is("2024-04-14T15:52:31.677366Z"));
+ }
+
+ @Tag(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME)
+ @Test
+ void testHandleCommandImpulseLengthQuantityTypeTooManyFractionDigits()
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+ Instant testDate = Instant.now();
+ getFixture().setCurrentDateTimeProvider(() -> testDate);
+
+ // 0.08 s of 1.58 s will be discarded because API precision is limited to deciseconds
+ getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH),
+ new QuantityType<Time>(1.58, Units.SECOND));
+ verify(getBridgeHandler()).putState(eq(getDeviceID()), eq(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME),
+ impulseSwitchServiceStateCaptor.capture());
+ ImpulseSwitchServiceState state = impulseSwitchServiceStateCaptor.getValue();
+ assertThat(state.impulseState, is(false));
+ assertThat(state.impulseLength, is(15));
+ assertThat(state.instantOfLastImpulse, is("2024-04-14T15:52:31.677366Z"));
+ }
+
@Test
void testHandleCommandImpulseStateOff()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
verify(getBridgeHandler(), times(0)).postState(eq(getDeviceID()),
eq(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME), any());
}
+
+ @Test
+ void testUpdateModePropertyIfApplicablePowerSwitchMode() {
+ verify(getCallback(), times(2)).thingUpdated(argThat(t -> PowerSwitchService.POWER_SWITCH_SERVICE_NAME
+ .equals(t.getProperties().get(RelayHandler.PROPERTY_MODE))));
+ }
+
+ @Tag(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME)
+ @Test
+ void testUpdateModePropertyIfApplicableImpulseSwitchMode() {
+ verify(getCallback(), times(2)).thingUpdated(argThat(t -> ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME
+ .equals(t.getProperties().get(RelayHandler.PROPERTY_MODE))));
+ }
}