Skip to content

Commit

Permalink
[knx] Implement console commands (openhab#15064)
Browse files Browse the repository at this point in the history
* [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 <[email protected]>
Signed-off-by: Thomas Burri <[email protected]>
  • Loading branch information
holgerfriedrich authored and tburri-alstom committed Jun 19, 2023
1 parent 833dad3 commit c77a58a
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 13 deletions.
5 changes: 5 additions & 0 deletions bundles/org.openhab.binding.knx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,8 @@ knx.items:
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" }
```

## 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.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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;
Expand Down Expand Up @@ -91,6 +92,7 @@ public enum ClientState {
private final int readRetriesLimit;
private final StatusUpdateCallback statusUpdateCallback;
private final ScheduledExecutorService knxScheduler;
private final CommandExtensionData commandExtensionData;

private @Nullable ProcessCommunicator processCommunicator;
private @Nullable ProcessCommunicationResponder responseCommunicator;
Expand Down Expand Up @@ -137,14 +139,16 @@ public void groupReadResponse(ProcessEvent e) {
};

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;
this.readingPause = readingPause;
this.readRetriesLimit = readRetriesLimit;
this.knxScheduler = knxScheduler;
this.statusUpdateCallback = statusUpdateCallback;
this.commandExtensionData = commandExtensionData;
}

public void initialize() {
Expand Down Expand Up @@ -323,11 +327,26 @@ private void processEvent(String task, ProcessEvent event, ListenerNotification
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

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;
Expand Down Expand Up @@ -86,9 +87,10 @@ public IPClient(IpConnectionType ipConnectionType, String ip, String localSource
@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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
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;
Expand All @@ -49,7 +51,7 @@
* @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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_DEVICE,
Expand All @@ -58,6 +60,7 @@ public class KNXHandlerFactory extends BaseThingHandlerFactory {
@Nullable
private final NetworkAddressService networkAddressService;
private final SerialPortManager serialPortManager;
private final Map<ThingUID, KNXBridgeBaseThingHandler> bridges = new ConcurrentHashMap<>();

@Activate
public KNXHandlerFactory(final @Reference NetworkAddressService networkAddressService, Map<String, Object> config,
Expand Down Expand Up @@ -99,16 +102,27 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {

@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) {
Expand All @@ -126,4 +140,8 @@ private ThingUID getSerialBridgeThingUID(ThingTypeUID thingTypeUID, @Nullable Th
String serialPort = (String) configuration.get(SERIAL_PORT);
return new ThingUID(thingTypeUID, serialPort);
}

public Collection<KNXBridgeBaseThingHandler> getBridges() {
return Set.copyOf(bridges.values());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ public void initializeLater() {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,20 +67,33 @@ public SecureRoutingConfig() {
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<>();
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)
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public void initialize() {
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
Expand Down

0 comments on commit c77a58a

Please sign in to comment.