added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.nikobus-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-nikobus" description="Nikobus Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.nikobus/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,47 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link NikobusBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public class NikobusBindingConstants {
private static final String BINDING_ID = "nikobus";
// List of all Thing Type UIDs
public static final ThingTypeUID BRIDGE_TYPE_PCLINK = new ThingTypeUID(BINDING_ID, "pc-link");
public static final ThingTypeUID THING_TYPE_PUSH_BUTTON = new ThingTypeUID(BINDING_ID, "push-button");
public static final ThingTypeUID THING_TYPE_SWITCH_MODULE = new ThingTypeUID(BINDING_ID, "switch-module");
public static final ThingTypeUID THING_TYPE_DIMMER_MODULE = new ThingTypeUID(BINDING_ID, "dimmer-module");
public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER_MODULE = new ThingTypeUID(BINDING_ID,
"rollershutter-module");
// List of all Channel ids
public static final String CHANNEL_BUTTON = "button";
public static final String CHANNEL_OUTPUT_PREFIX = "output-";
// Configuration parameters
public static final String CONFIG_REFRESH_INTERVAL = "refreshInterval";
public static final String CONFIG_IMPACTED_MODULES = "impactedModules";
public static final String CONFIG_ADDRESS = "address";
public static final String CONFIG_PORT_NAME = "port";
}

View File

@@ -0,0 +1,95 @@
/**
* 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;
import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.*;
import java.util.Collections;
import java.util.Set;
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.handler.NikobusDimmerModuleHandler;
import org.openhab.binding.nikobus.internal.handler.NikobusPcLinkHandler;
import org.openhab.binding.nikobus.internal.handler.NikobusPushButtonHandler;
import org.openhab.binding.nikobus.internal.handler.NikobusRollershutterModuleHandler;
import org.openhab.binding.nikobus.internal.handler.NikobusSwitchModuleHandler;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link NikobusHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.nikobus", service = ThingHandlerFactory.class)
public class NikobusHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(BRIDGE_TYPE_PCLINK, THING_TYPE_PUSH_BUTTON, THING_TYPE_SWITCH_MODULE,
THING_TYPE_DIMMER_MODULE, THING_TYPE_ROLLERSHUTTER_MODULE).collect(Collectors.toSet()));
private @NonNullByDefault({}) SerialPortManager serialPortManager;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (BRIDGE_TYPE_PCLINK.equals(thingTypeUID)) {
return new NikobusPcLinkHandler((Bridge) thing, serialPortManager);
}
if (THING_TYPE_PUSH_BUTTON.equals(thingTypeUID)) {
return new NikobusPushButtonHandler(thing);
}
if (THING_TYPE_SWITCH_MODULE.equals(thingTypeUID)) {
return new NikobusSwitchModuleHandler(thing);
}
if (THING_TYPE_DIMMER_MODULE.equals(thingTypeUID)) {
return new NikobusDimmerModuleHandler(thing);
}
if (THING_TYPE_ROLLERSHUTTER_MODULE.equals(thingTypeUID)) {
return new NikobusRollershutterModuleHandler(thing);
}
return null;
}
@Reference
protected void setSerialPortManager(final SerialPortManager serialPortManager) {
this.serialPortManager = serialPortManager;
}
protected void unsetSerialPortManager(final SerialPortManager serialPortManager) {
this.serialPortManager = null;
}
}

View File

@@ -0,0 +1,63 @@
/**
* 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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nikobus.internal.NikobusBindingConstants;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
/**
* The {@link NikobusBaseThingHandler} class defines utility logic to be consumed by Nikobus thing(s).
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
abstract class NikobusBaseThingHandler extends BaseThingHandler {
private @Nullable String address;
protected NikobusBaseThingHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
address = (String) getConfig().get(NikobusBindingConstants.CONFIG_ADDRESS);
if (address == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Address must be set!");
return;
}
updateStatus(ThingStatus.UNKNOWN);
}
protected @Nullable NikobusPcLinkHandler getPcLink() {
Bridge bridge = getBridge();
if (bridge != null) {
return (NikobusPcLinkHandler) bridge.getHandler();
}
return null;
}
protected String getAddress() {
String address = this.address;
if (address == null) {
throw new IllegalStateException();
}
return address;
}
}

View File

@@ -0,0 +1,69 @@
/**
* 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.handler;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup;
import org.openhab.binding.nikobus.internal.utils.Utils;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* The {@link NikobusDimmerModuleHandler} is responsible for communication between Nikobus dim-controller and binding.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public class NikobusDimmerModuleHandler extends NikobusSwitchModuleHandler {
private @Nullable Future<?> requestUpdateFuture;
public NikobusDimmerModuleHandler(Thing thing) {
super(thing);
}
@Override
public void dispose() {
Utils.cancel(requestUpdateFuture);
requestUpdateFuture = null;
super.dispose();
}
@Override
public void requestStatus(SwitchModuleGroup group) {
Utils.cancel(requestUpdateFuture);
super.requestStatus(group);
requestUpdateFuture = scheduler.schedule(() -> super.requestStatus(group), 1, TimeUnit.SECONDS);
}
@Override
protected int valueFromCommand(Command command) {
if (command instanceof PercentType) {
return Math.round(((PercentType) command).floatValue() / 100f * 255f);
}
return super.valueFromCommand(command);
}
@Override
protected State stateFromValue(int value) {
int result = Math.round(value * 100f / 255f);
return new PercentType(result);
}
}

View File

@@ -0,0 +1,242 @@
/**
* 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.handler;
import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.CHANNEL_OUTPUT_PREFIX;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nikobus.internal.protocol.NikobusCommand;
import org.openhab.binding.nikobus.internal.protocol.SwitchModuleCommandFactory;
import org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NikobusSwitchModuleHandler} is responsible for communication between Nikobus modules and binding.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
abstract class NikobusModuleHandler extends NikobusBaseThingHandler {
private final EnumSet<SwitchModuleGroup> pendingRefresh = EnumSet.noneOf(SwitchModuleGroup.class);
private final Logger logger = LoggerFactory.getLogger(NikobusModuleHandler.class);
private final Map<String, Integer> cachedStates = new HashMap<>();
private final List<ChannelUID> linkedChannels = new ArrayList<>();
protected NikobusModuleHandler(Thing thing) {
super(thing);
}
@Override
public void dispose() {
super.dispose();
synchronized (cachedStates) {
cachedStates.clear();
}
synchronized (pendingRefresh) {
pendingRefresh.clear();
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
refreshChannel(channelUID);
} else {
processWrite(channelUID, command);
}
}
private void refreshChannel(ChannelUID channelUID) {
logger.debug("Refreshing channel '{}'", channelUID.getId());
if (!isLinked(channelUID)) {
logger.debug("Refreshing channel '{}' skipped since it is not linked", channelUID.getId());
return;
}
updateGroup(SwitchModuleGroup.mapFromChannel(channelUID));
}
@Override
public void channelLinked(ChannelUID channelUID) {
synchronized (linkedChannels) {
linkedChannels.add(channelUID);
}
super.channelLinked(channelUID);
}
@Override
public void channelUnlinked(ChannelUID channelUID) {
synchronized (linkedChannels) {
linkedChannels.remove(channelUID);
}
super.channelUnlinked(channelUID);
}
public void refreshModule() {
Set<SwitchModuleGroup> groups = new HashSet<>();
synchronized (linkedChannels) {
for (ChannelUID channelUID : linkedChannels) {
groups.add(SwitchModuleGroup.mapFromChannel(channelUID));
}
}
if (groups.isEmpty()) {
logger.debug("Nothing to refresh for '{}'", thing.getUID());
return;
}
logger.debug("Refreshing {} - {}", thing.getUID(), groups);
for (SwitchModuleGroup group : groups) {
updateGroup(group);
}
}
public void requestStatus(SwitchModuleGroup group) {
updateGroup(group);
}
private void updateGroup(SwitchModuleGroup group) {
synchronized (pendingRefresh) {
if (pendingRefresh.contains(group)) {
logger.debug("Refresh already scheduled for group {} of module '{}'", group, getAddress());
return;
}
pendingRefresh.add(group);
}
logger.debug("Refreshing group {} of switch module '{}'", group, getAddress());
NikobusPcLinkHandler pcLink = getPcLink();
if (pcLink != null) {
NikobusCommand command = SwitchModuleCommandFactory.createReadCommand(getAddress(), group,
result -> processStatusUpdate(result, group));
pcLink.sendCommand(command);
}
}
private void processStatusUpdate(NikobusCommand.Result result, SwitchModuleGroup group) {
try {
String responsePayload = result.get();
logger.debug("processStatusUpdate '{}' for group {} in module '{}'", responsePayload, group, getAddress());
if (thing.getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
// Update channel's statuses based on response.
for (int i = 0; i < group.getCount(); i++) {
String channelId = CHANNEL_OUTPUT_PREFIX + (i + group.getOffset());
String responseDigits = responsePayload.substring(9 + (i * 2), 11 + (i * 2));
int value = Integer.parseInt(responseDigits, 16);
updateStateAndCacheValue(channelId, value);
}
} catch (Exception e) {
logger.warn("Processing response for '{}'-{} failed with {}", getAddress(), group, e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} finally {
synchronized (pendingRefresh) {
pendingRefresh.remove(group);
}
}
}
private void updateStateAndCacheValue(String channelId, int value) {
if (value < 0x00 || value > 0xff) {
throw new IllegalArgumentException("Invalid range. 0x00 - 0xff expected but got value " + value);
}
logger.debug("setting channel '{}' to {}", channelId, value);
synchronized (cachedStates) {
cachedStates.put(channelId, value);
}
updateState(channelId, stateFromValue(value));
}
@SuppressWarnings({ "unused", "null" })
private void processWrite(ChannelUID channelUID, Command command) {
StringBuilder commandPayload = new StringBuilder();
SwitchModuleGroup group = SwitchModuleGroup.mapFromChannel(channelUID);
for (int i = group.getOffset(); i < group.getOffset() + group.getCount(); i++) {
String channelId = CHANNEL_OUTPUT_PREFIX + i;
Integer digits;
if (channelId.equals(channelUID.getId())) {
digits = valueFromCommand(command);
updateStateAndCacheValue(channelId, digits.intValue());
} else {
synchronized (cachedStates) {
digits = cachedStates.get(channelId);
}
}
if (digits == null) {
commandPayload.append("00");
logger.warn("no cached value found for '{}' in module '{}'", channelId, getAddress());
} else {
commandPayload.append(String.format("%02X", digits.intValue()));
}
}
NikobusPcLinkHandler pcLink = getPcLink();
if (pcLink != null) {
pcLink.sendCommand(SwitchModuleCommandFactory.createWriteCommand(getAddress(), group,
commandPayload.toString(), this::processWriteCommandResponse));
}
}
private void processWriteCommandResponse(NikobusCommand.Result result) {
try {
String responsePayload = result.get();
logger.debug("processWriteCommandResponse '{}'", responsePayload);
if (thing.getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
} catch (Exception e) {
logger.warn("Processing write confirmation failed with {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
protected abstract int valueFromCommand(Command command);
protected abstract State stateFromValue(int value);
}

View File

@@ -0,0 +1,337 @@
/**
* 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.handler;
import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.CONFIG_REFRESH_INTERVAL;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
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.protocol.NikobusCommand;
import org.openhab.binding.nikobus.internal.protocol.NikobusConnection;
import org.openhab.binding.nikobus.internal.utils.Utils;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
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.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NikobusPcLinkHandler} is responsible for handling commands, which are
* sent or received from the PC-Link Nikobus component.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public class NikobusPcLinkHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(NikobusPcLinkHandler.class);
private final Map<String, Runnable> commandListeners = Collections.synchronizedMap(new HashMap<>());
private final LinkedList<NikobusCommand> pendingCommands = new LinkedList<>();
private final StringBuilder stringBuilder = new StringBuilder();
private final SerialPortManager serialPortManager;
private @Nullable NikobusConnection connection;
private @Nullable NikobusCommand currentCommand;
private @Nullable ScheduledFuture<?> scheduledRefreshFuture;
private @Nullable ScheduledFuture<?> scheduledSendCommandWatchdogFuture;
private @Nullable String ack;
private int refreshThingIndex = 0;
public NikobusPcLinkHandler(Bridge bridge, SerialPortManager serialPortManager) {
super(bridge);
this.serialPortManager = serialPortManager;
}
@Override
public void initialize() {
ack = null;
stringBuilder.setLength(0);
updateStatus(ThingStatus.UNKNOWN);
String portName = (String) getConfig().get(NikobusBindingConstants.CONFIG_PORT_NAME);
if (portName == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
return;
}
connection = new NikobusConnection(serialPortManager, portName, this::processReceivedValue);
int refreshInterval = ((Number) getConfig().get(CONFIG_REFRESH_INTERVAL)).intValue();
scheduledRefreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, refreshInterval, refreshInterval,
TimeUnit.SECONDS);
}
@Override
public void dispose() {
super.dispose();
Utils.cancel(scheduledSendCommandWatchdogFuture);
scheduledSendCommandWatchdogFuture = null;
Utils.cancel(scheduledRefreshFuture);
scheduledRefreshFuture = null;
NikobusConnection connection = this.connection;
this.connection = null;
if (connection != null) {
connection.close();
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Noop.
}
@SuppressWarnings("null")
private void processReceivedValue(byte value) {
logger.trace("Received {}", value);
if (value == 13) {
String command = stringBuilder.toString();
stringBuilder.setLength(0);
logger.debug("Received command '{}', ack = '{}'", command, ack);
try {
if (command.startsWith("$")) {
String ack = this.ack;
this.ack = null;
processResponse(command, ack);
} else {
Runnable listener = commandListeners.get(command);
if (listener != null) {
listener.run();
}
}
} catch (RuntimeException e) {
logger.debug("Processing command '{}' failed due {}", command, e.getMessage(), e);
}
} else {
stringBuilder.append((char) value);
// Take ACK part, i.e. "$0512"
if (stringBuilder.length() == 5) {
String payload = stringBuilder.toString();
if (payload.startsWith("$05")) {
ack = payload;
logger.debug("Received ack '{}'", ack);
stringBuilder.setLength(0);
}
} else if (stringBuilder.length() > 128) {
// Fuse, if for some reason we don't receive \r don't fill buffer.
stringBuilder.setLength(0);
logger.warn("Resetting read buffer, should not happen, am I connected to Nikobus?");
}
}
}
@SuppressWarnings("null")
public void addListener(String command, Runnable listener) {
if (commandListeners.put(command, listener) != null) {
logger.warn("Multiple registrations for '{}'", command);
}
}
public void removeListener(String command) {
commandListeners.remove(command);
}
private void processResponse(String commandPayload, @Nullable String ack) {
NikobusCommand command;
synchronized (pendingCommands) {
command = currentCommand;
}
if (command == null) {
logger.debug("Processing response but no command pending");
return;
}
NikobusCommand.ResponseHandler responseHandler = command.getResponseHandler();
if (responseHandler == null) {
logger.debug("No response expected for current command");
return;
}
if (ack == null) {
logger.debug("No ack received");
return;
}
String requestCommandId = command.getPayload().substring(3, 5);
String ackCommandId = ack.substring(3, 5);
if (!ackCommandId.equals(requestCommandId)) {
logger.debug("Unexpected command's ack '{}' != '{}'", requestCommandId, ackCommandId);
return;
}
// Check if response has expected length.
if (commandPayload.length() != responseHandler.getResponseLength()) {
logger.debug("Unexpected response length");
return;
}
if (!commandPayload.startsWith(responseHandler.getResponseCode())) {
logger.debug("Unexpected response command code");
return;
}
String requestCommandAddress = command.getPayload().substring(5, 9);
String ackCommandAddress = commandPayload.substring(responseHandler.getAddressStart(),
responseHandler.getAddressStart() + 4);
if (!requestCommandAddress.equals(ackCommandAddress)) {
logger.debug("Unexpected response address");
return;
}
if (responseHandler.complete(commandPayload)) {
resetProcessingAndProcessNext();
}
}
public void sendCommand(NikobusCommand command) {
synchronized (pendingCommands) {
pendingCommands.addLast(command);
}
scheduler.submit(this::processCommand);
}
@SuppressWarnings({ "unused", "null" })
private void processCommand() {
NikobusCommand command;
synchronized (pendingCommands) {
if (currentCommand != null) {
return;
}
command = pendingCommands.pollFirst();
if (command == null) {
return;
}
currentCommand = command;
}
sendCommand(command, 3);
}
private void sendCommand(NikobusCommand command, int retry) {
logger.debug("Sending retry = {}, command '{}'", retry, command.getPayload());
NikobusConnection connection = this.connection;
if (connection == null) {
return;
}
try {
connectIfNeeded(connection);
OutputStream outputStream = connection.getOutputStream();
if (outputStream == null) {
return;
}
outputStream.write(command.getPayload().getBytes());
outputStream.flush();
} catch (IOException e) {
logger.debug("Sending command failed due {}", e.getMessage(), e);
connection.close();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} finally {
NikobusCommand.ResponseHandler responseHandler = command.getResponseHandler();
if (responseHandler == null) {
resetProcessingAndProcessNext();
} else if (retry > 0) {
scheduleSendCommandTimeout(() -> {
if (!responseHandler.isCompleted()) {
sendCommand(command, retry - 1);
}
});
} else {
scheduleSendCommandTimeout(() -> processTimeout(responseHandler));
}
}
}
private void scheduleSendCommandTimeout(Runnable command) {
scheduledSendCommandWatchdogFuture = scheduler.schedule(command, 2, TimeUnit.SECONDS);
}
private void processTimeout(NikobusCommand.ResponseHandler responseHandler) {
if (responseHandler.completeExceptionally(new TimeoutException("Waiting for response timed-out."))) {
resetProcessingAndProcessNext();
}
}
private void resetProcessingAndProcessNext() {
Utils.cancel(scheduledSendCommandWatchdogFuture);
synchronized (pendingCommands) {
currentCommand = null;
}
scheduler.submit(this::processCommand);
}
private void refresh() {
List<Thing> things = getThing().getThings().stream()
.filter(thing -> thing.getHandler() instanceof NikobusModuleHandler).collect(Collectors.toList());
if (things.isEmpty()) {
logger.debug("Nothing to refresh");
return;
}
refreshThingIndex = (refreshThingIndex + 1) % things.size();
ThingHandler thingHandler = things.get(refreshThingIndex).getHandler();
if (thingHandler == null) {
return;
}
NikobusModuleHandler handler = (NikobusModuleHandler) thingHandler;
handler.refreshModule();
}
private synchronized void connectIfNeeded(NikobusConnection connection) throws IOException {
if (!connection.isConnected()) {
connection.connect();
// Send connection sequence, mimicking the Nikobus software. If this is not send, PC-Link
// sometimes does not forward button presses via serial interface.
Stream.of(new String[] { "++++", "ATH0", "ATZ", "$10110000B8CF9D", "#L0", "#E0", "#L0", "#E1" })
.map(NikobusCommand::new).forEach(this::sendCommand);
updateStatus(ThingStatus.ONLINE);
}
}
}

View File

@@ -0,0 +1,221 @@
/**
* 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.handler;
import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.*;
import static org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nikobus.internal.protocol.NikobusCommand;
import org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup;
import org.openhab.binding.nikobus.internal.utils.Utils;
import org.openhab.core.common.AbstractUID;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NikobusPushButtonHandler} is responsible for handling Nikobus push buttons.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public class NikobusPushButtonHandler extends NikobusBaseThingHandler {
private static class ImpactedModule {
private final ThingUID thingUID;
private final SwitchModuleGroup group;
ImpactedModule(ThingUID thingUID, SwitchModuleGroup group) {
this.thingUID = thingUID;
this.group = group;
}
public ThingUID getThingUID() {
return thingUID;
}
public SwitchModuleGroup getGroup() {
return group;
}
@Override
public String toString() {
return "'" + thingUID + "'-" + group;
}
}
private static class ImpactedModuleUID extends AbstractUID {
ImpactedModuleUID(String uid) {
super(uid);
}
String getThingTypeId() {
return getSegment(0);
}
String getThingId() {
return getSegment(1);
}
SwitchModuleGroup getGroup() {
if (getSegment(2).equals("1")) {
return FIRST;
}
if (getSegment(2).equals("2")) {
return SECOND;
}
throw new IllegalArgumentException("Unexpected group found " + getSegment(2));
}
@Override
protected int getMinimalNumberOfSegments() {
return 3;
}
}
private static final String END_OF_TRANSMISSION = "\r#E1";
private final Logger logger = LoggerFactory.getLogger(NikobusPushButtonHandler.class);
private final List<ImpactedModule> impactedModules = Collections.synchronizedList(new ArrayList<>());
private @Nullable Future<?> requestUpdateFuture;
public NikobusPushButtonHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
if (thing.getStatus() == ThingStatus.OFFLINE) {
return;
}
impactedModules.clear();
try {
ThingUID bridgeUID = thing.getBridgeUID();
if (bridgeUID == null) {
throw new IllegalArgumentException("Bridge does not exist!");
}
String[] impactedModulesString = getConfig().get(CONFIG_IMPACTED_MODULES).toString().split(",");
for (String impactedModuleString : impactedModulesString) {
ImpactedModuleUID impactedModuleUID = new ImpactedModuleUID(impactedModuleString.trim());
ThingTypeUID thingTypeUID = new ThingTypeUID(bridgeUID.getBindingId(),
impactedModuleUID.getThingTypeId());
ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, impactedModuleUID.getThingId());
impactedModules.add(new ImpactedModule(thingUID, impactedModuleUID.getGroup()));
}
} catch (RuntimeException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return;
}
logger.debug("Impacted modules for {} = {}", thing.getUID(), impactedModules);
NikobusPcLinkHandler pcLink = getPcLink();
if (pcLink != null) {
pcLink.addListener(getAddress(), this::commandReceived);
}
}
@Override
public void dispose() {
super.dispose();
Utils.cancel(requestUpdateFuture);
requestUpdateFuture = null;
NikobusPcLinkHandler pcLink = getPcLink();
if (pcLink != null) {
pcLink.removeListener(getAddress());
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("handleCommand '{}' '{}'", channelUID, command);
if (!CHANNEL_BUTTON.equals(channelUID.getId())) {
return;
}
// Whenever the button receives an ON command,
// we send a simulated button press to the Nikobus.
if (command == OnOffType.ON) {
NikobusPcLinkHandler pcLink = getPcLink();
if (pcLink != null) {
pcLink.sendCommand(new NikobusCommand(getAddress() + END_OF_TRANSMISSION));
}
}
}
private void commandReceived() {
if (thing.getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
updateState(CHANNEL_BUTTON, OnOffType.ON);
Utils.cancel(requestUpdateFuture);
requestUpdateFuture = scheduler.schedule(this::update, 400, TimeUnit.MILLISECONDS);
}
private void update() {
for (ImpactedModule module : impactedModules) {
NikobusModuleHandler switchModule = getModuleWithId(module.getThingUID());
if (switchModule != null) {
switchModule.requestStatus(module.getGroup());
}
}
}
private @Nullable NikobusModuleHandler getModuleWithId(ThingUID thingUID) {
Bridge bridge = getBridge();
if (bridge == null) {
return null;
}
Thing thing = bridge.getThing(thingUID);
if (thing == null) {
return null;
}
ThingHandler thingHandler = thing.getHandler();
if (thingHandler instanceof NikobusModuleHandler) {
return (NikobusModuleHandler) thingHandler;
}
return null;
}
@Override
protected String getAddress() {
return "#N" + super.getAddress();
}
}

View File

@@ -0,0 +1,63 @@
/**
* 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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* The {@link NikobusRollershutterModuleHandler} is responsible for communication between Nikobus
* rollershutter-controller and binding.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public class NikobusRollershutterModuleHandler extends NikobusModuleHandler {
public NikobusRollershutterModuleHandler(Thing thing) {
super(thing);
}
@Override
protected int valueFromCommand(Command command) {
if (command == UpDownType.DOWN || command == StopMoveType.MOVE) {
return 0x02;
}
if (command == UpDownType.UP) {
return 0x01;
}
if (command == StopMoveType.STOP) {
return 0x00;
}
throw new IllegalArgumentException("Command '" + command + "' not supported");
}
@Override
protected State stateFromValue(int value) {
if (value == 0x00) {
return OnOffType.OFF;
}
if (value == 0x01) {
return UpDownType.UP;
}
if (value == 0x02) {
return UpDownType.DOWN;
}
throw new IllegalArgumentException("Unexpected value " + value + " received");
}
}

View File

@@ -0,0 +1,49 @@
/**
* 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.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
/**
* The {@link NikobusSwitchModuleHandler} is responsible for communication between Nikobus switch module and binding.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public class NikobusSwitchModuleHandler extends NikobusModuleHandler {
public NikobusSwitchModuleHandler(Thing thing) {
super(thing);
}
@Override
protected int valueFromCommand(Command command) {
if (command == OnOffType.ON) {
return 0xff;
}
if (command == OnOffType.OFF) {
return 0x00;
}
throw new IllegalArgumentException("Command '" + command + "' not supported");
}
@Override
protected State stateFromValue(int value) {
return value != 0 ? OnOffType.ON : OnOffType.OFF;
}
}

View File

@@ -0,0 +1,125 @@
/**
* 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.protocol;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NikobusCommand} class holds a command that can be send to Nikobus installation.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public class NikobusCommand {
public static class Result {
private final Callable<String> callable;
private Result(String result) {
callable = () -> result;
}
private Result(Exception exception) {
callable = () -> {
throw exception;
};
}
public String get() throws Exception {
return callable.call();
}
}
public static class ResponseHandler {
private final Logger logger = LoggerFactory.getLogger(ResponseHandler.class);
private final Consumer<Result> resultConsumer;
private final int responseLength;
private final int addressStart;
private final String responseCode;
private final AtomicBoolean isCompleted = new AtomicBoolean();
private ResponseHandler(int responseLength, int addressStart, String responseCode,
Consumer<Result> resultConsumer) {
this.responseLength = responseLength;
this.addressStart = addressStart;
this.responseCode = responseCode;
this.resultConsumer = resultConsumer;
}
public boolean isCompleted() {
return isCompleted.get();
}
public boolean complete(String result) {
return complete(new Result(result));
}
public boolean completeExceptionally(Exception exception) {
return complete(new Result(exception));
}
private boolean complete(Result result) {
if (isCompleted.getAndSet(true)) {
return false;
}
try {
resultConsumer.accept(result);
} catch (RuntimeException e) {
logger.warn("Processing result {} failed with {}", result, e.getMessage(), e);
}
return true;
}
public int getResponseLength() {
return responseLength;
}
public int getAddressStart() {
return addressStart;
}
public String getResponseCode() {
return responseCode;
}
}
private final String payload;
private final @Nullable ResponseHandler responseHandler;
public NikobusCommand(String payload) {
this.payload = payload + '\r';
this.responseHandler = null;
}
public NikobusCommand(String payload, int responseLength, int addressStart, String responseCode,
Consumer<Result> resultConsumer) {
this.payload = payload + '\r';
this.responseHandler = new ResponseHandler(responseLength, addressStart, responseCode, resultConsumer);
}
public String getPayload() {
return payload;
}
public @Nullable ResponseHandler getResponseHandler() {
return responseHandler;
}
}

View File

@@ -0,0 +1,152 @@
/**
* 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.protocol;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.TooManyListenersException;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortEvent;
import org.openhab.core.io.transport.serial.SerialPortEventListener;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NikobusConnection } is responsible for creating connections to clients.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public class NikobusConnection implements SerialPortEventListener {
private final Logger logger = LoggerFactory.getLogger(NikobusConnection.class);
private final SerialPortManager serialPortManager;
private final String portName;
private final Consumer<Byte> processData;
private @Nullable SerialPort serialPort;
public NikobusConnection(SerialPortManager serialPortManager, String portName, Consumer<Byte> processData) {
this.serialPortManager = serialPortManager;
this.portName = portName;
this.processData = processData;
}
/**
* Return true if this manager is connected.
*
* @return
*/
public boolean isConnected() {
return serialPort != null;
}
/**
* Connect to the receiver.
*
**/
public void connect() throws IOException {
if (isConnected()) {
return;
}
SerialPortIdentifier portId = serialPortManager.getIdentifier(portName);
if (portId == null) {
throw new IOException(String.format("Port '%s' is not known!", portName));
}
logger.info("Connecting to {}", portName);
try {
SerialPort serialPort = portId.open("org.openhab.binding.nikobus.pc-link", 2000);
serialPort.addEventListener(this);
serialPort.notifyOnDataAvailable(true);
this.serialPort = serialPort;
logger.info("Connected to {}", portName);
} catch (PortInUseException e) {
throw new IOException(String.format("Port '%s' is in use!", portName), e);
} catch (TooManyListenersException e) {
throw new IOException(String.format("Cannot attach listener to port '%s'!", portName), e);
}
}
/**
* Closes the connection.
**/
public void close() {
SerialPort serialPort = this.serialPort;
this.serialPort = null;
if (serialPort != null) {
try {
serialPort.removeEventListener();
OutputStream outputStream = serialPort.getOutputStream();
if (outputStream != null) {
outputStream.close();
}
InputStream inputStream = serialPort.getInputStream();
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
logger.debug("Error closing serial port.", e);
} finally {
serialPort.close();
logger.debug("Closed serial port.");
}
}
}
/**
* Returns an output stream for this connection.
*/
public @Nullable OutputStream getOutputStream() throws IOException {
SerialPort serialPort = this.serialPort;
if (serialPort == null) {
return null;
}
return serialPort.getOutputStream();
}
@Override
public void serialEvent(SerialPortEvent event) {
if (event.getEventType() != SerialPortEvent.DATA_AVAILABLE) {
return;
}
SerialPort serialPort = this.serialPort;
if (serialPort == null) {
return;
}
try {
InputStream inputStream = serialPort.getInputStream();
if (inputStream == null) {
return;
}
byte[] readBuffer = new byte[64];
while (inputStream.available() > 0) {
int length = inputStream.read(readBuffer);
for (int i = 0; i < length; ++i) {
processData.accept(readBuffer[i]);
}
}
} catch (IOException e) {
logger.debug("Error reading from serial port: {}", e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,53 @@
/**
* 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.protocol;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nikobus.internal.protocol.NikobusCommand.Result;
import org.openhab.binding.nikobus.internal.utils.CRCUtil;
/**
* The {@link NikobusCommand} class defines factory functions to create commands that can be send to Nikobus
* installation.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public class SwitchModuleCommandFactory {
public static NikobusCommand createReadCommand(String address, SwitchModuleGroup group,
Consumer<Result> resultConsumer) {
checkAddress(address);
String commandPayload = CRCUtil.appendCRC2("$10" + CRCUtil.appendCRC(group.getStatusRequest() + address));
return new NikobusCommand(commandPayload, 27, 3, "$1C", resultConsumer);
}
public static NikobusCommand createWriteCommand(String address, SwitchModuleGroup group, String value,
Consumer<Result> resultConsumer) {
checkAddress(address);
if (value.length() != 12) {
throw new IllegalArgumentException(String.format("Value must have 12 chars but got '%s'", value));
}
String payload = group.getStatusUpdate() + address + value + "FF";
return new NikobusCommand(CRCUtil.appendCRC2("$1E" + CRCUtil.appendCRC(payload)), 13, 5, "$0E", resultConsumer);
}
private static void checkAddress(String address) {
if (address.length() != 4) {
throw new IllegalArgumentException(String.format("Address must have 4 chars but got '%s'", address));
}
}
}

View File

@@ -0,0 +1,75 @@
/**
* 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.protocol;
import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.CHANNEL_OUTPUT_PREFIX;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ChannelUID;
/**
* The {@link SwitchModuleGroup} class defines Nikobus module group used for reading status or set its new value.
* Nikobus module can always operate only in groups and not per-channel.
*
* @author Boris Krivonog - Initial contribution
*/
@NonNullByDefault
public enum SwitchModuleGroup {
FIRST("12", "15", 1),
SECOND("17", "16", 7);
private final String statusRequest;
private final String statusUpdate;
private final int offset;
private SwitchModuleGroup(String statusRequest, String statusUpdate, int offset) {
this.statusRequest = statusRequest;
this.statusUpdate = statusUpdate;
this.offset = offset;
}
public String getStatusRequest() {
return statusRequest;
}
public String getStatusUpdate() {
return statusUpdate;
}
public int getOffset() {
return offset;
}
public int getCount() {
return 6;
}
public static SwitchModuleGroup mapFromChannel(ChannelUID channelUID) {
if (!channelUID.getIdWithoutGroup().startsWith(CHANNEL_OUTPUT_PREFIX)) {
throw new IllegalArgumentException("Unexpected channel " + channelUID.getId());
}
String channelNumber = channelUID.getIdWithoutGroup().substring(CHANNEL_OUTPUT_PREFIX.length());
return mapFromChannel(Integer.parseInt(channelNumber));
}
public static SwitchModuleGroup mapFromChannel(int channelNumber) {
int max = SECOND.getOffset() + SECOND.getCount();
if (channelNumber < FIRST.getOffset() || channelNumber > max) {
throw new IllegalArgumentException(String.format("Channel number should be between [%d, %d], but got %d",
FIRST.getOffset(), max, channelNumber));
}
return channelNumber >= SECOND.getOffset() ? SECOND : FIRST;
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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.utils;
import org.apache.commons.lang.StringUtils;
import org.openhab.core.util.HexUtils;
/**
* The {@link CRCUtil} class defines utility functions to calculate CRC used by the Nikobus communication protocol.
*
* @author Davy Vanherbergen - Initial contribution
* @author Boris Krivonog - Removed dependency to javax.xml.bind.DatatypeConverter
*/
public class CRCUtil {
private static final int CRC_INIT = 0xFFFF;
private static final int POLYNOMIAL = 0x1021;
/**
* Calculate the CRC16-CCITT checksum on the input string and return the
* input string with the checksum appended.
*
* @param input
* String representing hex numbers.
* @return input string + CRC.
*/
public static String appendCRC(String input) {
if (input == null) {
return null;
}
int check = CRC_INIT;
for (byte b : HexUtils.hexToBytes(input)) {
for (int i = 0; i < 8; i++) {
if (((b >> (7 - i) & 1) == 1) ^ ((check >> 15 & 1) == 1)) {
check = check << 1;
check = check ^ POLYNOMIAL;
} else {
check = check << 1;
}
}
}
check = check & CRC_INIT;
String checksum = StringUtils.leftPad(Integer.toHexString(check), 4, "0");
return (input + checksum).toUpperCase();
}
/**
* Calculate the second checksum on the input string and return the
* input string with the checksum appended.
*
* @param input
* String representing a nikobus command.
* @return input string + CRC.
*/
public static String appendCRC2(String input) {
int check = 0;
for (byte b : input.getBytes()) {
check = check ^ b;
for (int i = 0; i < 8; i++) {
if (((check & 0xff) >> 7) != 0) {
check = check << 1;
check = check ^ 0x99;
} else {
check = check << 1;
}
check = check & 0xff;
}
}
return input + StringUtils.leftPad(Integer.toHexString(check), 2, "0").toUpperCase();
}
}

View File

@@ -0,0 +1,28 @@
/**
* 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.utils;
import java.util.concurrent.Future;
/**
* The {@link Utils} class defines commonly used utility functions.
*
* @author Boris Krivonog - Initial contribution
*/
public class Utils {
public static void cancel(Future<?> future) {
if (future != null) {
future.cancel(true);
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="nikobus" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Nikobus Binding</name>
<description>This is the binding for Nikobus.</description>
<author>Boris Krivonog</author>
</binding:binding>

View File

@@ -0,0 +1,228 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nikobus"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="pc-link">
<label>PC-Link</label>
<description>PC-Link via serial connection</description>
<config-description>
<parameter name="port" type="text" required="true">
<label>Port</label>
<context>serial-port</context>
<limitToOptions>false</limitToOptions>
<description>The serial port used to connect to the Nikobus PC Link.</description>
</parameter>
<parameter name="refreshInterval" type="integer" max="65535" min="10" required="false">
<default>60</default>
<label>Refresh Interval</label>
<description>Refresh interval in seconds.</description>
</parameter>
</config-description>
</bridge-type>
<thing-type id="push-button">
<supported-bridge-type-refs>
<bridge-type-ref id="pc-link"/>
</supported-bridge-type-refs>
<label>Push Button</label>
<description>A single push button</description>
<channels>
<channel id="button" typeId="button"/>
</channels>
<config-description>
<parameter name="address" type="text">
<label>Address</label>
<description>The Nikobus address of the module</description>
</parameter>
<parameter name="impactedModules" type="text">
<label>Impacted Modules</label>
<description>Comma separated list of impacted modules, i.e. 4C6C-1,4C6C-2</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="button">
<item-type>Switch</item-type>
<label>Button Event</label>
<description>Fires when the button is pressed</description>
<config-description>
<parameter name="address" type="text">
<label>Address</label>
<description>The Nikobus address of the module</description>
</parameter>
</config-description>
</channel-type>
<thing-type id="switch-module">
<supported-bridge-type-refs>
<bridge-type-ref id="pc-link"/>
</supported-bridge-type-refs>
<label>Switch Module</label>
<description>Nikobus Switch module</description>
<channels>
<channel id="output-1" typeId="switch-output">
<label>Output 1</label>
</channel>
<channel id="output-2" typeId="switch-output">
<label>Output 2</label>
</channel>
<channel id="output-3" typeId="switch-output">
<label>Output 3</label>
</channel>
<channel id="output-4" typeId="switch-output">
<label>Output 4</label>
</channel>
<channel id="output-5" typeId="switch-output">
<label>Output 5</label>
</channel>
<channel id="output-6" typeId="switch-output">
<label>Output 6</label>
</channel>
<channel id="output-7" typeId="switch-output">
<label>Output 7</label>
</channel>
<channel id="output-8" typeId="switch-output">
<label>Output 8</label>
</channel>
<channel id="output-9" typeId="switch-output">
<label>Output 9</label>
</channel>
<channel id="output-10" typeId="switch-output">
<label>Output 10</label>
</channel>
<channel id="output-11" typeId="switch-output">
<label>Output 11</label>
</channel>
<channel id="output-12" typeId="switch-output">
<label>Output 12</label>
</channel>
</channels>
<config-description>
<parameter name="address" type="text">
<label>Address</label>
<description>The Nikobus address of the module</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="dimmer-module">
<supported-bridge-type-refs>
<bridge-type-ref id="pc-link"/>
</supported-bridge-type-refs>
<label>Dimmer Module</label>
<description>Nikobus Dimmer module</description>
<channels>
<channel id="output-1" typeId="dimmer-output">
<label>Output 1</label>
</channel>
<channel id="output-2" typeId="dimmer-output">
<label>Output 2</label>
</channel>
<channel id="output-3" typeId="dimmer-output">
<label>Output 3</label>
</channel>
<channel id="output-4" typeId="dimmer-output">
<label>Output 4</label>
</channel>
<channel id="output-5" typeId="dimmer-output">
<label>Output 5</label>
</channel>
<channel id="output-6" typeId="dimmer-output">
<label>Output 6</label>
</channel>
<channel id="output-7" typeId="dimmer-output">
<label>Output 7</label>
</channel>
<channel id="output-8" typeId="dimmer-output">
<label>Output 8</label>
</channel>
<channel id="output-9" typeId="dimmer-output">
<label>Output 9</label>
</channel>
<channel id="output-10" typeId="dimmer-output">
<label>Output 10</label>
</channel>
<channel id="output-11" typeId="dimmer-output">
<label>Output 11</label>
</channel>
<channel id="output-12" typeId="dimmer-output">
<label>Output 12</label>
</channel>
</channels>
<config-description>
<parameter name="address" type="text">
<label>Address</label>
<description>The Nikobus address of the module</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="rollershutter-module">
<supported-bridge-type-refs>
<bridge-type-ref id="pc-link"/>
</supported-bridge-type-refs>
<label>Rollershutter Module</label>
<description>Nikobus Rollershutter module</description>
<channels>
<channel id="output-1" typeId="rollershutter-output">
<label>Output 1</label>
</channel>
<channel id="output-2" typeId="rollershutter-output">
<label>Output 2</label>
</channel>
<channel id="output-3" typeId="rollershutter-output">
<label>Output 3</label>
</channel>
<channel id="output-4" typeId="rollershutter-output">
<label>Output 4</label>
</channel>
<channel id="output-5" typeId="rollershutter-output">
<label>Output 5</label>
</channel>
<channel id="output-6" typeId="rollershutter-output">
<label>Output 6</label>
</channel>
</channels>
<config-description>
<parameter name="address" type="text">
<label>Address</label>
<description>The Nikobus address of the module</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="switch-output">
<item-type>Switch</item-type>
<label>Output</label>
<description>Switch Module's Output</description>
</channel-type>
<channel-type id="dimmer-output">
<item-type>Dimmer</item-type>
<label>Output</label>
<description>Dimmer Module's Output</description>
</channel-type>
<channel-type id="rollershutter-output">
<item-type>Rollershutter</item-type>
<label>Output</label>
<description>Rollershutter Module's Output</description>
</channel-type>
</thing:thing-descriptions>