* removed state update if value didn't change so expiry binding can actually - expire. If expire time was larger than refresh interval of the binding, expiration never happened ...
Signed-off-by: Boris Krivonog <boris.krivonog@inova.si>
* `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.
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
+
+
+
+```
+ 1 = A
+ 2 = B
+ ```
+
+##### 4 buttons switch
+
+
+
+maps as
+
+```
+ 3 1
+ 4 2
+```
+
+so
+
+```
+1 = C
+2 = D
+3 = A
+4 = B
+```
+
+##### 8 buttons switch
+
+
+
+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
--- /dev/null
+/**
+ * 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<String, Object> 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();
+ }
+}
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);
import java.io.IOException;
import java.io.OutputStream;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
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;
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;
private @Nullable ScheduledFuture<?> scheduledRefreshFuture;
private @Nullable ScheduledFuture<?> scheduledSendCommandWatchdogFuture;
private @Nullable String ack;
+ private @Nullable Consumer<String> unhandledCommandsProcessor;
private int refreshThingIndex = 0;
public NikobusPcLinkHandler(Bridge bridge, SerialPortManager serialPortManager) {
// Noop.
}
- @SuppressWarnings("null")
+ @Override
+ public Collection<Class<? extends ThingHandlerService>> getServices() {
+ return Collections.singleton(NikobusDiscoveryService.class);
+ }
+
private void processReceivedValue(byte value) {
logger.trace("Received {}", value);
Runnable listener = commandListeners.get(command);
if (listener != null) {
listener.run();
+ } else {
+ Consumer<String> processor = unhandledCommandsProcessor;
+ if (processor != null) {
+ processor.accept(command);
+ }
}
}
} catch (RuntimeException e) {
}
}
- @SuppressWarnings("null")
public void addListener(String command, Runnable listener) {
if (commandListeners.put(command, listener) != null) {
logger.warn("Multiple registrations for '{}'", command);
commandListeners.remove(command);
}
+ public void setUnhandledCommandProcessor(Consumer<String> 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) {
scheduler.submit(this::processCommand);
}
- @SuppressWarnings({ "unused", "null" })
private void processCommand() {
NikobusCommand command;
synchronized (pendingCommands) {
}
check = check & CRC_INIT;
- String checksum = leftPadWithZeros(Integer.toHexString(check), 4);
+ String checksum = Utils.leftPadWithZeros(Integer.toHexString(check), 4);
return (input + checksum).toUpperCase();
}
}
}
- 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();
}
}
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();
+ }
}