Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[nikohomecontrol] Add console commands #17352

Merged
merged 4 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bundles/org.openhab.binding.nikohomecontrol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ Thing nikohomecontrol:alarm:mybridge:myalarm [ alarmId="abcdef01-dcba-1234-ab98-
| notice | | | | bridge | trigger channel with notice event message, can be used in rules |


## Console Commands

To help with further development, a number of console commands allow you to collect information about your current system:

- `nikohomecontrol controllers`: Lists all controllers in the network and return the controller ID
- `nikohomecontrol systeminfo <controller ID>`: Info about the system
- `nikohomecontrol devicelist <controller ID>`: JSON list of devices with their characteristics in a Niko Home Control II system
- `nikohomecontrol devicelist <controller ID> dump`: Dump system info and device characteristics in a file

## Limitations

The binding has been tested with a Niko Home Control I IP-interface (550-00508) and the Niko Home Control Connected Controller (550-00003) for Niko Home Control I and II, and the Niko Home Control Wireless Smart Hub for Niko Home Control II.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public class NikoHomeControlBindingConstants {
public static final String CHANNEL_NOTICE = "notice";

// Bridge config properties
public static final String CONFIG_CONTROLLER_ID = "controllerId";
public static final String CONFIG_HOST_NAME = "addr";
public static final String CONFIG_PORT = "port";
public static final String CONFIG_REFRESH = "refresh";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
if (BRIDGEII_THING_TYPE.equals(thing.getThingTypeUID())) {
return new NikoHomeControlBridgeHandler2((Bridge) thing, networkAddressService, timeZoneProvider);
} else {
return new NikoHomeControlBridgeHandler1((Bridge) thing, timeZoneProvider);
return new NikoHomeControlBridgeHandler1((Bridge) thing, networkAddressService, timeZoneProvider);
}
} else if (ACTION_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) {
return new NikoHomeControlActionHandler(thing);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/**
* 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.nikohomecontrol.internal.console;

import static org.openhab.binding.nikohomecontrol.internal.NikoHomeControlBindingConstants.BINDING_ID;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlBridgeHandler;
import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlBridgeHandler2;
import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlDiscover;
import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NikoHomeControlCommunication2;
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.net.NetworkAddressService;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingStatus;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;

/**
* The {@link NikoHomeControlCommandExtension} is responsible for handling console commands
*
* @author Mark Herwege - Initial contribution
*/

@NonNullByDefault
@Component(service = ConsoleCommandExtension.class)
public class NikoHomeControlCommandExtension extends AbstractConsoleCommandExtension
implements ConsoleCommandCompleter {

private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();

private static final String CONTROLLERS = "controllers";
private static final String SYSTEMINFO = "systeminfo";
private static final String DEVICELIST = "devicelist";
private static final String DUMP = "dump";

private static final String ROOT_PATH = System.getProperty("user.home") + File.separator + BINDING_ID
+ File.separator + DEVICELIST;

private static final StringsCompleter CMD_COMPLETER = new StringsCompleter(
List.of(CONTROLLERS, SYSTEMINFO, DEVICELIST), false);
private static final StringsCompleter DUMP_COMPLETER = new StringsCompleter(List.of(DUMP), false);

private final ThingRegistry thingRegistry;
private final NetworkAddressService networkAddressService;

private List<NikoHomeControlBridgeHandler> bridgeHandlers = List.of();

@Activate
public NikoHomeControlCommandExtension(final @Reference ThingRegistry thingRegistry,
final @Reference NetworkAddressService networkAddressService) {
super("nikohomecontrol", "Interact with the Niko Home Control binding");
this.thingRegistry = thingRegistry;
this.networkAddressService = networkAddressService;
}

@Override
public void execute(String[] args, Console console) {
if ((args.length < 1) || (args.length > 3)) {
console.println("Invalid number of arguments");
printUsage(console);
return;
}

bridgeHandlers = thingRegistry.getAll().stream()
.filter(t -> t.getHandler() instanceof NikoHomeControlBridgeHandler)
.map(b -> ((NikoHomeControlBridgeHandler) b.getHandler())).toList();
Map<String, String> bridgeNhcVersion = bridgeHandlers.stream().collect(Collectors
.toMap(b -> b.getControllerId(), b -> b instanceof NikoHomeControlBridgeHandler2 ? "II" : "I"));

NikoHomeControlBridgeHandler bridgeHandler = null;
if (args.length > 1) {
Optional<NikoHomeControlBridgeHandler> bridgeOptional = bridgeHandlers.stream()
.filter(b -> b.getControllerId().toLowerCase().equals(args[1].toLowerCase())).findAny();
if (bridgeOptional.isEmpty()) {
console.println("'" + args[1] + "' is not a valid controller ID");
printUsage(console);
return;
}
bridgeHandler = bridgeOptional.get();
}

switch (args[0].toLowerCase()) {
case CONTROLLERS:
if (args.length > 1) {
console.println("No extra argument allowed after 'controllers'");
printUsage(console);
return;
} else {
Map<String, String> bridgeIds = bridgeHandlers.stream().collect(Collectors
.toMap(b -> b.getThing().getUID().toString(), b -> b.getControllerId().toLowerCase()));
List<String> controllerIds = List.of();
Map<String, String> controllerNhcVersion = Map.of();
try {
String broadcastAddr = networkAddressService.getConfiguredBroadcastAddress();
if (broadcastAddr == null) {
console.println(
"Controller discovery not possible, no broadcast address found, result only contains bridges");
} else {
NikoHomeControlDiscover nhcDiscover;
nhcDiscover = new NikoHomeControlDiscover(broadcastAddr);
controllerIds = nhcDiscover.getNhcBridgeIds().stream().map(String::toLowerCase)
.filter(id -> !bridgeIds.containsValue(id)).toList();
controllerNhcVersion = controllerIds.stream().collect(
Collectors.toMap(Function.identity(), id -> nhcDiscover.isNhcII(id) ? "II" : "I"));
}
} catch (IOException e) {
console.println(
"Controller discovery not possible, network error, result only contains bridges");
}
Map<String, String> nhcVersion = Map.copyOf(controllerNhcVersion);
console.printf("%-14s %-12s %s%n", "Controller-ID", "NHC-Version", "Bridge-ID");
console.printf("%-14s %-12s %s%n", "-------------", "-----------", "---------");
bridgeIds.forEach(
(bridge, id) -> console.printf("%-14s %-12s %s%n", id, bridgeNhcVersion.get(id), bridge));
controllerIds.forEach(id -> console.printf("%-14s %-12s %s%n", id, nhcVersion.get(id), " "));
}
break;
case SYSTEMINFO:
if (args.length < 2) {
console.println("No controller ID provided");
printUsage(console);
return;
}
if (bridgeHandler != null) {
if (!ThingStatus.ONLINE.equals(bridgeHandler.getThing().getStatus())) {
console.println("Niko Home Control bridge not online, system info may be out of date");
}
console.println("Property Value");
console.println("-------- -----");
Map<String, String> properties = bridgeHandler.getThing().getProperties();
for (String key : properties.keySet()) {
console.printf("%-28.28s %s%n", key, properties.get(key));
}
}
break;
case DEVICELIST:
if (args.length < 2) {
console.println("No controller ID provided");
printUsage(console);
return;
}
if (!"II".equals(bridgeNhcVersion.get(args[1]))) {
console.println("'" + args[1] + "' is not a Niko Home Control II bridge");
printUsage(console);
return;
}
if (bridgeHandler != null) {
if (!ThingStatus.ONLINE.equals(bridgeHandler.getThing().getStatus())) {
console.println("Niko Home Control bridge not online, device list may be out of date");
}
NikoHomeControlCommunication2 nhcComm = (NikoHomeControlCommunication2) bridgeHandler
.getCommunication();
if (nhcComm != null) {
String devices = prettyJson(nhcComm.getRawDevicesListResponse());
if (args.length == 2) {
console.println(devices);
} else if (DUMP.equals(args[2])) {
String filename = ROOT_PATH + LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE)
+ ".json";
String systeminfo = GSON.toJson(bridgeHandler.getThing().getProperties());
writeJsonToFile(filename, systeminfo, console);
writeJsonToFile(filename, devices, console);
console.printf("System info and device list dumped to file '%s'%n", filename);
} else {
console.println("Command argument '" + args[2] + "' not recognized");
printUsage(console);
return;
}
}
}
break;
default:
console.println("Command argument '" + args[0] + "' not recognized");
printUsage(console);
}
}

private String prettyJson(String json) {
try {
return GSON.toJson(JsonParser.parseString(json));
} catch (JsonSyntaxException e) {
// Keep the unformatted json if there is a syntax exception
return json;
}
}

private void writeJsonToFile(String filename, String json, Console console) {
try {
JsonElement element = JsonParser.parseString(json);
if (element.isJsonNull() || (element.isJsonArray() && ((JsonArray) element).size() == 0)) {
console.println("Empty device list, nothing to dump");
return;
}
} catch (JsonSyntaxException e) {
// Just continue and write the file with non-valid json anyway
}

// ensure full path exists
File file = new File(filename);
File parentFile = file.getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
}

final byte[] contents = (json + "\n").getBytes(StandardCharsets.UTF_8);
try {
Files.write(file.toPath(), contents, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
console.println("I/O error writing device list to file");
}
}

@Override
public List<String> getUsages() {
return Arrays.asList(new String[] { buildCommandUsage(CONTROLLERS, "list all Niko Home Control Controllers"),
buildCommandUsage(SYSTEMINFO + " <controller ID>",
"show system info for Controller with controller ID"),
buildCommandUsage(DEVICELIST + " <controller ID>",
"create device list of installation on Controller with controller ID"),
buildCommandUsage(DEVICELIST + " <controller ID> " + DUMP,
"dump device list of installation with controller ID to file") });
}

@Override
public @Nullable ConsoleCommandCompleter getCompleter() {
return this;
}

@Override
public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List<String> candidates) {
if (cursorArgumentIndex <= 0) {
return CMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
} else if (cursorArgumentIndex == 1) {
return new StringsCompleter(bridgeHandlers.stream().filter(b -> b instanceof NikoHomeControlBridgeHandler2)
.map(b -> b.getControllerId()).toList(), false)
.complete(args, cursorArgumentIndex, cursorPosition, candidates);
} else if (cursorArgumentIndex == 2) {
return DUMP_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ private void addNhcIBridge(InetAddress addr, String bridgeId) {
ThingUID uid = new ThingUID(BINDING_ID, "bridge", bridgeId);

DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withLabel(bridgeName)
.withProperty(CONFIG_HOST_NAME, addr.getHostAddress()).withRepresentationProperty(CONFIG_HOST_NAME)
.build();
.withProperty(CONFIG_HOST_NAME, addr.getHostAddress()).withProperty(CONFIG_CONTROLLER_ID, bridgeId)
.withRepresentationProperty(CONFIG_CONTROLLER_ID).build();
thingDiscovered(discoveryResult);
}

Expand All @@ -104,8 +104,8 @@ private void addNhcIIBridge(InetAddress addr, String bridgeId) {
ThingUID uid = new ThingUID(BINDING_ID, "bridge2", bridgeId);

DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(uid).withLabel(bridgeName)
.withProperty(CONFIG_HOST_NAME, addr.getHostAddress()).withRepresentationProperty(CONFIG_HOST_NAME)
.build();
.withProperty(CONFIG_HOST_NAME, addr.getHostAddress()).withProperty(CONFIG_CONTROLLER_ID, bridgeId)
.withRepresentationProperty(CONFIG_CONTROLLER_ID).build();
thingDiscovered(discoveryResult);
}

Expand Down
Loading