From 1cfaf20fdd73caf696ca88b343c4ab0832010f95 Mon Sep 17 00:00:00 2001 From: Gerd Zanker Date: Mon, 26 Feb 2024 21:04:04 +0100 Subject: [PATCH] [boschshc] Add command to list SHC device mappings (#15060) * [boschshc] add command to list Bosch Smart Home Controller devices and mapping to openhab devices and related services Signed-off-by: Gerd Zanker --- .../console/BoschShcCommandExtension.java | 264 ++++++++++++++++++ .../devices/bridge/BridgeHandler.java | 22 +- .../internal/devices/bridge/dto/Device.java | 2 +- .../devices/bridge/dto/PublicInformation.java | 6 + .../internal/devices/bridge/dto/Scenario.java | 10 +- .../bridge/dto/SoftwareUpdateState.java | 30 ++ .../devices/bridge/dto/SubscribeResult.java | 2 +- .../devices/bridge/dto/UserDefinedState.java | 2 +- .../discovery/ThingDiscoveryService.java | 2 +- .../console/BoschShcCommandExtensionTest.java | 228 +++++++++++++++ 10 files changed, 555 insertions(+), 13 deletions(-) create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SoftwareUpdateState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtensionTest.java diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java new file mode 100644 index 00000000000..822146bff43 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java @@ -0,0 +1,264 @@ +/** + * Copyright (c) 2010-2024 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.console; + +import static org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService.DEVICEMODEL_TO_THINGTYPE_MAP; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; +import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.ConsoleCommandCompleter; +import org.openhab.core.io.console.StringsCompleter; +import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Console command to list Bosch SHC devices and openhab support. + * Use the SHC API to get all SHC devices and SHC services + * and tries to lookup openhab devices and implemented service classes. + * Prints each name and looked-up implementation on console. + * + * @author Gerd Zanker - Initial contribution + */ +@NonNullByDefault +@Component(service = ConsoleCommandExtension.class) +public class BoschShcCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter { + + static final String SHOW_BINDINGINFO = "showBindingInfo"; + static final String SHOW_DEVICES = "showDevices"; + static final String SHOW_SERVICES = "showServices"; + + static final String GET_BRIDGEINFO = "bridgeInfo"; + static final String GET_DEVICES = "deviceInfo"; + private static final StringsCompleter SUBCMD_COMPLETER = new StringsCompleter( + List.of(SHOW_BINDINGINFO, SHOW_DEVICES, SHOW_SERVICES, GET_BRIDGEINFO, GET_DEVICES), false); + + private final ThingRegistry thingRegistry; + + @Activate + public BoschShcCommandExtension(final @Reference ThingRegistry thingRegistry) { + super(BoschSHCBindingConstants.BINDING_ID, "Interact with the Bosch Smart Home Controller."); + this.thingRegistry = thingRegistry; + } + + /** + * Returns all implemented services of this Bosch SHC binding. + * This list shall contain all available services and needs to be extended when a new service is added. + * A unit tests checks if this list matches with the existing subfolders in + * "src/main/java/org/openhab/binding/boschshc/internal/services". + */ + List getAllBoschShcServices() { + return List.of("airqualitylevel", "batterylevel", "binaryswitch", "bypass", "cameranotification", "childlock", + "communicationquality", "hsbcoloractuator", "humiditylevel", "illuminance", "intrusion", "keypad", + "latestmotion", "multilevelswitch", "powermeter", "powerswitch", "privacymode", "roomclimatecontrol", + "shuttercontact", "shuttercontrol", "silentmode", "smokedetectorcheck", "temperaturelevel", "userstate", + "valvetappet"); + } + + @Override + public void execute(String[] args, Console console) { + if (args.length == 0) { + printUsage(console); + return; + } + try { + if (GET_BRIDGEINFO.equals(args[0])) { + console.print(buildBridgeInfo()); + return; + } + if (GET_DEVICES.equals(args[0])) { + console.print(buildDeviceInfo()); + return; + } + if (SHOW_BINDINGINFO.equals(args[0])) { + console.print(buildBindingInfo()); + return; + } + if (SHOW_DEVICES.equals(args[0])) { + console.print(buildSupportedDeviceStatus()); + return; + } + if (SHOW_SERVICES.equals(args[0])) { + console.print(buildSupportedServiceStatus()); + return; + } + } catch (BoschSHCException | ExecutionException | TimeoutException e) { + console.print(String.format("Error %1s%n", e.getMessage())); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + // unsupported command, print usage + printUsage(console); + } + + private List getBridgeHandlers() { + List bridges = new ArrayList<>(); + for (Thing thing : thingRegistry.getAll()) { + ThingHandler thingHandler = thing.getHandler(); + if (thingHandler instanceof BridgeHandler bridgeHandler) { + bridges.add(bridgeHandler); + } + } + return bridges; + } + + String buildBridgeInfo() throws BoschSHCException, InterruptedException, ExecutionException, TimeoutException { + List bridges = getBridgeHandlers(); + StringBuilder builder = new StringBuilder(); + for (BridgeHandler bridgeHandler : bridges) { + builder.append(String.format("Bridge: %1s%n", bridgeHandler.getThing().getLabel())); + builder.append(String.format(" access possible: %1s%n", bridgeHandler.checkBridgeAccess())); + + PublicInformation publicInformation = bridgeHandler.getPublicInformation(); + builder.append(String.format(" SHC Generation: %1s%n", publicInformation.shcGeneration)); + builder.append(String.format(" IP Address: %1s%n", publicInformation.shcIpAddress)); + builder.append(String.format(" API Versions: %1s%n", publicInformation.apiVersions)); + builder.append(String.format(" Software Version: %1s%n", + publicInformation.softwareUpdateState.swInstalledVersion)); + builder.append(String.format(" Version Update State: %1s%n", + publicInformation.softwareUpdateState.swUpdateState)); + builder.append(String.format(" Available Version: %1s%n", + publicInformation.softwareUpdateState.swUpdateAvailableVersion)); + builder.append(String.format("%n")); + } + return builder.toString(); + } + + String buildDeviceInfo() throws InterruptedException { + StringBuilder builder = new StringBuilder(); + for (Thing thing : thingRegistry.getAll()) { + ThingHandler thingHandler = thing.getHandler(); + if (thingHandler instanceof BridgeHandler bridgeHandler) { + builder.append(String.format("thing: %1s%n", thing.getLabel())); + builder.append(String.format(" thingHandler: %1s%n", thingHandler.getClass().getName())); + builder.append(String.format("bridge access possible: %1s%n", bridgeHandler.checkBridgeAccess())); + + List devices = bridgeHandler.getDevices(); + builder.append(String.format("devices (%1d): %n", devices.size())); + for (Device device : devices) { + builder.append(buildDeviceInfo(device)); + builder.append(String.format("%n")); + } + } + } + return builder.toString(); + } + + private String buildDeviceInfo(Device device) { + StringBuilder builder = new StringBuilder(); + builder.append(String.format(" deviceID: %1s%n", device.id)); + builder.append(String.format(" type: %1s -> ", device.deviceModel)); + if (DEVICEMODEL_TO_THINGTYPE_MAP.containsKey(device.deviceModel)) { + builder.append(DEVICEMODEL_TO_THINGTYPE_MAP.get(device.deviceModel).getId()); + } else { + builder.append("!UNSUPPORTED!"); + } + builder.append(String.format("%n")); + + builder.append(buildDeviceServices(device.deviceServiceIds)); + return builder.toString(); + } + + private String buildDeviceServices(List deviceServiceIds) { + StringBuilder builder = new StringBuilder(); + List existingServices = getAllBoschShcServices(); + for (String serviceName : deviceServiceIds) { + builder.append(String.format(" service: %1s -> ", serviceName)); + + if (existingServices.stream().anyMatch(s -> s.equals(serviceName.toLowerCase()))) { + for (String existingService : existingServices) { + if (existingService.equals(serviceName.toLowerCase())) { + builder.append(existingService); + } + } + } else { + builder.append("!UNSUPPORTED!"); + } + builder.append(String.format("%n")); + } + return builder.toString(); + } + + String buildBindingInfo() { + StringBuilder builder = new StringBuilder(); + builder.append(String.format("Bosch SHC Binding%n")); + Bundle bundle = FrameworkUtil.getBundle(getClass()); + if (bundle != null) { + builder.append(String.format(" SymbolicName %1s%n", bundle.getSymbolicName())); + builder.append(String.format(" Version %1s%n", bundle.getVersion())); + } + return builder.toString(); + } + + String buildSupportedDeviceStatus() { + StringBuilder builder = new StringBuilder(); + builder.append(String.format("Supported Devices (%1d):%n", DEVICEMODEL_TO_THINGTYPE_MAP.size())); + for (Map.Entry entry : DEVICEMODEL_TO_THINGTYPE_MAP.entrySet()) { + builder.append( + String.format(" - %1s = %1s%n", entry.getKey(), DEVICEMODEL_TO_THINGTYPE_MAP.get(entry.getKey()))); + } + return builder.toString(); + } + + String buildSupportedServiceStatus() { + StringBuilder builder = new StringBuilder(); + List supportedServices = getAllBoschShcServices(); + builder.append(String.format("Supported Services (%1d):%n", supportedServices.size())); + for (String service : supportedServices) { + builder.append(String.format(" - %1s%n", service)); + } + return builder.toString(); + } + + @Override + public List getUsages() { + return List.of(buildCommandUsage(SHOW_BINDINGINFO, "list detailed information about this binding"), + buildCommandUsage(SHOW_DEVICES, "list all devices supported by this binding"), + buildCommandUsage(SHOW_SERVICES, "list all services supported by this binding"), + buildCommandUsage(GET_DEVICES, "get all Bosch SHC devices"), + buildCommandUsage(GET_BRIDGEINFO, "get detailed information from Bosch SHC")); + } + + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return this; + } + + @Override + public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) { + if (cursorArgumentIndex <= 0) { + return SUBCMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); + } + return false; + } +} 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 0cd68c8a6dd..bfcedd426e8 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 @@ -39,6 +39,7 @@ import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; 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.LongPollResult; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation; import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room; import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario; import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState; @@ -432,6 +433,23 @@ public class BridgeHandler extends BaseBridgeHandler { } } + /** + * Get public information from Bosch SHC. + */ + public PublicInformation getPublicInformation() + throws InterruptedException, BoschSHCException, ExecutionException, TimeoutException { + @Nullable + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == null) { + throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED); + } + + String url = localHttpClient.getPublicInformationUrl(); + Request request = localHttpClient.createRequest(url, GET); + + return localHttpClient.sendRequest(request, PublicInformation.class, PublicInformation::isValid, null); + } + public boolean registerDiscoveryListener(ThingDiscoveryService listener) { if (thingDiscoveryService == null) { thingDiscoveryService = listener; @@ -604,7 +622,7 @@ public class BridgeHandler extends BaseBridgeHandler { @Nullable BoschHttpClient localHttpClient = this.httpClient; if (localHttpClient == null) { - throw new BoschSHCException("HTTP client not initialized"); + throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED); } String url = localHttpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId)); @@ -634,7 +652,7 @@ public class BridgeHandler extends BaseBridgeHandler { @Nullable BoschHttpClient locaHttpClient = this.httpClient; if (locaHttpClient == null) { - throw new BoschSHCException("HTTP client not initialized"); + throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED); } String url = locaHttpClient.getBoschSmartHomeUrl(String.format("userdefinedstates/%s", stateId)); diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java index 845d2af7247..2dbb3128a89 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java @@ -55,7 +55,7 @@ public class Device { public String status; public List childDeviceIds; - public static Boolean isValid(Device obj) { + public static boolean isValid(Device obj) { return obj != null && obj.id != null; } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/PublicInformation.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/PublicInformation.java index a049f649718..f102dd1ee06 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/PublicInformation.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/PublicInformation.java @@ -42,4 +42,10 @@ public class PublicInformation { public List apiVersions; public String shcIpAddress; public String shcGeneration; + public SoftwareUpdateState softwareUpdateState; + + public static boolean isValid(PublicInformation obj) { + return obj != null && obj.shcIpAddress != null && obj.shcGeneration != null && obj.apiVersions != null + && SoftwareUpdateState.isValid(obj.softwareUpdateState); + } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Scenario.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Scenario.java index a67fc817121..d6b6157153d 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Scenario.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Scenario.java @@ -48,17 +48,13 @@ public class Scenario extends BoschSHCServiceState { return scenario; } - public static Boolean isValid(Scenario[] scenarios) { + public static boolean isValid(Scenario[] scenarios) { return Arrays.stream(scenarios).allMatch(scenario -> (scenario.id != null)); } @Override public String toString() { - final StringBuilder sb = new StringBuilder("Scenario{"); - sb.append("name='").append(name).append("'"); - sb.append(", id='").append(id).append("'"); - sb.append(", lastTimeTriggered='").append(lastTimeTriggered).append("'"); - sb.append('}'); - return sb.toString(); + return "Scenario{" + "name='" + name + "'" + ", id='" + id + "'" + ", lastTimeTriggered='" + lastTimeTriggered + + "'" + '}'; } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SoftwareUpdateState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SoftwareUpdateState.java new file mode 100644 index 00000000000..a7e94098a40 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SoftwareUpdateState.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 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.dto; + +/** + * Software Update State is part of PublicInformation. + * + * @author Gerd Zanker - Initial contribution + */ +public class SoftwareUpdateState { + + public String swUpdateState; + public String swInstalledVersion; + public String swUpdateAvailableVersion; + + public static boolean isValid(SoftwareUpdateState obj) { + return obj != null && obj.swUpdateState != null && obj.swInstalledVersion != null + && obj.swUpdateAvailableVersion != null; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java index 55df8a0db95..c74933b5c0d 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java @@ -31,7 +31,7 @@ public class SubscribeResult { return this.jsonrpc; } - public static Boolean isValid(SubscribeResult obj) { + public static boolean isValid(SubscribeResult obj) { return obj != null && obj.result != null && obj.jsonrpc != null; } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedState.java index 60ac6d37dd5..ff1615b4801 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedState.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedState.java @@ -70,7 +70,7 @@ public class UserDefinedState extends BoschSHCServiceState { + type + '\'' + '}'; } - public static Boolean isValid(UserDefinedState obj) { + public static boolean isValid(UserDefinedState obj) { return obj != null && obj.id != null; } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java index 0562dcacf25..4e0246e778f 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java @@ -68,7 +68,7 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService< BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR); // @formatter:off - protected static final Map DEVICEMODEL_TO_THINGTYPE_MAP = Map.ofEntries( + public static final Map DEVICEMODEL_TO_THINGTYPE_MAP = Map.ofEntries( new AbstractMap.SimpleEntry<>("BBL", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL), new AbstractMap.SimpleEntry<>("TWINGUARD", BoschSHCBindingConstants.THING_TYPE_TWINGUARD), new AbstractMap.SimpleEntry<>("BSM", BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH), diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtensionTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtensionTest.java new file mode 100644 index 00000000000..85f7ea3a431 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtensionTest.java @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2010-2024 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.console; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +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.devices.bridge.dto.Device; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.SoftwareUpdateState; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.core.io.console.Console; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; + +/** + * Unit tests for Console command to list Bosch SHC devices and openhab support. + * + * @author Gerd Zanker - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@NonNullByDefault +class BoschShcCommandExtensionTest { + + private @NonNullByDefault({}) BoschShcCommandExtension fixture; + + private @Mock @NonNullByDefault({}) ThingRegistry thingRegistry; + + @BeforeEach + void setUp() { + fixture = new BoschShcCommandExtension(thingRegistry); + } + + @Test + void execute() { + // only sanity checks, content is tested with the functions called by execute + Console consoleMock = mock(Console.class); + when(thingRegistry.getAll()).thenReturn(Collections.emptyList()); + + fixture.execute(new String[] {}, consoleMock); + verify(consoleMock, times(5)).printUsage(any()); + fixture.execute(new String[] { "" }, consoleMock); + verify(consoleMock, times(10)).printUsage(any()); + + fixture.execute(new String[] { BoschShcCommandExtension.SHOW_BINDINGINFO }, consoleMock); + verify(consoleMock, atLeastOnce()).print(any()); + fixture.execute(new String[] { BoschShcCommandExtension.SHOW_DEVICES }, consoleMock); + verify(consoleMock, atLeastOnce()).print(any()); + fixture.execute(new String[] { BoschShcCommandExtension.SHOW_SERVICES }, consoleMock); + verify(consoleMock, atLeastOnce()).print(any()); + + fixture.execute(new String[] { BoschShcCommandExtension.GET_BRIDGEINFO }, consoleMock); + verify(consoleMock, atLeastOnce()).print(any()); + fixture.execute(new String[] { BoschShcCommandExtension.GET_DEVICES }, consoleMock); + verify(consoleMock, atLeastOnce()).print(any()); + } + + @Test + void getCompleter() { + assertThat(fixture.getCompleter(), is(fixture)); + } + + @Test + void getUsages() { + List strings = fixture.getUsages(); + assertThat(strings.size(), is(5)); + assertThat(strings.get(0), is("boschshc showBindingInfo - list detailed information about this binding")); + assertThat(strings.get(1), is("boschshc showDevices - list all devices supported by this binding")); + } + + @Test + void complete() { + ArrayList candidates = new ArrayList<>(); + assertThat(fixture.complete(new String[] { "" }, 1, 0, candidates), is(false)); + assertThat(fixture.complete(new String[] { "" }, 0, 0, candidates), is(true)); + // for empty arguments, the completer suggest all usage commands + assertThat(candidates.size(), is(fixture.getUsages().size())); + } + + @Test + void printBridgeInfo() throws BoschSHCException, ExecutionException, InterruptedException, TimeoutException { + // no bridge + when(thingRegistry.getAll()).thenReturn(Collections.emptyList()); + assertThat(fixture.buildBridgeInfo(), is("")); + + // one bridge + PublicInformation publicInformation = new PublicInformation(); + publicInformation.shcGeneration = "Gen-T"; + publicInformation.shcIpAddress = "1.2.3.4"; + publicInformation.softwareUpdateState = new SoftwareUpdateState(); + Bridge mockBridge = mock(Bridge.class); + when(mockBridge.getLabel()).thenReturn("TestLabel"); + BridgeHandler mockBridgeHandler = mock(BridgeHandler.class); + when(mockBridgeHandler.getThing()).thenReturn(mockBridge); + when(mockBridgeHandler.getPublicInformation()).thenReturn(publicInformation); + Thing mockBridgeThing = mock(Thing.class); + when(mockBridgeThing.getHandler()).thenReturn(mockBridgeHandler); + when(thingRegistry.getAll()).thenReturn(Collections.singletonList(mockBridgeThing)); + assertThat(fixture.buildBridgeInfo(), + allOf(containsString("Bridge: TestLabel"), containsString("access possible: false"), + containsString("SHC Generation: Gen-T"), containsString("IP Address: 1.2.3.4"))); + + // two bridges + PublicInformation publicInformation2 = new PublicInformation(); + publicInformation2.shcGeneration = "Gen-U"; + publicInformation2.shcIpAddress = "11.22.33.44"; + publicInformation2.softwareUpdateState = new SoftwareUpdateState(); + Bridge mockBridge2 = mock(Bridge.class); + when(mockBridge2.getLabel()).thenReturn("Bridge 2"); + BridgeHandler mockBridgeHandler2 = mock(BridgeHandler.class); + when(mockBridgeHandler2.getThing()).thenReturn(mockBridge2); + when(mockBridgeHandler2.getPublicInformation()).thenReturn(publicInformation2); + Thing mockBridgeThing2 = mock(Thing.class); + when(mockBridgeThing2.getHandler()).thenReturn(mockBridgeHandler2); + when(thingRegistry.getAll()).thenReturn(Arrays.asList(mockBridgeThing, mockBridgeThing2)); + assertThat(fixture.buildBridgeInfo(), + allOf(containsString("Bridge: TestLabel"), containsString("access possible: false"), + containsString("SHC Generation: Gen-T"), containsString("IP Address: 1.2.3.4"), + containsString("Bridge: Bridge 2"), containsString("access possible: false"), + containsString("SHC Generation: Gen-U"), containsString("IP Address: 11.22.33.44"))); + } + + @Test + void printDeviceInfo() throws InterruptedException { + // no bridge + when(thingRegistry.getAll()).thenReturn(Collections.emptyList()); + assertThat(fixture.buildDeviceInfo(), is("")); + + // One bridge, No device + BridgeHandler mockBridgeHandler = mock(BridgeHandler.class); + Thing mockBridgeThing = mock(Thing.class); + when(mockBridgeThing.getLabel()).thenReturn("TestLabel"); + when(mockBridgeThing.getHandler()).thenReturn(mockBridgeHandler); + when(thingRegistry.getAll()).thenReturn(Collections.singletonList(mockBridgeThing)); + assertThat(fixture.buildDeviceInfo(), allOf(containsString("thing: TestLabel"), containsString("devices (0)"))); + + // One bridge, One UNsupported device + Device mockShcDevice = mock(Device.class); + mockShcDevice.deviceModel = ""; + mockShcDevice.deviceServiceIds = Collections.emptyList(); + when(mockBridgeHandler.getDevices()).thenReturn(List.of(mockShcDevice)); + assertThat(fixture.buildDeviceInfo(), allOf(containsString("thing: TestLabel"), containsString("devices (1)"), + containsString("!UNSUPPORTED!"))); + + // One bridge, One supported device + mockShcDevice.deviceModel = "TWINGUARD"; + mockShcDevice.deviceServiceIds = Collections.emptyList(); + when(mockBridgeHandler.getDevices()).thenReturn(List.of(mockShcDevice)); + assertThat(fixture.buildDeviceInfo(), allOf(containsString("thing: TestLabel"), containsString("devices (1)"), + containsString("TWINGUARD -> twinguard"))); + + // One bridge, One supported device with services + mockShcDevice.deviceModel = "TWINGUARD"; + mockShcDevice.deviceServiceIds = List.of("unknownService", "batterylevel"); + when(mockBridgeHandler.getDevices()).thenReturn(List.of(mockShcDevice)); + assertThat(fixture.buildDeviceInfo(), allOf(containsString("thing: TestLabel"), containsString("devices (1)"), + containsString("TWINGUARD -> twinguard"), containsString("service: unknownService -> !UNSUPPORTED!"), + containsString("batterylevel -> batterylevel"))); + } + + @Test + void printBindingInfo() { + assertThat(fixture.buildBindingInfo(), containsString("Bosch SHC Binding")); + } + + @Test + void printSupportedDevices() { + assertThat(fixture.buildSupportedDeviceStatus(), + allOf(containsString("Supported Devices"), containsString("BBL = boschshc:shutter-control"))); + } + + @Test + void printSupportedServices() { + assertThat(fixture.buildSupportedServiceStatus(), + allOf(containsString("Supported Services"), containsString("airqualitylevel"))); + } + + /** + * The list of services returned by getAllBoschShcServices() shall match + * the implemented services in org.openhab.bindings.boschshc.internal.services. + * Because reflection doesn't return all services classes during runtime + * this test supports consistency between the lists of services and the implemented services. + */ + @Test + void getAllBoschShcServices() throws IOException { + List services = Files + .walk(Paths.get("src/main/java/org/openhab/binding/boschshc/internal/services").toAbsolutePath(), 1) + .filter(Files::isDirectory).map(Path::getFileName).map(Path::toString) + // exclude folders which no service implementation + .filter(name -> !name.equals("dto")).filter(name -> !name.equals("services")).sorted() + .collect(Collectors.toList()); + assertThat(services, is(fixture.getAllBoschShcServices())); + } +}