[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 <mail@holger-friedrich.de>
This commit is contained in:
Holger Friedrich 2023-06-13 17:30:17 +02:00 committed by GitHub
parent db0132409b
commit 5e010db738
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 162 additions and 13 deletions

View File

@ -428,3 +428,8 @@ knx.items:
Switch demoSwitch "Light [%s]" <light> { channel="hue:0210:bridge:1:color", channel="knx:device:bridge:generic:controlSwitch" } Switch demoSwitch "Light [%s]" <light> { channel="hue:0210:bridge:1:color", channel="knx:device:bridge:generic:controlSwitch" }
Dimmer demoDimmer "Dimmer [%d %%]" <light> { channel="hue:0210:bridge:1:color", channel="knx:device:bridge:generic:controlDimmer" } Dimmer demoDimmer "Dimmer [%d %%]" <light> { 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.

View File

@ -29,6 +29,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.dpt.ValueEncoder; import org.openhab.binding.knx.internal.dpt.ValueEncoder;
import org.openhab.binding.knx.internal.handler.GroupAddressListener; 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.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
@ -91,6 +92,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
private final int readRetriesLimit; private final int readRetriesLimit;
private final StatusUpdateCallback statusUpdateCallback; private final StatusUpdateCallback statusUpdateCallback;
private final ScheduledExecutorService knxScheduler; private final ScheduledExecutorService knxScheduler;
private final CommandExtensionData commandExtensionData;
private @Nullable ProcessCommunicator processCommunicator; private @Nullable ProcessCommunicator processCommunicator;
private @Nullable ProcessCommunicationResponder responseCommunicator; 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, 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.autoReconnectPeriod = autoReconnectPeriod;
this.thingUID = thingUID; this.thingUID = thingUID;
this.responseTimeout = responseTimeout; this.responseTimeout = responseTimeout;
@ -145,6 +148,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
this.readRetriesLimit = readRetriesLimit; this.readRetriesLimit = readRetriesLimit;
this.knxScheduler = knxScheduler; this.knxScheduler = knxScheduler;
this.statusUpdateCallback = statusUpdateCallback; this.statusUpdateCallback = statusUpdateCallback;
this.commandExtensionData = commandExtensionData;
} }
public void initialize() { public void initialize() {
@ -323,11 +327,26 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
IndividualAddress source = event.getSourceAddr(); IndividualAddress source = event.getSourceAddr();
byte[] asdu = event.getASDU(); byte[] asdu = event.getASDU();
logger.trace("Received a {} telegram from '{}' to '{}' with value '{}'", task, source, destination, asdu); logger.trace("Received a {} telegram from '{}' to '{}' with value '{}'", task, source, destination, asdu);
boolean isHandled = false;
for (GroupAddressListener listener : groupAddressListeners) { for (GroupAddressListener listener : groupAddressListeners) {
if (listener.listensTo(destination)) { if (listener.listensTo(destination)) {
isHandled = true;
knxScheduler.schedule(() -> action.apply(listener, source, destination, asdu), 0, TimeUnit.SECONDS); 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 // datapoint is null at end of the list, warning is misleading

View File

@ -22,6 +22,7 @@ import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler.CommandExtensionData;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -86,9 +87,10 @@ public class IPClient extends AbstractKNXClient {
@Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod, @Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod,
byte[] secureRoutingBackboneGroupKey, long secureRoutingLatencyToleranceMs, byte[] secureTunnelDevKey, byte[] secureRoutingBackboneGroupKey, long secureRoutingLatencyToleranceMs, byte[] secureTunnelDevKey,
int secureTunnelUser, byte[] secureTunnelUserKey, ThingUID thingUID, int responseTimeout, int readingPause, 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, super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler,
statusUpdateCallback); commandExtensionData, statusUpdateCallback);
this.ipConnectionType = ipConnectionType; this.ipConnectionType = ipConnectionType;
this.ip = ip; this.ip = ip;
this.localSource = localSource; this.localSource = localSource;

View File

@ -20,6 +20,7 @@ import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager; import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.ThingUID; 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, public SerialClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause,
int readRetriesLimit, ScheduledExecutorService knxScheduler, String serialPort, boolean useCemi, 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, super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler,
statusUpdateCallback); commandExtensionData, statusUpdateCallback);
this.serialPortManager = serialPortManager; this.serialPortManager = serialPortManager;
this.serialPort = serialPort; this.serialPort = serialPort;
this.useCemi = useCemi; this.useCemi = useCemi;

View File

@ -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<String, Long> entry : bridgeHandler.getCommandExtensionData().unknownGA().entrySet()) {
console.println(entry.getKey() + " " + entry.getValue());
}
}
return;
}
printUsage(console);
}
@Override
public List<String> 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<String> candidates) {
if (cursorArgumentIndex <= 0) {
return CMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates);
}
return false;
}
}

View File

@ -17,12 +17,14 @@ import static org.openhab.binding.knx.internal.KNXBindingConstants.*;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.client.SerialTransportAdapter; import org.openhab.binding.knx.internal.client.SerialTransportAdapter;
import org.openhab.binding.knx.internal.handler.DeviceThingHandler; import org.openhab.binding.knx.internal.handler.DeviceThingHandler;
import org.openhab.binding.knx.internal.handler.IPBridgeThingHandler; 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.handler.SerialBridgeThingHandler;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider; import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.config.core.Configuration; 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 * @author Simon Kaufmann - Initial contribution and API
*/ */
@NonNullByDefault @NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.knx") @Component(service = { ThingHandlerFactory.class, KNXHandlerFactory.class }, configurationPid = "binding.knx")
public class KNXHandlerFactory extends BaseThingHandlerFactory { public class KNXHandlerFactory extends BaseThingHandlerFactory {
public static final Collection<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_DEVICE, public static final Collection<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_DEVICE,
@ -58,6 +60,7 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory {
@Nullable @Nullable
private final NetworkAddressService networkAddressService; private final NetworkAddressService networkAddressService;
private final SerialPortManager serialPortManager; private final SerialPortManager serialPortManager;
private final Map<ThingUID, KNXBridgeBaseThingHandler> bridges = new ConcurrentHashMap<>();
@Activate @Activate
public KNXHandlerFactory(final @Reference NetworkAddressService networkAddressService, Map<String, Object> config, public KNXHandlerFactory(final @Reference NetworkAddressService networkAddressService, Map<String, Object> config,
@ -99,16 +102,27 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory {
@Override @Override
protected @Nullable ThingHandler createHandler(Thing thing) { protected @Nullable ThingHandler createHandler(Thing thing) {
if (thing.getThingTypeUID().equals(THING_TYPE_IP_BRIDGE)) { ThingTypeUID thingTypeUID = thing.getThingTypeUID();
return new IPBridgeThingHandler((Bridge) thing, networkAddressService); if (thingTypeUID.equals(THING_TYPE_IP_BRIDGE)) {
} else if (thing.getThingTypeUID().equals(THING_TYPE_SERIAL_BRIDGE)) { KNXBridgeBaseThingHandler bridgeHandler = new IPBridgeThingHandler((Bridge) thing, networkAddressService);
return new SerialBridgeThingHandler((Bridge) thing, serialPortManager); bridges.put(thing.getUID(), bridgeHandler);
} else if (thing.getThingTypeUID().equals(THING_TYPE_DEVICE)) { 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 new DeviceThingHandler(thing);
} }
return null; return null;
} }
@Override
public void unregisterHandler(Thing thing) {
bridges.remove(thing.getUID());
super.unregisterHandler(thing);
}
private ThingUID getIPBridgeThingUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID, private ThingUID getIPBridgeThingUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID,
Configuration configuration) { Configuration configuration) {
if (thingUID != null) { if (thingUID != null) {
@ -126,4 +140,8 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory {
String serialPort = (String) configuration.get(SERIAL_PORT); String serialPort = (String) configuration.get(SERIAL_PORT);
return new ThingUID(thingTypeUID, serialPort); return new ThingUID(thingTypeUID, serialPort);
} }
public Collection<KNXBridgeBaseThingHandler> getBridges() {
return Set.copyOf(bridges.values());
}
} }

View File

@ -185,7 +185,8 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
client = new IPClient(ipConnectionType, ip, localSource, port, localEndPoint, useNAT, autoReconnectPeriod, client = new IPClient(ipConnectionType, ip, localSource, port, localEndPoint, useNAT, autoReconnectPeriod,
secureRouting.backboneGroupKey, secureRouting.latencyToleranceMs, secureTunnel.devKey, secureRouting.backboneGroupKey, secureRouting.latencyToleranceMs, secureTunnel.devKey,
secureTunnel.user, secureTunnel.userKey, thing.getUID(), config.getResponseTimeout(), secureTunnel.user, secureTunnel.userKey, thing.getUID(), config.getResponseTimeout(),
config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), this); config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), getCommandExtensionData(),
this);
IPClient tmpClient = client; IPClient tmpClient = client;
if (tmpClient != null) { if (tmpClient != null) {

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.knx.internal.handler; package org.openhab.binding.knx.internal.handler;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -65,20 +67,33 @@ public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implem
public long latencyToleranceMs = 0; public long latencyToleranceMs = 0;
} }
/**
* Helper class to carry information which can be used by the
* command line extension (openHAB console).
*/
public record CommandExtensionData(Map<String, Long> unknownGA) {
}
protected ConcurrentHashMap<IndividualAddress, Destination> destinations = new ConcurrentHashMap<>(); protected ConcurrentHashMap<IndividualAddress, Destination> destinations = new ConcurrentHashMap<>();
private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx"); private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx");
private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor(); private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor();
protected SecureRoutingConfig secureRouting; protected SecureRoutingConfig secureRouting;
protected SecureTunnelConfig secureTunnel; protected SecureTunnelConfig secureTunnel;
private CommandExtensionData commandExtensionData;
public KNXBridgeBaseThingHandler(Bridge bridge) { public KNXBridgeBaseThingHandler(Bridge bridge) {
super(bridge); super(bridge);
secureRouting = new SecureRoutingConfig(); secureRouting = new SecureRoutingConfig();
secureTunnel = new SecureTunnelConfig(); secureTunnel = new SecureTunnelConfig();
commandExtensionData = new CommandExtensionData(new TreeMap<>());
} }
protected abstract KNXClient getClient(); protected abstract KNXClient getClient();
public CommandExtensionData getCommandExtensionData() {
return commandExtensionData;
}
/*** /***
* Initialize KNX secure if configured (full interface) * Initialize KNX secure if configured (full interface)
* *

View File

@ -57,7 +57,7 @@ public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler {
SerialBridgeConfiguration config = getConfigAs(SerialBridgeConfiguration.class); SerialBridgeConfiguration config = getConfigAs(SerialBridgeConfiguration.class);
client = new SerialClient(config.getAutoReconnectPeriod(), thing.getUID(), config.getResponseTimeout(), client = new SerialClient(config.getAutoReconnectPeriod(), thing.getUID(), config.getResponseTimeout(),
config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), config.getSerialPort(), config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), config.getSerialPort(),
config.useCemi(), serialPortManager, this); config.useCemi(), serialPortManager, getCommandExtensionData(), this);
updateStatus(ThingStatus.UNKNOWN); updateStatus(ThingStatus.UNKNOWN);
// delay actual initialization, allow for longer runtime of actual initialization // delay actual initialization, allow for longer runtime of actual initialization