[nikohomecontrol] Add console commands (#17352)

* dump devices from console

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Mark Herwege 2024-09-12 15:14:22 +02:00 committed by Ciprian Pascu
parent a4d083281a
commit a815d684dd
10 changed files with 361 additions and 17 deletions

View File

@ -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.

View File

@ -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";

View File

@ -68,7 +68,7 @@ public class NikoHomeControlHandlerFactory extends BaseThingHandlerFactory {
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);

View File

@ -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;
}
}

View File

@ -92,8 +92,8 @@ public class NikoHomeControlBridgeDiscoveryService extends AbstractDiscoveryServ
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);
}
@ -104,8 +104,8 @@ public class NikoHomeControlBridgeDiscoveryService extends AbstractDiscoveryServ
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);
}

View File

@ -14,6 +14,7 @@ package org.openhab.binding.nikohomecontrol.internal.handler;
import static org.openhab.binding.nikohomecontrol.internal.NikoHomeControlBindingConstants.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.ZoneId;
@ -29,8 +30,10 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nikohomecontrol.internal.discovery.NikoHomeControlDiscoveryService;
import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent;
import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlDiscover;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
@ -57,10 +60,14 @@ public abstract class NikoHomeControlBridgeHandler extends BaseBridgeHandler imp
private volatile @Nullable ScheduledFuture<?> refreshTimer;
protected final NetworkAddressService networkAddressService;
protected final TimeZoneProvider timeZoneProvider;
public NikoHomeControlBridgeHandler(Bridge nikoHomeControlBridge, TimeZoneProvider timeZoneProvider) {
public NikoHomeControlBridgeHandler(Bridge nikoHomeControlBridge, NetworkAddressService networkAddressService,
TimeZoneProvider timeZoneProvider) {
super(nikoHomeControlBridge);
this.networkAddressService = networkAddressService;
this.timeZoneProvider = timeZoneProvider;
}
@ -268,4 +275,31 @@ public abstract class NikoHomeControlBridgeHandler extends BaseBridgeHandler imp
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(NikoHomeControlDiscoveryService.class);
}
public String getControllerId() {
String id = thing.getProperties().get(CONFIG_CONTROLLER_ID);
if (id != null) {
return id;
}
try {
id = "";
String broadcastAddr = networkAddressService.getConfiguredBroadcastAddress();
if (broadcastAddr != null) {
NikoHomeControlDiscover nhcDiscover = new NikoHomeControlDiscover(broadcastAddr);
InetAddress address = getAddr();
if (address != null) {
id = nhcDiscover.getBridgeId(address);
id = id != null ? id : "";
}
}
} catch (IOException e) {
id = "";
}
if (!id.isEmpty()) {
thing.setProperty(CONFIG_CONTROLLER_ID, id);
} else {
logger.warn("failure setting controller ID property");
}
return id;
}
}

View File

@ -21,6 +21,7 @@ import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nikohomecontrol.internal.protocol.nhc1.NikoHomeControlCommunication1;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
@ -38,14 +39,17 @@ public class NikoHomeControlBridgeHandler1 extends NikoHomeControlBridgeHandler
private final Logger logger = LoggerFactory.getLogger(NikoHomeControlBridgeHandler1.class);
public NikoHomeControlBridgeHandler1(Bridge nikoHomeControlBridge, TimeZoneProvider timeZoneProvider) {
super(nikoHomeControlBridge, timeZoneProvider);
public NikoHomeControlBridgeHandler1(Bridge nikoHomeControlBridge, NetworkAddressService networkAddressService,
TimeZoneProvider timeZoneProvider) {
super(nikoHomeControlBridge, networkAddressService, timeZoneProvider);
}
@Override
public void initialize() {
logger.debug("initializing bridge handler");
scheduler.submit(() -> getControllerId());
InetAddress addr = getAddr();
int port = getPort();
@ -63,7 +67,7 @@ public class NikoHomeControlBridgeHandler1 extends NikoHomeControlBridgeHandler
@Override
protected void updateProperties() {
Map<String, String> properties = new HashMap<>();
Map<String, String> properties = new HashMap<>(thing.getProperties());
NikoHomeControlCommunication1 comm = (NikoHomeControlCommunication1) nhcComm;
if (comm != null) {

View File

@ -49,18 +49,17 @@ public class NikoHomeControlBridgeHandler2 extends NikoHomeControlBridgeHandler
private final Gson gson = new GsonBuilder().create();
NetworkAddressService networkAddressService;
public NikoHomeControlBridgeHandler2(Bridge nikoHomeControlBridge, NetworkAddressService networkAddressService,
TimeZoneProvider timeZoneProvider) {
super(nikoHomeControlBridge, timeZoneProvider);
this.networkAddressService = networkAddressService;
super(nikoHomeControlBridge, networkAddressService, timeZoneProvider);
}
@Override
public void initialize() {
logger.debug("initializing NHC II bridge handler");
scheduler.submit(() -> getControllerId());
Date expiryDate = getTokenExpiryDate();
if (expiryDate == null) {
if (getToken().isEmpty()) {
@ -98,7 +97,7 @@ public class NikoHomeControlBridgeHandler2 extends NikoHomeControlBridgeHandler
@Override
protected void updateProperties() {
Map<String, String> properties = new HashMap<>();
Map<String, String> properties = new HashMap<>(thing.getProperties());
NikoHomeControlCommunication2 comm = (NikoHomeControlCommunication2) nhcComm;
if (comm != null) {

View File

@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -50,7 +51,7 @@ public final class NikoHomeControlDiscover {
private final Logger logger = LoggerFactory.getLogger(NikoHomeControlDiscover.class);
private List<String> nhcBridgeIds = new ArrayList<>();
private Map<String, InetAddress> inetAdresses = new HashMap<>();
private Map<String, InetAddress> inetAddresses = new HashMap<>();
private Map<String, Boolean> isNhcII = new HashMap<>();
/**
@ -104,7 +105,16 @@ public final class NikoHomeControlDiscover {
* @return the addr, null if not in the list of discovered bridgeId's
*/
public @Nullable InetAddress getAddr(String bridgeId) {
return inetAdresses.get(bridgeId);
return inetAddresses.get(bridgeId);
}
/**
* @param inetAddress inetAddress of the controller
* @return the bridgeId, null if not found
*/
public @Nullable String getBridgeId(InetAddress inetAddress) {
return inetAddresses.entrySet().stream().filter(entry -> inetAddress.equals(entry.getValue()))
.map(Entry::getKey).findAny().get();
}
/**
@ -168,7 +178,7 @@ public final class NikoHomeControlDiscover {
*/
private InetAddress setAddr(String bridgeId, DatagramPacket packet) {
InetAddress address = packet.getAddress();
inetAdresses.put(bridgeId, address);
inetAddresses.put(bridgeId, address);
return address;
}

View File

@ -85,6 +85,8 @@ public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication
private volatile String profile = "";
private String rawDevicesListResponse = "";
private volatile @Nullable NhcSystemInfo2 nhcSystemInfo;
private volatile @Nullable NhcTimeInfo2 nhcTimeInfo;
@ -270,6 +272,7 @@ public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication
}
private void devicesListRsp(String response) {
rawDevicesListResponse = response;
Type messageType = new TypeToken<NhcMessage2>() {
}.getType();
List<NhcDevice2> deviceList = null;
@ -1278,6 +1281,10 @@ public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication
return services.stream().map(NhcService2::name).collect(Collectors.joining(", "));
}
public String getRawDevicesListResponse() {
return rawDevicesListResponse;
}
@Override
public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) {
// do in separate thread as this method needs to return early