[boschshc] Handle relay mode changes during initialization (#17160)

* [boschshc] Handle relay mode changes during initialization

Signed-off-by: David Pace <dev@davidpace.de>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
David Pace 2024-08-01 09:44:11 +02:00 committed by Ciprian Pascu
parent 476a37544a
commit ed9afe8fbe
8 changed files with 276 additions and 110 deletions

View File

@ -235,9 +235,11 @@ The smart switching relay is your universal all-rounder for smart switching.
| child-protection | Switch | &#9745; | Indicates whether the child protection is active. |
| power-switch | Switch | &#9745; | Switches the relay on or off. Only available if the relay is in power switch mode. |
| impulse-switch | Switch | &#9745; | 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 | &#9745; | 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 | &#9745; | 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 | &#9744; | 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.

View File

@ -12,7 +12,6 @@
*/
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;
@ -23,8 +22,11 @@ import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConst
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;
@ -37,17 +39,19 @@ import org.openhab.binding.boschshc.internal.services.communicationquality.Commu
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;
@ -79,6 +83,13 @@ public class RelayHandler extends AbstractPowerSwitchHandler {
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;
@ -108,10 +119,21 @@ public class RelayHandler extends AbstractPowerSwitchHandler {
@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>
@ -123,94 +145,54 @@ public class RelayHandler extends AbstractPowerSwitchHandler {
* 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);
}
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) {
@ -263,8 +245,8 @@ public class RelayHandler extends AbstractPowerSwitchHandler {
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);
}
}
@ -288,10 +270,15 @@ public class RelayHandler extends AbstractPowerSwitchHandler {
}
}
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);
@ -300,6 +287,18 @@ public class RelayHandler extends AbstractPowerSwitchHandler {
}
}
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();

View File

@ -220,3 +220,4 @@ offline.conf-error.invalid-device-id = Device ID is invalid.
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.

View File

@ -13,7 +13,9 @@
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;
@ -21,6 +23,7 @@ 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;
@ -45,14 +48,15 @@ public abstract class AbstractBatteryPoweredDeviceHandlerTest<T extends Abstract
@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

View File

@ -28,6 +28,7 @@ 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.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ -72,8 +73,19 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
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"));
@ -86,7 +98,29 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
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();

View File

@ -18,6 +18,7 @@ import static org.mockito.ArgumentMatchers.anyString;
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;
@ -28,6 +29,7 @@ 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.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
@ -59,13 +61,14 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
@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

View File

@ -29,6 +29,7 @@ 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.junit.jupiter.api.TestInfo;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
@ -56,14 +57,15 @@ public abstract class AbstractPowerSwitchHandlerWithPowerMeterTest<T extends Abs
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

View File

@ -12,22 +12,28 @@
*/
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;
@ -36,11 +42,19 @@ import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
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;
@ -59,6 +73,61 @@ class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
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());
@ -118,10 +187,10 @@ class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
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",
@ -133,6 +202,7 @@ class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
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(
@ -143,10 +213,10 @@ class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
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",
@ -167,28 +237,33 @@ class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
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"
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"))));
}
""");
getFixture().processUpdate(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME, impulseSwitchServiceState);
@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
@ -211,11 +286,10 @@ class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
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);
@ -229,11 +303,10 @@ class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
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);
@ -247,6 +320,41 @@ class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
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 {
@ -255,4 +363,17 @@ class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
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))));
}
}