diff --git a/bundles/org.openhab.binding.nikobus/README.md b/bundles/org.openhab.binding.nikobus/README.md index 48aa00657..c23fe401f 100644 --- a/bundles/org.openhab.binding.nikobus/README.md +++ b/bundles/org.openhab.binding.nikobus/README.md @@ -33,10 +33,6 @@ The bridge enables communication with other Nikobus components: * `rollershutter-module` - Nikobus roller shutter module, * `push-button` - Nikobus physical push button. -## Discovery - -The binding does not support any automatic discovery of Things. - ## Bridge Configuration The binding can connect to the PC-Link via serial interface. @@ -174,6 +170,84 @@ Thing push-button pb1 [ address = "28092A", impactedModules = "switch-module:s1: In addition to the status requests triggered by button presses, there is also a scheduled status update interval defined by the `refreshInterval` parameter and explained above. +## Discovery + +Pressing a physical Nikobus push-button will generate a new inbox entry with an exception of buttons already discovered or setup. + +Nikobus push buttons have the following format in inbox: + +``` +Nikobus Push Button 14E7F4:3 +4BF9CA +nikobus:push-button +``` + +where first line contains name of the discovered button and second one contains button's bus address. + +Each discovered button has a Nikobus address appended to its name, same as can be seen in Nikobus's PC application, `14E7F4:3` in above example. + + * `14E7F4` - address of the Nikobus switch, as can be seen in Nikobus PC software and + * `3` - represents a button on Nikobus switch. + +### Button mappings + +##### 2 buttons switch + +![Nikobus Switch with 2 buttons](doc/s2.png) + +``` + 1 = A + 2 = B + ``` + +##### 4 buttons switch + +![Nikobus Switch with 4 buttons](doc/s4.png) + +maps as + +``` + 3 1 + 4 2 +``` + +so + +``` +1 = C +2 = D +3 = A +4 = B +``` + +##### 8 buttons switch + +![Nikobus Switch with 8 buttons](doc/s8.png) + +maps as + +``` + 7 5 3 1 + 8 6 4 2 +``` + +so + +``` +1 = 2C +2 = 2D +3 = 2A +4 = 2B +5 = 1C +6 = 1D +7 = 1A +8 = 1B +``` + +Above example `14E7F4:3` would give: +* for 4 buttons switch - push button A, +* for 8 buttons switch - push button 2A. + ## Full Example ### nikobus.things diff --git a/bundles/org.openhab.binding.nikobus/doc/s2.png b/bundles/org.openhab.binding.nikobus/doc/s2.png new file mode 100644 index 000000000..26f003a16 Binary files /dev/null and b/bundles/org.openhab.binding.nikobus/doc/s2.png differ diff --git a/bundles/org.openhab.binding.nikobus/doc/s4.png b/bundles/org.openhab.binding.nikobus/doc/s4.png new file mode 100644 index 000000000..de0044e26 Binary files /dev/null and b/bundles/org.openhab.binding.nikobus/doc/s4.png differ diff --git a/bundles/org.openhab.binding.nikobus/doc/s8.png b/bundles/org.openhab.binding.nikobus/doc/s8.png new file mode 100644 index 000000000..ad4eebb10 Binary files /dev/null and b/bundles/org.openhab.binding.nikobus/doc/s8.png differ diff --git a/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/discovery/NikobusDiscoveryService.java b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/discovery/NikobusDiscoveryService.java new file mode 100644 index 000000000..9e08f7913 --- /dev/null +++ b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/discovery/NikobusDiscoveryService.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2020 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.nikobus.internal.discovery; + +import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nikobus.internal.handler.NikobusPcLinkHandler; +import org.openhab.binding.nikobus.internal.utils.Utils; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NikobusDiscoveryService} discovers push button things for Nikobus switches. + * Buttons are not discovered via scan but only when physical button is pressed and a new + * nikobus push button bus address is detected. + * + * @author Boris Krivonog - Initial contribution + */ +@NonNullByDefault +public class NikobusDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + + private final Logger logger = LoggerFactory.getLogger(NikobusDiscoveryService.class); + private @Nullable NikobusPcLinkHandler bridgeHandler; + + public NikobusDiscoveryService() throws IllegalArgumentException { + super(Collections.singleton(THING_TYPE_PUSH_BUTTON), 0); + } + + @Override + protected void startScan() { + } + + @Override + protected void stopBackgroundDiscovery() { + NikobusPcLinkHandler handler = bridgeHandler; + if (handler != null) { + handler.resetUnhandledCommandProcessor(); + } + } + + @Override + protected void startBackgroundDiscovery() { + NikobusPcLinkHandler handler = bridgeHandler; + if (handler != null) { + handler.setUnhandledCommandProcessor(this::process); + } + } + + private void process(String command) { + if (command.length() <= 2 || !command.startsWith("#N")) { + logger.debug("Ignoring command() '{}'", command); + } + + String address = command.substring(2); + logger.debug("Received address = '{}'", address); + + NikobusPcLinkHandler handler = bridgeHandler; + if (handler != null) { + ThingUID thingUID = new ThingUID(THING_TYPE_PUSH_BUTTON, handler.getThing().getUID(), address); + + Map properties = new HashMap<>(); + properties.put(CONFIG_ADDRESS, address); + + String humanReadableNikobusAddress = Utils.convertToHumanReadableNikobusAddress(address).toUpperCase(); + logger.debug("Detected Nikobus Push Button: '{}'", humanReadableNikobusAddress); + thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(THING_TYPE_PUSH_BUTTON) + .withLabel("Nikobus Push Button " + humanReadableNikobusAddress).withProperties(properties) + .withRepresentationProperty(CONFIG_ADDRESS).withBridge(handler.getThing().getUID()).build()); + } + } + + @Override + public void setThingHandler(ThingHandler handler) { + if (handler instanceof NikobusPcLinkHandler) { + bridgeHandler = (NikobusPcLinkHandler) handler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } + + @Override + public void activate() { + super.activate(null); + } + + @Override + public void deactivate() { + super.deactivate(); + } +} diff --git a/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusModuleHandler.java b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusModuleHandler.java index bb7e2eb04..85cbb2561 100644 --- a/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusModuleHandler.java +++ b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusModuleHandler.java @@ -176,14 +176,16 @@ abstract class NikobusModuleHandler extends NikobusBaseThingHandler { logger.debug("setting channel '{}' to {}", channelId, value); + Integer previousValue; synchronized (cachedStates) { - cachedStates.put(channelId, value); + previousValue = cachedStates.put(channelId, value); } - updateState(channelId, stateFromValue(value)); + if (previousValue == null || previousValue.intValue() != value) { + updateState(channelId, stateFromValue(value)); + } } - @SuppressWarnings({ "unused", "null" }) private void processWrite(ChannelUID channelUID, Command command) { StringBuilder commandPayload = new StringBuilder(); SwitchModuleGroup group = SwitchModuleGroup.mapFromChannel(channelUID); diff --git a/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusPcLinkHandler.java b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusPcLinkHandler.java index e05928c24..bcb2c74a3 100644 --- a/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusPcLinkHandler.java +++ b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/handler/NikobusPcLinkHandler.java @@ -16,6 +16,7 @@ import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.CONFI import java.io.IOException; import java.io.OutputStream; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -24,12 +25,14 @@ import java.util.Map; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.nikobus.internal.NikobusBindingConstants; +import org.openhab.binding.nikobus.internal.discovery.NikobusDiscoveryService; import org.openhab.binding.nikobus.internal.protocol.NikobusCommand; import org.openhab.binding.nikobus.internal.protocol.NikobusConnection; import org.openhab.binding.nikobus.internal.utils.Utils; @@ -41,6 +44,7 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,6 +67,7 @@ public class NikobusPcLinkHandler extends BaseBridgeHandler { private @Nullable ScheduledFuture scheduledRefreshFuture; private @Nullable ScheduledFuture scheduledSendCommandWatchdogFuture; private @Nullable String ack; + private @Nullable Consumer unhandledCommandsProcessor; private int refreshThingIndex = 0; public NikobusPcLinkHandler(Bridge bridge, SerialPortManager serialPortManager) { @@ -113,7 +118,11 @@ public class NikobusPcLinkHandler extends BaseBridgeHandler { // Noop. } - @SuppressWarnings("null") + @Override + public Collection> getServices() { + return Collections.singleton(NikobusDiscoveryService.class); + } + private void processReceivedValue(byte value) { logger.trace("Received {}", value); @@ -133,6 +142,11 @@ public class NikobusPcLinkHandler extends BaseBridgeHandler { Runnable listener = commandListeners.get(command); if (listener != null) { listener.run(); + } else { + Consumer processor = unhandledCommandsProcessor; + if (processor != null) { + processor.accept(command); + } } } } catch (RuntimeException e) { @@ -157,7 +171,6 @@ public class NikobusPcLinkHandler extends BaseBridgeHandler { } } - @SuppressWarnings("null") public void addListener(String command, Runnable listener) { if (commandListeners.put(command, listener) != null) { logger.warn("Multiple registrations for '{}'", command); @@ -168,6 +181,17 @@ public class NikobusPcLinkHandler extends BaseBridgeHandler { commandListeners.remove(command); } + public void setUnhandledCommandProcessor(Consumer processor) { + if (unhandledCommandsProcessor != null) { + logger.debug("Unexpected override of unhandledCommandsProcessor"); + } + unhandledCommandsProcessor = processor; + } + + public void resetUnhandledCommandProcessor() { + unhandledCommandsProcessor = null; + } + private void processResponse(String commandPayload, @Nullable String ack) { NikobusCommand command; synchronized (pendingCommands) { @@ -229,7 +253,6 @@ public class NikobusPcLinkHandler extends BaseBridgeHandler { scheduler.submit(this::processCommand); } - @SuppressWarnings({ "unused", "null" }) private void processCommand() { NikobusCommand command; synchronized (pendingCommands) { diff --git a/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/utils/CRCUtil.java b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/utils/CRCUtil.java index a02dc70da..322d60e4b 100644 --- a/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/utils/CRCUtil.java +++ b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/utils/CRCUtil.java @@ -56,7 +56,7 @@ public class CRCUtil { } check = check & CRC_INIT; - String checksum = leftPadWithZeros(Integer.toHexString(check), 4); + String checksum = Utils.leftPadWithZeros(Integer.toHexString(check), 4); return (input + checksum).toUpperCase(); } @@ -87,14 +87,6 @@ public class CRCUtil { } } - return input + leftPadWithZeros(Integer.toHexString(check), 2).toUpperCase(); - } - - private static String leftPadWithZeros(String text, int size) { - StringBuilder builder = new StringBuilder(text); - while (builder.length() < size) { - builder.insert(0, '0'); - } - return builder.toString(); + return input + Utils.leftPadWithZeros(Integer.toHexString(check), 2).toUpperCase(); } } diff --git a/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/utils/Utils.java b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/utils/Utils.java index db0c62e38..45830821b 100644 --- a/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/utils/Utils.java +++ b/bundles/org.openhab.binding.nikobus/src/main/java/org/openhab/binding/nikobus/internal/utils/Utils.java @@ -29,4 +29,62 @@ public class Utils { future.cancel(true); } } + + /** + * Convert bus address to push button's address as seen in Nikobus + * PC software. + * + * @param addressString + * String representing a bus Push Button's address. + * @return Push button's address as seen in Nikobus PC software. + */ + public static String convertToHumanReadableNikobusAddress(String addressString) { + try { + int address = Integer.parseInt(addressString, 16); + int nikobusAddress = 0; + + for (int i = 0; i < 21; ++i) { + nikobusAddress = (nikobusAddress << 1) | ((address >> i) & 1); + } + + nikobusAddress = (nikobusAddress << 1); + int button = (address >> 21) & 0x07; + + return leftPadWithZeros(Integer.toHexString(nikobusAddress), 6) + ":" + mapButton(button); + + } catch (NumberFormatException e) { + return "[" + addressString + "]"; + } + } + + private static String mapButton(int buttonIndex) { + switch (buttonIndex) { + case 0: + return "1"; + case 1: + return "5"; + case 2: + return "2"; + case 3: + return "6"; + case 4: + return "3"; + case 5: + return "7"; + case 6: + return "4"; + case 7: + return "8"; + default: + return "?"; + } + } + + public static String leftPadWithZeros(String text, int size) { + StringBuilder builder = new StringBuilder(text); + while (builder.length() < size) { + builder.insert(0, '0'); + } + return builder.toString(); + } }