[satel] Discovery fix for roller shutter things, other minor changes (#18444)

Signed-off-by: Krzysztof Goworek <krzysztof.goworek@gmail.com>
This commit is contained in:
druciak
2025-03-25 21:58:10 +01:00
committed by GitHub
parent 1608b202ab
commit 0e61bfb32d
3 changed files with 252 additions and 49 deletions
@@ -15,6 +15,7 @@ package org.openhab.binding.satel.internal.discovery;
import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -50,19 +51,19 @@ import org.slf4j.LoggerFactory;
public class SatelDeviceDiscoveryService extends AbstractDiscoveryService {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Stream
.of(DEVICE_THING_TYPES_UIDS, VIRTUAL_THING_TYPES_UIDS).flatMap(uids -> uids.stream())
.of(DEVICE_THING_TYPES_UIDS, VIRTUAL_THING_TYPES_UIDS).flatMap(Collection::stream)
.collect(Collectors.toSet());
private static final int OUTPUT_FUNCTION_SHUTTER = 105;
private final Logger logger = LoggerFactory.getLogger(SatelDeviceDiscoveryService.class);
private SatelBridgeHandler bridgeHandler;
private Function<ThingTypeUID, ThingType> thingTypeProvider;
private final SatelBridgeHandler bridgeHandler;
private final Function<ThingTypeUID, ThingType> thingTypeProvider;
private volatile boolean scanStopped;
public SatelDeviceDiscoveryService(SatelBridgeHandler bridgeHandler,
Function<ThingTypeUID, ThingType> thingTypeProvider) {
super(SUPPORTED_THING_TYPES, 60, true);
super(SUPPORTED_THING_TYPES, 60, false);
this.bridgeHandler = bridgeHandler;
this.thingTypeProvider = thingTypeProvider;
}
@@ -74,20 +75,14 @@ public class SatelDeviceDiscoveryService extends AbstractDiscoveryService {
// add virtual things by default
for (ThingTypeUID thingTypeUID : VIRTUAL_THING_TYPES_UIDS) {
ThingType thingType = thingTypeProvider.apply(thingTypeUID);
addThing(thingTypeUID, null, thingType.getLabel(), Collections.emptyMap());
addThing(thingTypeUID, toCamelCase(thingTypeUID.getId()), thingType.getLabel(), Collections.emptyMap());
}
}
if (!scanStopped) {
scanForDevices(DeviceType.KEYPAD, 8);
scanForDevices(DeviceType.PARTITION, bridgeHandler.getIntegraType().getPartitions());
}
if (!scanStopped) {
scanForDevices(DeviceType.EXPANDER, 64);
}
if (!scanStopped) {
scanForDevices(DeviceType.PARTITION_WITH_OBJECT, bridgeHandler.getIntegraType().getPartitions());
}
if (!scanStopped) {
scanForDevices(DeviceType.ZONE_WITH_PARTITION, bridgeHandler.getIntegraType().getZones());
scanForDevices(DeviceType.ZONE, bridgeHandler.getIntegraType().getZones());
}
if (!scanStopped) {
scanForDevices(DeviceType.OUTPUT, bridgeHandler.getIntegraType().getZones());
@@ -109,9 +104,8 @@ public class SatelDeviceDiscoveryService extends AbstractDiscoveryService {
if (bridgeHandler.sendCommand(cmd, false)) {
String name = cmd.getName(encoding);
int deviceKind = cmd.getDeviceKind();
int info = cmd.getAdditionalInfo();
logger.debug("Found device: type={}, id={}, name={}, kind/function={}, info={}", deviceType.name(), i,
name, deviceKind, info);
name, deviceKind, cmd.getAdditionalInfo());
if (isDeviceAvailable(deviceType, deviceKind)) {
addDevice(deviceType, deviceKind, i, name);
}
@@ -119,7 +113,7 @@ public class SatelDeviceDiscoveryService extends AbstractDiscoveryService {
// serious failure, disconnection or so
scanStopped = true;
logger.error("Unexpected failure during scan for {} using {}", deviceType.name(),
bridgeHandler.getThing().getUID().toString());
bridgeHandler.getThing().getUID());
}
}
if (scanStopped) {
@@ -135,7 +129,7 @@ public class SatelDeviceDiscoveryService extends AbstractDiscoveryService {
if (thingTypeUID == null) {
logger.warn("Unknown device found: type={}, kind={}, name={}", deviceType.name(), deviceKind, deviceName);
} else if (!getSupportedThingTypes().contains(thingTypeUID)) {
logger.warn("Unsupported device: {}", thingTypeUID.toString());
logger.warn("Unsupported device: {}", thingTypeUID);
} else {
Map<String, Object> properties = new HashMap<>();
@@ -150,44 +144,29 @@ public class SatelDeviceDiscoveryService extends AbstractDiscoveryService {
}
}
private void addThing(ThingTypeUID thingTypeUID, @Nullable String deviceId, String label,
Map<String, Object> properties) {
private void addThing(ThingTypeUID thingTypeUID, String deviceId, String label, Map<String, Object> properties) {
final ThingUID bridgeUID = bridgeHandler.getThing().getUID();
final ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID,
deviceId == null ? toCamelCase(thingTypeUID.getId()) : deviceId);
final ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, deviceId);
final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
.withBridge(bridgeUID).withLabel(label).withProperties(properties).build();
thingDiscovered(discoveryResult);
}
private static @Nullable ThingTypeUID getThingTypeUID(DeviceType deviceType, int deviceKind) {
switch (deviceType) {
case OUTPUT:
return (deviceKind == OUTPUT_FUNCTION_SHUTTER) ? THING_TYPE_SHUTTER : THING_TYPE_OUTPUT;
case PARTITION:
case PARTITION_WITH_OBJECT:
return THING_TYPE_PARTITION;
case ZONE:
case ZONE_WITH_PARTITION:
return THING_TYPE_ZONE;
default:
return null;
}
return switch (deviceType) {
case OUTPUT -> (deviceKind == OUTPUT_FUNCTION_SHUTTER) ? THING_TYPE_SHUTTER : THING_TYPE_OUTPUT;
case PARTITION -> THING_TYPE_PARTITION;
case ZONE -> THING_TYPE_ZONE;
default -> null;
};
}
private static boolean isDeviceAvailable(DeviceType deviceType, int deviceKind) {
switch (deviceType) {
case OUTPUT:
return deviceKind != 0 && deviceKind != OUTPUT_FUNCTION_SHUTTER
&& (deviceKind != OUTPUT_FUNCTION_SHUTTER + 1);
case PARTITION:
case PARTITION_WITH_OBJECT:
case ZONE:
case ZONE_WITH_PARTITION:
return true;
default:
return false;
}
return switch (deviceType) {
case OUTPUT -> deviceKind != 0 && deviceKind != (OUTPUT_FUNCTION_SHUTTER + 1);
case PARTITION, ZONE -> true;
default -> false;
};
}
private static String toCamelCase(String s) {
@@ -0,0 +1,228 @@
/*
* Copyright (c) 2010-2025 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.satel.internal.discovery;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.*;
import static org.openhab.binding.satel.internal.SatelBindingConstants.*;
import static org.openhab.binding.satel.internal.command.SatelCommand.State.FAILED;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.function.Function;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.satel.internal.command.ReadDeviceInfoCommand;
import org.openhab.binding.satel.internal.event.EventDispatcher;
import org.openhab.binding.satel.internal.handler.SatelBridgeHandler;
import org.openhab.binding.satel.internal.protocol.SatelMessage;
import org.openhab.binding.satel.internal.types.IntegraType;
import org.openhab.core.config.discovery.DiscoveryListener;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.internal.BridgeImpl;
import org.openhab.core.thing.type.ThingType;
/**
* @author Krzysztof Goworek - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
class SatelDeviceDiscoveryServiceTest {
private static final Charset bridgeEncoding = StandardCharsets.US_ASCII;
@Mock
private SatelBridgeHandler bridgeHandler;
@Mock
private Function<ThingTypeUID, ThingType> thingTypeProvider;
@Mock
private EventDispatcher eventDispatcher;
@Mock
private DiscoveryListener listener;
@InjectMocks
private SatelDeviceDiscoveryService testSubject;
@BeforeEach
void setUp() {
when(bridgeHandler.getIntegraType()).thenReturn(IntegraType.I24);
when(bridgeHandler.getEncoding()).thenReturn(bridgeEncoding);
testSubject.addDiscoveryListener(listener);
}
@Test
void startScanShouldNotAddAnyThingWhenBridgeIsNotInitialized() {
when(bridgeHandler.getThing()).thenReturn(new BridgeImpl(THING_TYPE_ETHM1, "bridgeId"));
testSubject.startScan();
verifyNoInteractions(listener);
}
@Test
void startScanShouldAddVirtualThingsWhenBridgeIsInitialized() {
ThingType thingType = mock(ThingType.class);
when(thingTypeProvider.apply(any())).thenReturn(thingType);
when(bridgeHandler.isInitialized()).thenReturn(true);
when(bridgeHandler.getThing()).thenReturn(new BridgeImpl(THING_TYPE_ETHM1, "bridgeId"));
testSubject.startScan();
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
verify(listener, atLeastOnce()).thingDiscovered(any(), resultCaptor.capture());
List<DiscoveryResult> results = resultCaptor.getAllValues();
assertEquals(2, results.size());
assertEquals(THING_TYPE_SYSTEM, results.get(0).getThingTypeUID());
assertEquals(THING_TYPE_EVENTLOG, results.get(1).getThingTypeUID());
}
@Test
void startScanShouldContinueWhenFailureOccurred() {
setUpCommandFailure();
testSubject.startScan();
verifyNoInteractions(listener);
verify(bridgeHandler, times(52)).sendCommand(any(), eq(false));
}
@Test
void startScanShouldAddAllDevices() {
BridgeImpl bridge = new BridgeImpl(THING_TYPE_ETHM1, "bridgeId");
when(bridgeHandler.getThing()).thenReturn(bridge);
setUpCommandResponse(1);
testSubject.startScan();
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
verify(listener, atLeastOnce()).thingDiscovered(any(), resultCaptor.capture());
List<DiscoveryResult> results = resultCaptor.getAllValues();
assertEquals(4,
results.stream().filter(result -> THING_TYPE_PARTITION.equals(result.getThingTypeUID())).count());
assertEquals(24, results.stream().filter(result -> THING_TYPE_ZONE.equals(result.getThingTypeUID())).count());
assertEquals(24, results.stream().filter(result -> THING_TYPE_OUTPUT.equals(result.getThingTypeUID())).count());
assertTrue(results.stream().allMatch(r -> "Device".equals(r.getLabel())));
for (DiscoveryResult result : results) {
assertEquals("Device", result.getLabel());
assertEquals(bridge.getUID(), result.getBridgeUID());
assertEquals(1, result.getProperties().size());
}
}
@Test
void startScanShouldAddShutters() {
BridgeImpl bridge = new BridgeImpl(THING_TYPE_ETHM1, "bridgeId");
when(bridgeHandler.getThing()).thenReturn(bridge);
setUpCommandResponse(105);
testSubject.startScan();
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
verify(listener, atLeastOnce()).thingDiscovered(any(), resultCaptor.capture());
List<DiscoveryResult> results = resultCaptor.getAllValues().stream()
.filter(result -> THING_TYPE_SHUTTER.equals(result.getThingTypeUID())).toList();
assertEquals(24, results.size());
for (DiscoveryResult result : results) {
assertEquals("Device", result.getLabel());
assertEquals(bridge.getUID(), result.getBridgeUID());
assertEquals(2, result.getProperties().size());
}
}
@Test
void startScanShouldSkipUnusedOutput() {
when(bridgeHandler.getThing()).thenReturn(new BridgeImpl(THING_TYPE_ETHM1, "bridgeId"));
setUpCommandResponse(0);
testSubject.startScan();
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
verify(listener, atLeastOnce()).thingDiscovered(any(), resultCaptor.capture());
List<DiscoveryResult> results = resultCaptor.getAllValues();
assertEquals(0, results.stream().filter(result -> THING_TYPE_OUTPUT.equals(result.getThingTypeUID())).count());
assertEquals(0, results.stream().filter(result -> THING_TYPE_SHUTTER.equals(result.getThingTypeUID())).count());
}
@Test
void startScanShouldSkipSecondShutterOutput() {
when(bridgeHandler.getThing()).thenReturn(new BridgeImpl(THING_TYPE_ETHM1, "bridgeId"));
setUpCommandResponse(106);
testSubject.startScan();
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
verify(listener, atLeastOnce()).thingDiscovered(any(), resultCaptor.capture());
List<DiscoveryResult> results = resultCaptor.getAllValues();
assertEquals(0, results.stream().filter(result -> THING_TYPE_OUTPUT.equals(result.getThingTypeUID())).count());
assertEquals(0, results.stream().filter(result -> THING_TYPE_SHUTTER.equals(result.getThingTypeUID())).count());
}
@Test
void stopScanShouldSkipDiscovery() throws InterruptedException {
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch stopLatch = new CountDownLatch(1);
Thread thread = new Thread(() -> {
reset(bridgeHandler);
when(bridgeHandler.isInitialized()).thenAnswer(invocationOnMock -> {
startLatch.countDown();
stopLatch.await();
return false;
});
testSubject.startScan();
});
thread.start();
startLatch.await();
testSubject.stopScan();
stopLatch.countDown();
thread.join();
verifyNoMoreInteractions(bridgeHandler);
verifyNoInteractions(listener);
}
private void setUpCommandResponse(int deviceKind) {
when(bridgeHandler.sendCommand(isA(ReadDeviceInfoCommand.class), eq(false))).thenAnswer(invocationOnMock -> {
ReadDeviceInfoCommand cmd = invocationOnMock.getArgument(0);
byte[] payload = new byte[19];
byte[] nameBytes = "Device".getBytes(bridgeEncoding);
System.arraycopy(nameBytes, 0, payload, 3, nameBytes.length);
payload[2] = (byte) deviceKind;
cmd.handleResponse(eventDispatcher, new SatelMessage(ReadDeviceInfoCommand.COMMAND_CODE, payload));
return true;
});
}
private void setUpCommandFailure() {
when(bridgeHandler.sendCommand(isA(ReadDeviceInfoCommand.class), eq(false))).thenAnswer(invocationOnMock -> {
ReadDeviceInfoCommand cmd = invocationOnMock.getArgument(0);
cmd.setState(FAILED);
return false;
});
}
}
@@ -145,8 +145,4 @@ class SatelEventLogHandlerTest {
assertEquals(1, result.size());
assertTrue(result.contains(SatelEventLogActions.class));
}
@Test
void readEvent() {
}
}