From 5e010db738bda0d3b5722783784cc7a05500d8cf Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Tue, 13 Jun 2023 17:30:17 +0200 Subject: [PATCH] [knx] Implement console commands (#15064) * [knx] Implement console commands Add commands knx:* to openHAB console. Initial implementation of knx:show_unknown_ga, which shows group addresses which are seen in the installation but not used by openHAB. Signed-off-by: Holger Friedrich --- bundles/org.openhab.binding.knx/README.md | 5 ++ .../internal/client/AbstractKNXClient.java | 21 ++++- .../binding/knx/internal/client/IPClient.java | 6 +- .../knx/internal/client/SerialClient.java | 6 +- .../internal/console/KNXCommandExtension.java | 87 +++++++++++++++++++ .../internal/factory/KNXHandlerFactory.java | 30 +++++-- .../handler/IPBridgeThingHandler.java | 3 +- .../handler/KNXBridgeBaseThingHandler.java | 15 ++++ .../handler/SerialBridgeThingHandler.java | 2 +- 9 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/console/KNXCommandExtension.java diff --git a/bundles/org.openhab.binding.knx/README.md b/bundles/org.openhab.binding.knx/README.md index 3b5348d5fb..87c23942b2 100644 --- a/bundles/org.openhab.binding.knx/README.md +++ b/bundles/org.openhab.binding.knx/README.md @@ -428,3 +428,8 @@ knx.items: Switch demoSwitch "Light [%s]" { channel="hue:0210:bridge:1:color", channel="knx:device:bridge:generic:controlSwitch" } Dimmer demoDimmer "Dimmer [%d %%]" { channel="hue:0210:bridge:1:color", channel="knx:device:bridge:generic:controlDimmer" } ``` + +## Console Commands + +The KNX binding provides additional functionality which can be triggered from the openHAB console. +Type `openhab:knx` on the openHAB console for further information. diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java index 0c88798361..732c88520d 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java @@ -29,6 +29,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.dpt.ValueEncoder; import org.openhab.binding.knx.internal.handler.GroupAddressListener; +import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler.CommandExtensionData; import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; @@ -91,6 +92,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien private final int readRetriesLimit; private final StatusUpdateCallback statusUpdateCallback; private final ScheduledExecutorService knxScheduler; + private final CommandExtensionData commandExtensionData; private @Nullable ProcessCommunicator processCommunicator; private @Nullable ProcessCommunicationResponder responseCommunicator; @@ -137,7 +139,8 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien }; public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause, - int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback) { + int readRetriesLimit, ScheduledExecutorService knxScheduler, CommandExtensionData commandExtensionData, + StatusUpdateCallback statusUpdateCallback) { this.autoReconnectPeriod = autoReconnectPeriod; this.thingUID = thingUID; this.responseTimeout = responseTimeout; @@ -145,6 +148,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien this.readRetriesLimit = readRetriesLimit; this.knxScheduler = knxScheduler; this.statusUpdateCallback = statusUpdateCallback; + this.commandExtensionData = commandExtensionData; } public void initialize() { @@ -323,11 +327,26 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien IndividualAddress source = event.getSourceAddr(); byte[] asdu = event.getASDU(); logger.trace("Received a {} telegram from '{}' to '{}' with value '{}'", task, source, destination, asdu); + boolean isHandled = false; for (GroupAddressListener listener : groupAddressListeners) { if (listener.listensTo(destination)) { + isHandled = true; knxScheduler.schedule(() -> action.apply(listener, source, destination, asdu), 0, TimeUnit.SECONDS); } } + // Store information about unhandled GAs, can be shown on console using knx:list-unknown-ga. + // The idea is to store GA, message type, and size as key. The value counts the number of packets. + if (!isHandled) { + logger.trace("Address '{}' is not configured in openHAB", destination); + final String type = switch (event.getServiceCode()) { + case 0x80 -> " GROUP_WRITE("; + case 0x40 -> " GROUP_RESPONSE("; + case 0x00 -> " GROUP_READ("; + default -> " ?("; + }; + final String key = destination.toString() + type + event.getASDU().length + ")"; + commandExtensionData.unknownGA().compute(key, (k, v) -> v == null ? 1 : v + 1); + } } // datapoint is null at end of the list, warning is misleading diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java index f9dbad6971..648f66c1ab 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java @@ -22,6 +22,7 @@ import java.util.concurrent.ScheduledExecutorService; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler.CommandExtensionData; import org.openhab.core.thing.ThingUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,9 +87,10 @@ public class IPClient extends AbstractKNXClient { @Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod, byte[] secureRoutingBackboneGroupKey, long secureRoutingLatencyToleranceMs, byte[] secureTunnelDevKey, int secureTunnelUser, byte[] secureTunnelUserKey, ThingUID thingUID, int responseTimeout, int readingPause, - int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback) { + int readRetriesLimit, ScheduledExecutorService knxScheduler, CommandExtensionData commandExtensionData, + StatusUpdateCallback statusUpdateCallback) { super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler, - statusUpdateCallback); + commandExtensionData, statusUpdateCallback); this.ipConnectionType = ipConnectionType; this.ip = ip; this.localSource = localSource; diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/SerialClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/SerialClient.java index 20e269f830..bf1db21b64 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/SerialClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/SerialClient.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler.CommandExtensionData; import org.openhab.core.io.transport.serial.SerialPortIdentifier; import org.openhab.core.io.transport.serial.SerialPortManager; import org.openhab.core.thing.ThingUID; @@ -52,9 +53,10 @@ public class SerialClient extends AbstractKNXClient { public SerialClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause, int readRetriesLimit, ScheduledExecutorService knxScheduler, String serialPort, boolean useCemi, - SerialPortManager serialPortManager, StatusUpdateCallback statusUpdateCallback) { + SerialPortManager serialPortManager, CommandExtensionData commandExtensionData, + StatusUpdateCallback statusUpdateCallback) { super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler, - statusUpdateCallback); + commandExtensionData, statusUpdateCallback); this.serialPortManager = serialPortManager; this.serialPort = serialPort; this.useCemi = useCemi; diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/console/KNXCommandExtension.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/console/KNXCommandExtension.java new file mode 100644 index 0000000000..a365f489ca --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/console/KNXCommandExtension.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2023 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.knx.internal.console; + +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.knx.internal.KNXBindingConstants; +import org.openhab.binding.knx.internal.factory.KNXHandlerFactory; +import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler; +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.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link KNXCommandExtension} is responsible for handling console commands + * + * @author Holger Friedrich - Initial contribution + */ +@NonNullByDefault +@Component(service = ConsoleCommandExtension.class) +public class KNXCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter { + + private static final String CMD_LIST_UNKNOWN_GA = "list-unknown-ga"; + private static final StringsCompleter CMD_COMPLETER = new StringsCompleter(List.of(CMD_LIST_UNKNOWN_GA), false); + + private final KNXHandlerFactory knxHandlerFactory; + + @Activate + public KNXCommandExtension(final @Reference KNXHandlerFactory knxHandlerFactory) { + super(KNXBindingConstants.BINDING_ID, "Interact with KNX devices."); + this.knxHandlerFactory = knxHandlerFactory; + } + + @Override + public void execute(String[] args, Console console) { + if (args.length == 1 && CMD_LIST_UNKNOWN_GA.equalsIgnoreCase(args[0])) { + for (KNXBridgeBaseThingHandler bridgeHandler : knxHandlerFactory.getBridges()) { + console.println("KNX bridge \"" + bridgeHandler.getThing().getLabel() + + "\": group address, type, number of bytes, and number of occurence since last reload of binding:"); + // console.println(handler.getCommandExtensionData().unknownGA().toString()); + for (Entry entry : bridgeHandler.getCommandExtensionData().unknownGA().entrySet()) { + console.println(entry.getKey() + " " + entry.getValue()); + } + } + return; + } + printUsage(console); + } + + @Override + public List getUsages() { + return Arrays.asList( + buildCommandUsage(CMD_LIST_UNKNOWN_GA, "list group addresses which are not configured in openHAB")); + } + + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return this; + } + + @Override + public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) { + if (cursorArgumentIndex <= 0) { + return CMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); + } + return false; + } +} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java index b5265982c9..bc9a288ebd 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/factory/KNXHandlerFactory.java @@ -17,12 +17,14 @@ import static org.openhab.binding.knx.internal.KNXBindingConstants.*; import java.util.Collection; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.client.SerialTransportAdapter; import org.openhab.binding.knx.internal.handler.DeviceThingHandler; import org.openhab.binding.knx.internal.handler.IPBridgeThingHandler; +import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler; import org.openhab.binding.knx.internal.handler.SerialBridgeThingHandler; import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider; import org.openhab.core.config.core.Configuration; @@ -49,7 +51,7 @@ import org.osgi.service.component.annotations.Reference; * @author Simon Kaufmann - Initial contribution and API */ @NonNullByDefault -@Component(service = ThingHandlerFactory.class, configurationPid = "binding.knx") +@Component(service = { ThingHandlerFactory.class, KNXHandlerFactory.class }, configurationPid = "binding.knx") public class KNXHandlerFactory extends BaseThingHandlerFactory { public static final Collection SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_DEVICE, @@ -58,6 +60,7 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory { @Nullable private final NetworkAddressService networkAddressService; private final SerialPortManager serialPortManager; + private final Map bridges = new ConcurrentHashMap<>(); @Activate public KNXHandlerFactory(final @Reference NetworkAddressService networkAddressService, Map config, @@ -99,16 +102,27 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory { @Override protected @Nullable ThingHandler createHandler(Thing thing) { - if (thing.getThingTypeUID().equals(THING_TYPE_IP_BRIDGE)) { - return new IPBridgeThingHandler((Bridge) thing, networkAddressService); - } else if (thing.getThingTypeUID().equals(THING_TYPE_SERIAL_BRIDGE)) { - return new SerialBridgeThingHandler((Bridge) thing, serialPortManager); - } else if (thing.getThingTypeUID().equals(THING_TYPE_DEVICE)) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (thingTypeUID.equals(THING_TYPE_IP_BRIDGE)) { + KNXBridgeBaseThingHandler bridgeHandler = new IPBridgeThingHandler((Bridge) thing, networkAddressService); + bridges.put(thing.getUID(), bridgeHandler); + return bridgeHandler; + } else if (thingTypeUID.equals(THING_TYPE_SERIAL_BRIDGE)) { + KNXBridgeBaseThingHandler bridgeHandler = new SerialBridgeThingHandler((Bridge) thing, serialPortManager); + bridges.put(thing.getUID(), bridgeHandler); + return bridgeHandler; + } else if (thingTypeUID.equals(THING_TYPE_DEVICE)) { return new DeviceThingHandler(thing); } return null; } + @Override + public void unregisterHandler(Thing thing) { + bridges.remove(thing.getUID()); + super.unregisterHandler(thing); + } + private ThingUID getIPBridgeThingUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID, Configuration configuration) { if (thingUID != null) { @@ -126,4 +140,8 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory { String serialPort = (String) configuration.get(SERIAL_PORT); return new ThingUID(thingTypeUID, serialPort); } + + public Collection getBridges() { + return Set.copyOf(bridges.values()); + } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java index dbd2f1dfde..dcf6ad80fd 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java @@ -185,7 +185,8 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler { client = new IPClient(ipConnectionType, ip, localSource, port, localEndPoint, useNAT, autoReconnectPeriod, secureRouting.backboneGroupKey, secureRouting.latencyToleranceMs, secureTunnel.devKey, secureTunnel.user, secureTunnel.userKey, thing.getUID(), config.getResponseTimeout(), - config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), this); + config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), getCommandExtensionData(), + this); IPClient tmpClient = client; if (tmpClient != null) { diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java index c669248db0..eda5d51e32 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.knx.internal.handler; +import java.util.Map; +import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -65,20 +67,33 @@ public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implem public long latencyToleranceMs = 0; } + /** + * Helper class to carry information which can be used by the + * command line extension (openHAB console). + */ + public record CommandExtensionData(Map unknownGA) { + } + protected ConcurrentHashMap destinations = new ConcurrentHashMap<>(); private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx"); private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor(); protected SecureRoutingConfig secureRouting; protected SecureTunnelConfig secureTunnel; + private CommandExtensionData commandExtensionData; public KNXBridgeBaseThingHandler(Bridge bridge) { super(bridge); secureRouting = new SecureRoutingConfig(); secureTunnel = new SecureTunnelConfig(); + commandExtensionData = new CommandExtensionData(new TreeMap<>()); } protected abstract KNXClient getClient(); + public CommandExtensionData getCommandExtensionData() { + return commandExtensionData; + } + /*** * Initialize KNX secure if configured (full interface) * diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java index 0c66934ac8..bcd776abf0 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java @@ -57,7 +57,7 @@ public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler { SerialBridgeConfiguration config = getConfigAs(SerialBridgeConfiguration.class); client = new SerialClient(config.getAutoReconnectPeriod(), thing.getUID(), config.getResponseTimeout(), config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), config.getSerialPort(), - config.useCemi(), serialPortManager, this); + config.useCemi(), serialPortManager, getCommandExtensionData(), this); updateStatus(ThingStatus.UNKNOWN); // delay actual initialization, allow for longer runtime of actual initialization -- 2.47.3