[nikobus] discovery for push buttons, (#9134)

* 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>
This commit is contained in:
Boris Krivonog 2020-11-27 02:02:49 +01:00 committed by GitHub
parent d646215288
commit a138748ff3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 283 additions and 20 deletions

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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<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();
}
}

View File

@ -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);

View File

@ -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<String> 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<Class<? extends ThingHandlerService>> 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<String> 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<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) {
@ -229,7 +253,6 @@ public class NikobusPcLinkHandler extends BaseBridgeHandler {
scheduler.submit(this::processCommand);
}
@SuppressWarnings({ "unused", "null" })
private void processCommand() {
NikobusCommand command;
synchronized (pendingCommands) {

View File

@ -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();
}
}

View File

@ -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();
}
}