Skip to content

Commit

Permalink
[boschshc] Add command to list SHC device mappings (openhab#15060)
Browse files Browse the repository at this point in the history
* [boschshc] add command to list Bosch Smart Home Controller devices and mapping to openhab devices and related services

Signed-off-by: Gerd Zanker <gerd.zanker@web.de>
Signed-off-by: Jørgen Austvik <jaustvik@acm.org>
  • Loading branch information
GerdZanker authored and austvik committed Mar 27, 2024
1 parent 9a48113 commit 8de141b
Show file tree
Hide file tree
Showing 10 changed files with 555 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -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<String> 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<BridgeHandler> getBridgeHandlers() {
List<BridgeHandler> 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<BridgeHandler> 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<Device> 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<String> deviceServiceIds) {
StringBuilder builder = new StringBuilder();
List<String> 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<String, ThingTypeUID> 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<String> 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<String> 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<String> candidates) {
if (cursorArgumentIndex <= 0) {
return SUBCMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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;
Expand Down Expand Up @@ -432,6 +433,23 @@ public List<Room> getRooms() throws InterruptedException {
}
}

/**
* 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;
Expand Down Expand Up @@ -604,7 +622,7 @@ public Device getDeviceInfo(String deviceId)
@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));
Expand Down Expand Up @@ -634,7 +652,7 @@ public UserDefinedState getUserStateInfo(String stateId)
@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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class Device {
public String status;
public List<String> childDeviceIds;

public static Boolean isValid(Device obj) {
public static boolean isValid(Device obj) {
return obj != null && obj.id != null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,10 @@ public PublicInformation() {
public List<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,13 @@ public static Scenario createScenario(final String id, final String name, final
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
+ "'" + '}';
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public String getJsonrpc() {
return this.jsonrpc;
}

public static Boolean isValid(SubscribeResult obj) {
public static boolean isValid(SubscribeResult obj) {
return obj != null && obj.result != null && obj.jsonrpc != null;
}
}

0 comments on commit 8de141b

Please sign in to comment.