handlerFactory) {
+ this.count = count;
+ this.handlerFactory = handlerFactory;
+ }
+
+ /**
+ * Gets the number of Channels within the channel group.
+ *
+ * @return the Channel count
+ */
+ public int getCount() {
+ return count;
+ }
+
+ /**
+ * Checks the given Channel id against the max. Channel count in this Channel group.
+ *
+ * @param number the number to check
+ * @return true, if the number is in the range
+ */
+ public boolean isValidId(int number) {
+ return number >= 0 && number < count;
+ }
+
+ /**
+ * Gets the sub handler class to handle this Channel group.
+ *
+ * @return the sub handler class
+ */
+ public AbstractLcnModuleSubHandler createSubHandler(LcnModuleHandler handler, ModInfo info) {
+ return handlerFactory.apply(handler, info);
+ }
+
+ /**
+ * Converts a given table ID into the corresponding Channel group.
+ *
+ * @param tableId to convert
+ * @return the channel group
+ * @throws LcnException when the ID is out of range
+ */
+ public static LcnChannelGroup fromTableId(int tableId) throws LcnException {
+ switch (tableId) {
+ case 0:
+ return KEYLOCKTABLEA;
+ case 1:
+ return KEYLOCKTABLEB;
+ case 2:
+ return KEYLOCKTABLEC;
+ case 3:
+ return KEYLOCKTABLED;
+ default:
+ throw new LcnException("Unknown key table ID: " + tableId);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java
new file mode 100644
index 0000000000000..6ad123bc8affe
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnDefs.java
@@ -0,0 +1,156 @@
+/**
+ * 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.lcn.internal.common;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Common definitions and helpers for the PCK protocol.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+public final class LcnDefs {
+ /** Text encoding used by LCN-PCHK. */
+ public static final Charset LCN_ENCODING = StandardCharsets.UTF_8;
+ /** Number of thresholds registers of an LCN module */
+ public static final int THRESHOLD_REGISTER_COUNT = 4;
+ /** Number of key tables of an LCN module. */
+ public static final int KEY_TABLE_COUNT = 4;
+ /** Number of thresholds before LCN module firmware version 2013 */
+ public static final int THRESHOLD_COUNT_BEFORE_2013 = 5;
+ /**
+ * Default dimmer output ramp when used with roller shutters. Results in a switching delay of 600ms. Value copied
+ * from the LCN-PRO motor/shutter command dialog.
+ */
+ public static final int ROLLER_SHUTTER_RAMP_MS = 4000;
+ /** Max. value of a variable, threshold or regulator setpoint */
+ public static final int MAX_VARIABLE_VALUE = 32768;
+ /** The fixed ramp when output 1+2 are controlled */
+ public static final int FIXED_RAMP_MS = 250;
+ /** Authentication at LCN-PCHK: Request user name. */
+ public static final String AUTH_USERNAME = "Username:";
+ /** Authentication at LCN-PCHK: Request password. */
+ public static final String AUTH_PASSWORD = "Password:";
+ /** LCN-PK/PKU is connected. */
+ public static final String LCNCONNSTATE_CONNECTED = "$io:#LCN:connected";
+ /** LCN-PK/PKU is disconnected. */
+ public static final String LCNCONNSTATE_DISCONNECTED = "$io:#LCN:disconnected";
+ /** LCN-PCHK/PKE has not enough licenses to handle this connection. */
+ public static final String INSUFFICIENT_LICENSES = "$err:(license?)";
+
+ /**
+ * LCN dimming mode.
+ * If solely modules with firmware 170206 or newer are present, LCN-PRO automatically programs {@link #NATIVE200}.
+ * Otherwise the default is {@link #NATIVE50}.
+ * Since LCN-PCHK doesn't know the current mode, it must explicitly be set.
+ */
+ public enum OutputPortDimMode {
+ NATIVE50, // 0..50 dimming steps (all LCN module generations)
+ NATIVE200 // 0..200 dimming steps (since 170206)
+ }
+
+ /**
+ * Tells LCN-PCHK how to format output-port status-messages.
+ * {@link #NATIVE} allows to show the status in half-percent steps (e.g. "10.5").
+ * {@link #NATIVE} is completely backward compatible and there are no restrictions
+ * concerning the LCN module generations. It requires LCN-PCHK 2.3 or higher though.
+ */
+ public enum OutputPortStatusMode {
+ PERCENT, // Default (compatible with all versions of LCN-PCHK)
+ NATIVE // 0..200 steps (since LCN-PCHK 2.3)
+ }
+
+ /** Possible states for LCN LEDs. */
+ public enum LedStatus {
+ OFF,
+ ON,
+ BLINK,
+ FLICKER;
+ }
+
+ /** Possible states for LCN logic-operations. */
+ public enum LogicOpStatus {
+ NOT,
+ OR, // Note: Actually not correct since AND won't be OR also
+ AND;
+ }
+
+ /** Time units used for several LCN commands. */
+ public enum TimeUnit {
+ SECONDS,
+ MINUTES,
+ HOURS,
+ DAYS;
+ }
+
+ /** Relay-state modifiers used in LCN commands. */
+ public enum RelayStateModifier {
+ ON,
+ OFF,
+ TOGGLE,
+ NOCHANGE
+ }
+
+ /** Value-reference for relative LCN variable commands. */
+ public enum RelVarRef {
+ CURRENT,
+ PROG // Programmed value (LCN-PRO). Relevant for set-points and thresholds.
+ }
+
+ /** Command types used when sending LCN keys. */
+ public enum SendKeyCommand {
+ HIT,
+ MAKE,
+ BREAK,
+ DONTSEND
+ }
+
+ /** Key-lock modifiers used in LCN commands. */
+ public enum KeyLockStateModifier {
+ ON,
+ OFF,
+ TOGGLE,
+ NOCHANGE
+ }
+
+ /** List of key tables of an LCN module */
+ public enum KeyTable {
+ A,
+ B,
+ C,
+ D
+ }
+
+ /**
+ * Generates an array of booleans from an input integer (actually a byte).
+ *
+ * @param input the input byte (0..255)
+ * @return the array of 8 booleans
+ * @throws IllegalArgumentException if input is out of range (not a byte)
+ */
+ public static boolean[] getBooleanValue(int inputByte) throws IllegalArgumentException {
+ if (inputByte < 0 || inputByte > 255) {
+ throw new IllegalArgumentException();
+ }
+ boolean[] result = new boolean[8];
+ for (int i = 0; i < 8; ++i) {
+ result[i] = (inputByte & (1 << i)) != 0;
+ }
+ return result;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java
new file mode 100644
index 0000000000000..3731bf000b6a3
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/LcnException.java
@@ -0,0 +1,37 @@
+/**
+ * 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.lcn.internal.common;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Default checked exception.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnException extends Exception {
+ private static final long serialVersionUID = -4341882774124288028L;
+
+ public LcnException() {
+ super();
+ }
+
+ public LcnException(String message) {
+ super(message);
+ }
+
+ public LcnException(Exception e) {
+ super(e);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java
new file mode 100644
index 0000000000000..5c76e562639e3
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java
@@ -0,0 +1,780 @@
+/**
+ * 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.lcn.internal.common;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Helpers to generate LCN-PCK commands.
+ *
+ * LCN-PCK is the command-syntax used by LCN-PCHK to send and receive LCN commands.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+public final class PckGenerator {
+ private static final Logger LOGGER = LoggerFactory.getLogger(PckGenerator.class);
+ /** Termination character after a PCK message */
+ public static final String TERMINATION = "\n";
+
+ /**
+ * Generates a keep-alive.
+ * LCN-PCHK will close the connection if it does not receive any commands from
+ * an open {@link Connection} for a specific period (10 minutes by default).
+ *
+ * @param counter the current ping's id (optional, but "best practice"). Should start with 1
+ * @return the PCK command as text
+ */
+ public static String ping(int counter) {
+ return String.format("^ping%d", counter);
+ }
+
+ /**
+ * Generates a PCK command that will set the LCN-PCHK connection's operation mode.
+ * This influences how output-port commands and status are interpreted and must be
+ * in sync with the LCN bus.
+ *
+ * @param dimMode see {@link LcnDefs.OutputPortDimMode}
+ * @param statusMode see {@link LcnDefs.OutputPortStatusMode}
+ * @return the PCK command as text
+ */
+ public static String setOperationMode(LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode) {
+ return "!OM" + (dimMode == LcnDefs.OutputPortDimMode.NATIVE200 ? "1" : "0")
+ + (statusMode == LcnDefs.OutputPortStatusMode.PERCENT ? "P" : "N");
+ }
+
+ /**
+ * Generates a PCK address header.
+ * Used for commands to LCN modules and groups.
+ *
+ * @param addr the target's address (module or group)
+ * @param localSegId the local segment id where the physical bus connection is located
+ * @param wantsAck true to claim an acknowledge / receipt from the target
+ * @return the PCK address header as text
+ */
+ public static String generateAddressHeader(LcnAddr addr, int localSegId, boolean wantsAck) {
+ return String.format(">%s%03d%03d%s", addr.isGroup() ? "G" : "M", addr.getPhysicalSegmentId(localSegId),
+ addr.getId(), wantsAck ? "!" : ".");
+ }
+
+ /**
+ * Generates a scan-command for LCN segment-couplers.
+ * Used to detect the local segment (where the physical bus connection is located).
+ *
+ * @return the PCK command (without address header) as text
+ */
+ public static String segmentCouplerScan() {
+ return "SK";
+ }
+
+ /**
+ * Generates a firmware/serial-number request.
+ *
+ * @return the PCK command (without address header) as text
+ */
+ public static String requestSn() {
+ return "SN";
+ }
+
+ /**
+ * Generates a command to request a part of a name of a module.
+ *
+ * @param partNumber 0..1
+ * @return the PCK command (without address header) as text
+ */
+ public static String requestModuleName(int partNumber) {
+ return "NMN" + (partNumber + 1);
+ }
+
+ /**
+ * Generates an output-port status request.
+ *
+ * @param outputId 0..3
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String requestOutputStatus(int outputId) throws LcnException {
+ if (outputId < 0 || outputId > 3) {
+ throw new LcnException();
+ }
+ return String.format("SMA%d", outputId + 1);
+ }
+
+ /**
+ * Generates a dim command for a single output-port.
+ *
+ * @param outputId 0..3
+ * @param percent 0..100
+ * @param rampMs ramp in milliseconds
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String dimOutput(int outputId, double percent, int rampMs) throws LcnException {
+ if (outputId < 0 || outputId > 3) {
+ throw new LcnException();
+ }
+ int rampNative = PckGenerator.timeToRampValue(rampMs);
+ int n = (int) Math.round(percent * 2);
+ if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
+ return String.format("A%dDI%03d%03d", outputId + 1, n / 2, rampNative);
+ } else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
+ return String.format("O%dDI%03d%03d", outputId + 1, n, rampNative);
+ }
+ }
+
+ /**
+ * Generates a dim command for all output-ports.
+ *
+ * Attention: This command is supported since module firmware version 180501 AND LCN-PCHK 2.61
+ *
+ * @param firstPercent dimmer value of the first output 0..100
+ * @param secondPercent dimmer value of the first output 0..100
+ * @param thirdPercent dimmer value of the first output 0..100
+ * @param fourthPercent dimmer value of the first output 0..100
+ * @param rampMs ramp in milliseconds
+ * @return the PCK command (without address header) as text
+ */
+ public static String dimAllOutputs(double firstPercent, double secondPercent, double thirdPercent,
+ double fourthPercent, int rampMs) {
+ long n1 = Math.round(firstPercent * 2);
+ long n2 = Math.round(secondPercent * 2);
+ long n3 = Math.round(thirdPercent * 2);
+ long n4 = Math.round(fourthPercent * 2);
+
+ return String.format("OY%03d%03d%03d%03d%03d", n1, n2, n3, n4, timeToRampValue(rampMs));
+ }
+
+ /**
+ * Generates a control command for switching all outputs ON or OFF with a fixed ramp of 0.5s.
+ *
+ * @param percent 0..100
+ * @returnthe PCK command (without address header) as text
+ */
+ public static String controlAllOutputs(double percent) {
+ return String.format("AH%03d", Math.round(percent));
+ }
+
+ /**
+ * Generates a control command for switching dimmer output 1 and 2 both ON or OFF with a fixed ramp of 0.5s or
+ * without ramp.
+ *
+ * @param on true, if outputs shall be switched on
+ * @param ramp true, if the ramp shall be 0.5s, else 0s
+ * @return the PCK command (without address header) as text
+ */
+ public static String controlOutputs12(boolean on, boolean ramp) {
+ int commandByte;
+ if (on) {
+ commandByte = ramp ? 0xC8 : 0xFD;
+ } else {
+ commandByte = ramp ? 0x00 : 0xFC;
+ }
+ return String.format("X2%03d%03d%03d", 1, commandByte, commandByte);
+ }
+
+ /**
+ * Generates a dim command for setting the brightness of dimmer output 1 and 2 with a fixed ramp of 0.5s.
+ *
+ * @param percent brightness of both outputs 0..100
+ * @return the PCK command (without address header) as text
+ */
+ public static String dimOutputs12(double percent) {
+ long localPercent = Math.round(percent);
+ return String.format("AY%03d%03d", localPercent, localPercent);
+ }
+
+ /**
+ * Let an output flicker.
+ *
+ * @param outputId output id 0..3
+ * @param depth flicker depth, the higher the deeper 0..2
+ * @param ramp the flicker speed 0..2
+ * @param count number of flashes 1..15
+ * @return the PCK command (without address header) as text
+ * @throws LcnException when the input values are out of range
+ */
+ public static String flickerOutput(int outputId, int depth, int ramp, int count) throws LcnException {
+ if (outputId < 0 || outputId > 3) {
+ throw new LcnException("Output number out of range");
+ }
+ if (count < 1 || count > 15) {
+ throw new LcnException("Number of flashes out of range");
+ }
+ String depthString;
+ switch (depth) {
+ case 0:
+ depthString = "G";
+ break;
+ case 1:
+ depthString = "M";
+ break;
+ case 2:
+ depthString = "S";
+ break;
+ default:
+ throw new LcnException("Depth out of range");
+ }
+ String rampString;
+ switch (ramp) {
+ case 0:
+ rampString = "L";
+ break;
+ case 1:
+ rampString = "M";
+ break;
+ case 2:
+ rampString = "S";
+ break;
+ default:
+ throw new LcnException("Ramp out of range");
+ }
+ return String.format("A%dFL%s%s%02d", outputId + 1, depthString, rampString, count);
+ }
+
+ /**
+ * Generates a command to change the value of an output-port.
+ *
+ * @param outputId 0..3
+ * @param percent -100..100
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String relOutput(int outputId, double percent) throws LcnException {
+ if (outputId < 0 || outputId > 3) {
+ throw new LcnException();
+ }
+ int n = (int) Math.round(percent * 2);
+ if ((n % 2) == 0) { // Use the percent command (supported by all LCN-PCHK versions)
+ return String.format("A%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n / 2));
+ } else { // We have a ".5" value. Use the native command (supported since LCN-PCHK 2.3)
+ return String.format("O%d%s%03d", outputId + 1, percent >= 0 ? "AD" : "SB", Math.abs(n));
+ }
+ }
+
+ /**
+ * Generates a command that toggles a single output-port (on->off, off->on).
+ *
+ * @param outputId 0..3
+ * @param ramp see {@link PckGenerator#timeToRampValue(int)}
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String toggleOutput(int outputId, int ramp) throws LcnException {
+ if (outputId < 0 || outputId > 3) {
+ throw new LcnException();
+ }
+ return String.format("A%dTA%03d", outputId + 1, ramp);
+ }
+
+ /**
+ * Generates a command that toggles all output-ports (on->off, off->on).
+ *
+ * @param ramp see {@link PckGenerator#timeToRampValue(int)}
+ * @return the PCK command (without address header) as text
+ */
+ public static String toggleAllOutputs(int ramp) {
+ return String.format("AU%03d", ramp);
+ }
+
+ /**
+ * Generates a relays-status request.
+ *
+ * @return the PCK command (without address header) as text
+ */
+ public static String requestRelaysStatus() {
+ return "SMR";
+ }
+
+ /**
+ * Generates a command to control relays.
+ *
+ * @param states the 8 modifiers for the relay states
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String controlRelays(LcnDefs.RelayStateModifier[] states) throws LcnException {
+ if (states.length != 8) {
+ throw new LcnException();
+ }
+ StringBuilder ret = new StringBuilder("R8");
+ for (int i = 0; i < 8; ++i) {
+ switch (states[i]) {
+ case ON:
+ ret.append("1");
+ break;
+ case OFF:
+ ret.append("0");
+ break;
+ case TOGGLE:
+ ret.append("U");
+ break;
+ case NOCHANGE:
+ ret.append("-");
+ break;
+ default:
+ throw new LcnException();
+ }
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Generates a binary-sensors status request.
+ *
+ * @return the PCK command (without address header) as text
+ */
+ public static String requestBinSensorsStatus() {
+ return "SMB";
+ }
+
+ /**
+ * Generates a command that sets a variable absolute.
+ *
+ * @param number regulator number 0..1
+ * @param value the absolute value to set
+ * @return the PCK command (without address header) as text
+ * @throws LcnException
+ */
+ public static String setSetpointAbsolute(int number, int value) {
+ int internalValue = value;
+ // Set absolute (not in PCK yet)
+ int b1 = number << 6; // 01000000
+ b1 |= 0x20; // xx10xxxx (set absolute)
+ if (value < 1000) {
+ internalValue = 1000 - internalValue;
+ b1 |= 8;
+ } else {
+ internalValue -= 1000;
+ }
+ b1 |= (internalValue >> 8) & 0x0f; // xxxx1111
+ int b2 = internalValue & 0xff;
+ return String.format("X2%03d%03d%03d", 30, b1, b2);
+ }
+
+ /**
+ * Generates a command to change the value of a variable.
+ *
+ * @param variable the target variable to change
+ * @param type the reference-point
+ * @param value the native LCN value to add/subtract (can be negative)
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if command is not supported
+ */
+ public static String setVariableRelative(Variable variable, LcnDefs.RelVarRef type, int value) {
+ if (variable.getNumber() == 0) {
+ // Old command for variable 1 / T-var (compatible with all modules)
+ return String.format("Z%s%d", value >= 0 ? "A" : "S", Math.abs(value));
+ } else { // New command for variable 1-12 (compatible with all modules, since LCN-PCHK 2.8)
+ return String.format("Z%s%03d%d", value >= 0 ? "+" : "-", variable.getNumber() + 1, Math.abs(value));
+ }
+ }
+
+ /**
+ * Generates a command the change the value of a regulator setpoint relative.
+ *
+ * @param number 0..1
+ * @param type relative to the current or to the programmed value
+ * @param value the relative value -4000..+4000
+ * @return the PCK command (without address header) as text
+ */
+ public static String setSetpointRelative(int number, LcnDefs.RelVarRef type, int value) {
+ return String.format("RE%sS%s%s%d", number == 0 ? "A" : "B", type == LcnDefs.RelVarRef.CURRENT ? "A" : "P",
+ value >= 0 ? "+" : "-", Math.abs(value));
+ }
+
+ /**
+ * Generates a command the change the value of a threshold relative.
+ *
+ * @param variable the threshold to change
+ * @param type relative to the current or to the programmed value
+ * @param value the relative value -4000..+4000
+ * @param is2013 true, if the LCN module's firmware is equal to or newer than 2013
+ * @return the PCK command (without address header) as text
+ */
+ public static String setThresholdRelative(Variable variable, LcnDefs.RelVarRef type, int value, boolean is2013)
+ throws LcnException {
+ if (is2013) { // New command for registers 1-4 (since 170206, LCN-PCHK 2.8)
+ return String.format("SS%s%04d%sR%d%d", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
+ value >= 0 ? "A" : "S", variable.getNumber() + 1, variable.getThresholdNumber().get() + 1);
+ } else if (variable.getNumber() == 0) { // Old command for register 1 (before 170206)
+ return String.format("SS%s%04d%s%s%s%s%s%s", type == LcnDefs.RelVarRef.CURRENT ? "R" : "E", Math.abs(value),
+ value >= 0 ? "A" : "S", variable.getThresholdNumber().get() == 0 ? "1" : "0",
+ variable.getThresholdNumber().get() == 1 ? "1" : "0",
+ variable.getThresholdNumber().get() == 2 ? "1" : "0",
+ variable.getThresholdNumber().get() == 3 ? "1" : "0",
+ variable.getThresholdNumber().get() == 4 ? "1" : "0");
+ } else {
+ throw new LcnException(
+ "Module does not have threshold register " + (variable.getThresholdNumber().get() + 1));
+ }
+ }
+
+ /**
+ * Generates a variable value request.
+ *
+ * @param variable the variable to request
+ * @param firmwareVersion the target module's firmware version
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if command is not supported
+ */
+ public static String requestVarStatus(Variable variable, int firmwareVersion) throws LcnException {
+ if (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013) {
+ int id = variable.getNumber();
+ switch (variable.getType()) {
+ case UNKNOWN:
+ throw new LcnException("Variable unknown");
+ case VARIABLE:
+ return String.format("MWT%03d", id + 1);
+ case REGULATOR:
+ return String.format("MWS%03d", id + 1);
+ case THRESHOLD:
+ return String.format("SE%03d", id + 1); // Whole register
+ case S0INPUT:
+ return String.format("MWC%03d", id + 1);
+ }
+ throw new LcnException("Unsupported variable type: " + variable);
+ } else {
+ switch (variable) {
+ case VARIABLE1:
+ return "MWV";
+ case VARIABLE2:
+ return "MWTA";
+ case VARIABLE3:
+ return "MWTB";
+ case RVARSETPOINT1:
+ return "MWSA";
+ case RVARSETPOINT2:
+ return "MWSB";
+ case THRESHOLDREGISTER11:
+ case THRESHOLDREGISTER12:
+ case THRESHOLDREGISTER13:
+ case THRESHOLDREGISTER14:
+ case THRESHOLDREGISTER15:
+ return "SL1"; // Whole register
+ default:
+ throw new LcnException("Unsupported variable type: " + variable);
+ }
+ }
+ }
+
+ /**
+ * Generates a request for LED and logic-operations states.
+ *
+ * @return the PCK command (without address header) as text
+ */
+ public static String requestLedsAndLogicOpsStatus() {
+ return "SMT";
+ }
+
+ /**
+ * Generates a command to the set the state of a single LED.
+ *
+ * @param ledId 0..11
+ * @param state the state to set
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String controlLed(int ledId, LcnDefs.LedStatus state) throws LcnException {
+ if (ledId < 0 || ledId > 11) {
+ throw new LcnException();
+ }
+ return String.format("LA%03d%s", ledId + 1, state == LcnDefs.LedStatus.OFF ? "A"
+ : state == LcnDefs.LedStatus.ON ? "E" : state == LcnDefs.LedStatus.BLINK ? "B" : "F");
+ }
+
+ /**
+ * Generates a command to send LCN keys.
+ *
+ * @param cmds the 4 concrete commands to send for the tables (A-D)
+ * @param keys the tables' 8 key-states (true means "send")
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String sendKeys(LcnDefs.SendKeyCommand[] cmds, boolean[] keys) throws LcnException {
+ if (cmds.length != 4 || keys.length != 8) {
+ throw new LcnException();
+ }
+ StringBuilder ret = new StringBuilder("TS");
+ for (int i = 0; i < 4; ++i) {
+ switch (cmds[i]) {
+ case HIT:
+ ret.append("K");
+ break;
+ case MAKE:
+ ret.append("L");
+ break;
+ case BREAK:
+ ret.append("O");
+ break;
+ case DONTSEND:
+ // By skipping table D (if it is not used), we use the old command
+ // for table A-C which is compatible with older LCN modules
+ if (i < 3) {
+ ret.append("-");
+ }
+ break;
+ default:
+ throw new LcnException();
+ }
+ }
+ for (int i = 0; i < 8; ++i) {
+ ret.append(keys[i] ? "1" : "0");
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Generates a command to send LCN keys deferred / delayed.
+ *
+ * @param tableId 0(A)..3(D)
+ * @param time the delay time
+ * @param timeUnit the time unit
+ * @param keys the key-states (true means "send")
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String sendKeysHitDefered(int tableId, int time, LcnDefs.TimeUnit timeUnit, boolean[] keys)
+ throws LcnException {
+ if (tableId < 0 || tableId > 3 || keys.length != 8) {
+ throw new LcnException();
+ }
+ StringBuilder ret = new StringBuilder("TV");
+ switch (tableId) {
+ case 0:
+ ret.append("A");
+ break;
+ case 1:
+ ret.append("B");
+ break;
+ case 2:
+ ret.append("C");
+ break;
+ case 3:
+ ret.append("D");
+ break;
+ default:
+ throw new LcnException();
+ }
+ ret.append(String.format("%03d", time));
+ switch (timeUnit) {
+ case SECONDS:
+ if (time < 1 || time > 60) {
+ throw new LcnException();
+ }
+ ret.append("S");
+ break;
+ case MINUTES:
+ if (time < 1 || time > 90) {
+ throw new LcnException();
+ }
+ ret.append("M");
+ break;
+ case HOURS:
+ if (time < 1 || time > 50) {
+ throw new LcnException();
+ }
+ ret.append("H");
+ break;
+ case DAYS:
+ if (time < 1 || time > 45) {
+ throw new LcnException();
+ }
+ ret.append("D");
+ break;
+ default:
+ throw new LcnException();
+ }
+ for (int i = 0; i < 8; ++i) {
+ ret.append(keys[i] ? "1" : "0");
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Generates a request for key-lock states.
+ * Always requests table A-D. Supported since LCN-PCHK 2.8.
+ *
+ * @return the PCK command (without address header) as text
+ */
+ public static String requestKeyLocksStatus() {
+ return "STX";
+ }
+
+ /**
+ * Generates a command to lock keys.
+ *
+ * @param tableId 0(A)..3(D)
+ * @param states the 8 key-lock modifiers
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String lockKeys(int tableId, LcnDefs.KeyLockStateModifier[] states) throws LcnException {
+ if (tableId < 0 || tableId > 3 || states.length != 8) {
+ throw new LcnException();
+ }
+ StringBuilder ret = new StringBuilder(
+ String.format("TX%s", tableId == 0 ? "A" : tableId == 1 ? "B" : tableId == 2 ? "C" : "D"));
+ for (int i = 0; i < 8; ++i) {
+ switch (states[i]) {
+ case ON:
+ ret.append("1");
+ break;
+ case OFF:
+ ret.append("0");
+ break;
+ case TOGGLE:
+ ret.append("U");
+ break;
+ case NOCHANGE:
+ ret.append("-");
+ break;
+ default:
+ throw new LcnException();
+ }
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Generates a command to lock keys for table A temporary.
+ * There is no hardware-support for locking tables B-D.
+ *
+ * @param time the lock time
+ * @param timeUnit the time unit
+ * @param keys the 8 key-lock states (true means lock)
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String lockKeyTabATemporary(int time, LcnDefs.TimeUnit timeUnit, boolean[] keys) throws LcnException {
+ if (keys.length != 8) {
+ throw new LcnException();
+ }
+ StringBuilder ret = new StringBuilder(String.format("TXZA%03d", time));
+ switch (timeUnit) {
+ case SECONDS:
+ if (time < 1 || time > 60) {
+ throw new LcnException();
+ }
+ ret.append("S");
+ break;
+ case MINUTES:
+ if (time < 1 || time > 90) {
+ throw new LcnException();
+ }
+ ret.append("M");
+ break;
+ case HOURS:
+ if (time < 1 || time > 50) {
+ throw new LcnException();
+ }
+ ret.append("H");
+ break;
+ case DAYS:
+ if (time < 1 || time > 45) {
+ throw new LcnException();
+ }
+ ret.append("D");
+ break;
+ default:
+ throw new LcnException();
+ }
+ for (int i = 0; i < 8; ++i) {
+ ret.append(keys[i] ? "1" : "0");
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Generates the command header / start for sending dynamic texts.
+ * Used by LCN-GTxD periphery (supports 4 text rows).
+ * To complete the command, the text to send must be appended (UTF-8 encoding).
+ * Texts are split up into up to 5 parts with 12 "UTF-8 bytes" each.
+ *
+ * @param row 0..3
+ * @param part 0..4
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String dynTextHeader(int row, int part) throws LcnException {
+ if (row < 0 || row > 3 || part < 0 || part > 4) {
+ throw new LcnException("Row number is out of range: " + (row + 1));
+ }
+ return String.format("GTDT%d%d", row + 1, part + 1);
+ }
+
+ /**
+ * Generates a command to lock a regulator.
+ *
+ * @param regId 0..1
+ * @param state the lock state
+ * @return the PCK command (without address header) as text
+ * @throws LcnException if out of range
+ */
+ public static String lockRegulator(int regId, boolean state) throws LcnException {
+ if (regId < 0 || regId > 1) {
+ throw new LcnException();
+ }
+ return String.format("RE%sX%s", regId == 0 ? "A" : "B", state ? "S" : "A");
+ }
+
+ /**
+ * Generates a null command, used for broadcast messages.
+ *
+ * @return the PCK command (without address header) as text
+ */
+ public static String nullCommand() {
+ return "LEER";
+ }
+
+ /**
+ * Converts the given time into an LCN ramp value.
+ *
+ * @param timeMSec the time in milliseconds
+ * @return the (LCN-internal) ramp value (0..250)
+ */
+ private static int timeToRampValue(int timeMSec) {
+ int ret;
+ if (timeMSec < 250) {
+ ret = 0;
+ } else if (timeMSec < 500) {
+ ret = 1;
+ } else if (timeMSec < 660) {
+ ret = 2;
+ } else if (timeMSec < 1000) {
+ ret = 3;
+ } else if (timeMSec < 1400) {
+ ret = 4;
+ } else if (timeMSec < 2000) {
+ ret = 5;
+ } else if (timeMSec < 3000) {
+ ret = 6;
+ } else if (timeMSec < 4000) {
+ ret = 7;
+ } else if (timeMSec < 5000) {
+ ret = 8;
+ } else if (timeMSec < 6000) {
+ ret = 9;
+ } else {
+ ret = (timeMSec / 1000 - 6) / 2 + 10;
+ if (ret > 250) {
+ ret = 250;
+ LOGGER.warn("Ramp value is too high. Limiting value to 486s.");
+ }
+ }
+ return ret;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java
new file mode 100644
index 0000000000000..70a6c1fc82bd8
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/ReverseNumber.java
@@ -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.lcn.internal.common;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Helper to bitwise reverse numbers.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ */
+@NonNullByDefault
+final class ReverseNumber {
+ /** Cache with all reversed 8 bit values. */
+ private static final int[] REVERSED_UINT8 = new int[256];
+
+ /** Initializes static data once this class is first used. */
+ static {
+ for (int i = 0; i < 256; ++i) {
+ int reversed = 0;
+ for (int j = 0; j < 8; ++j) {
+ if ((i & (1 << j)) != 0) {
+ reversed |= (0x80 >> j);
+ }
+ }
+ REVERSED_UINT8[i] = reversed;
+ }
+ }
+
+ /**
+ * Reverses the given 8 bit value bitwise.
+ *
+ * @param value the value to reverse bitwise (treated as unsigned 8 bit value)
+ * @return the reversed value
+ * @throws LcnException if value is out of range (not unsigned 8 bit)
+ */
+ static int reverseUInt8(int value) throws LcnException {
+ if (value < 0 || value > 255) {
+ throw new LcnException();
+ }
+ return REVERSED_UINT8[value];
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java
new file mode 100644
index 0000000000000..c32573988d5ee
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/Variable.java
@@ -0,0 +1,278 @@
+/**
+ * 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.lcn.internal.common;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+
+/**
+ * LCN variable types.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+public enum Variable {
+ UNKNOWN(0, Type.UNKNOWN, LcnChannelGroup.VARIABLE), // Used if the real type is not known (yet)
+ VARIABLE1(0, Type.VARIABLE, LcnChannelGroup.VARIABLE), // or TVar
+ VARIABLE2(1, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE3(2, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE4(3, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE5(4, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE6(5, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE7(6, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE8(7, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE9(8, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE10(9, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE11(10, Type.VARIABLE, LcnChannelGroup.VARIABLE),
+ VARIABLE12(11, Type.VARIABLE, LcnChannelGroup.VARIABLE), // Since 170206
+ RVARSETPOINT1(0, Type.REGULATOR, LcnChannelGroup.RVARSETPOINT),
+ RVARSETPOINT2(1, Type.REGULATOR, LcnChannelGroup.RVARSETPOINT), // Set-points for regulators
+ THRESHOLDREGISTER11(0, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
+ THRESHOLDREGISTER12(0, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
+ THRESHOLDREGISTER13(0, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
+ THRESHOLDREGISTER14(0, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
+ // Register 1 (THRESHOLDREGISTER15 only before 170206)
+ THRESHOLDREGISTER15(0, 4, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER1),
+ THRESHOLDREGISTER21(1, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2),
+ THRESHOLDREGISTER22(1, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2),
+ THRESHOLDREGISTER23(1, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2),
+ THRESHOLDREGISTER24(1, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER2), // Register 2 (since 2012)
+ THRESHOLDREGISTER31(2, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3),
+ THRESHOLDREGISTER32(2, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3),
+ THRESHOLDREGISTER33(2, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3),
+ THRESHOLDREGISTER34(2, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER3), // Register 3 (since 2012)
+ THRESHOLDREGISTER41(3, 0, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4),
+ THRESHOLDREGISTER42(3, 1, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4),
+ THRESHOLDREGISTER43(3, 2, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4),
+ THRESHOLDREGISTER44(3, 3, Type.THRESHOLD, LcnChannelGroup.THRESHOLDREGISTER4), // Register 4 (since 2012)
+ S0INPUT1(0, Type.S0INPUT, LcnChannelGroup.S0INPUT),
+ S0INPUT2(1, Type.S0INPUT, LcnChannelGroup.S0INPUT),
+ S0INPUT3(2, Type.S0INPUT, LcnChannelGroup.S0INPUT),
+ S0INPUT4(3, Type.S0INPUT, LcnChannelGroup.S0INPUT); // LCN-BU4L
+
+ private final int number;
+ private final Optional thresholdNumber;
+ private final Type type;
+ private final LcnChannelGroup channelGroup;
+
+ /**
+ * Defines the origin of an LCN variable.
+ */
+ public enum Type {
+ UNKNOWN,
+ VARIABLE,
+ REGULATOR,
+ THRESHOLD,
+ S0INPUT
+ }
+
+ Variable(int number, Type type, LcnChannelGroup channelGroup) {
+ this(number, Optional.empty(), type, channelGroup);
+ }
+
+ Variable(int number, int thresholdNumber, Type type, LcnChannelGroup channelGroup) {
+ this(number, Optional.of(thresholdNumber), type, channelGroup);
+ }
+
+ Variable(int number, Optional thresholdNumber, Type type, LcnChannelGroup channelGroup) {
+ this.number = number;
+ this.type = type;
+ this.channelGroup = channelGroup;
+ this.thresholdNumber = thresholdNumber;
+ }
+
+ /**
+ * Gets the type of the variable's origin.
+ *
+ * @return the type
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Gets the channel type of the variable.
+ *
+ * @return the channel type
+ */
+ public LcnChannelGroup getChannelType() {
+ return channelGroup;
+ }
+
+ /**
+ * Gets the threshold number within a threshold register.
+ *
+ * @return the threshold number
+ */
+ public Optional getThresholdNumber() {
+ return thresholdNumber;
+ }
+
+ /**
+ * Gets the threshold register number.
+ *
+ * @return the threshold register number
+ */
+ public int getNumber() {
+ return number;
+ }
+
+ /**
+ * Translates a given id into a variable type.
+ *
+ * @param number 0..11
+ * @return the translated {@link Variable}
+ * @throws LcnException if out of range
+ */
+ public static Variable varIdToVar(int number) throws LcnException {
+ if (number < 0 || number >= LcnChannelGroup.VARIABLE.getCount()) {
+ throw new LcnException("Invalid variable number: " + (number + 1));
+ }
+ return getVariableFromNumberAndType(number, Type.VARIABLE, v -> true);
+ }
+
+ /**
+ * Translates a given id into a LCN set-point variable type.
+ *
+ * @param number 0..1
+ * @return the translated {@link Variable}
+ * @throws LcnException if out of range
+ */
+ public static Variable setPointIdToVar(int number) throws LcnException {
+ if (number < 0 || number >= LcnChannelGroup.RVARSETPOINT.getCount()) {
+ throw new LcnException();
+ }
+
+ return getVariableFromNumberAndType(number, Type.REGULATOR, v -> true);
+ }
+
+ /**
+ * Translates given ids into a LCN threshold variable type.
+ *
+ * @param registerNumber 0..3
+ * @param thresholdNumber 0..4 for register 0, 0..3 for registers 1..3
+ * @return the translated {@link Variable}
+ * @throws LcnException if out of range
+ */
+ public static Variable thrsIdToVar(int registerNumber, int thresholdNumber) throws LcnException {
+ if (registerNumber < 0 || registerNumber >= LcnDefs.THRESHOLD_REGISTER_COUNT) {
+ throw new LcnException("Threshold register number out of range: " + (registerNumber + 1));
+ }
+ if (thresholdNumber < 0 || thresholdNumber >= (registerNumber == 0 ? 5 : 4)) {
+ throw new LcnException("Threshold number out of range: " + (thresholdNumber + 1));
+ }
+ return getVariableFromNumberAndType(registerNumber, Type.THRESHOLD,
+ v -> v.thresholdNumber.get() == thresholdNumber);
+ }
+
+ /**
+ * Translates a given id into a LCN S0-input variable type.
+ *
+ * @param number 0..3
+ * @return the translated {@link Variable}
+ * @throws LcnException if out of range
+ */
+ public static Variable s0IdToVar(int number) throws LcnException {
+ if (number < 0 || number >= LcnChannelGroup.S0INPUT.getCount()) {
+ throw new LcnException();
+ }
+ return getVariableFromNumberAndType(number, Type.S0INPUT, v -> true);
+ }
+
+ private static Variable getVariableFromNumberAndType(int varId, Type type, Predicate filter)
+ throws LcnException {
+ return Stream.of(values()).filter(v -> v.type == type).filter(v -> v.number == varId).filter(filter).findAny()
+ .orElseThrow(LcnException::new);
+ }
+
+ /**
+ * Checks if this variable type uses special values.
+ * Examples for special values: "No value yet", "sensor defective" etc.
+ *
+ * @return true if special values are in use
+ */
+ public boolean useLcnSpecialValues() {
+ return type != Type.S0INPUT;
+ }
+
+ /**
+ * Module-generation check.
+ * Checks if the given variable type would receive a typed response if
+ * its status was requested.
+ *
+ * @param firmwareVersion the target LCN-modules firmware version
+ * @return true if a response would contain the variable's type
+ */
+ public boolean hasTypeInResponse(int firmwareVersion) {
+ return (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013
+ || (type != Type.VARIABLE && type != Type.REGULATOR));
+ }
+
+ /**
+ * Module-generation check.
+ * Checks if the given variable type automatically sends status-updates on
+ * value-change. It must be polled otherwise.
+ *
+ * @param firmwareVersion the target LCN-module's firmware version
+ * @return true if the LCN module supports automatic status-messages for this {@link Variable}
+ */
+ public boolean isEventBased(int firmwareVersion) {
+ return type == Type.REGULATOR || type == Type.S0INPUT || firmwareVersion >= LcnBindingConstants.FIRMWARE_2013;
+ }
+
+ /**
+ * Module-generation check.
+ * Checks if the target LCN module would automatically send status-updates if
+ * the given variable type was changed by command.
+ *
+ * @param variable the variable type to check
+ * @param is2013 the target module's-generation
+ * @return true if a poll is required to get the new status-value
+ */
+ public boolean shouldPollStatusAfterCommand(int firmwareVersion) {
+ // Regulator set-points will send status-messages on every change (all firmware versions)
+ if (type == Type.REGULATOR) {
+ return false;
+ }
+ // Thresholds since 170206 will send status-messages on every change
+ if (firmwareVersion >= LcnBindingConstants.FIRMWARE_2013 && type == Type.THRESHOLD) {
+ return false;
+ }
+ // Others:
+ // - Variables before 170206 will never send any status-messages
+ // - Variables since 170206 only send status-messages on "big" changes
+ // - Thresholds before 170206 will never send any status-messages
+ // - S0-inputs only send status-messages on "big" changes
+ // (all "big changes" cases force us to poll the status to get faster updates)
+ return true;
+ }
+
+ /**
+ * Module-generation check.
+ * Checks if the target LCN module would automatically send status-updates if
+ * the given regulator's lock-state was changed by command.
+ *
+ * @param firmwareVersion the target LCN-module's firmware version
+ * @param lockState the lock-state sent via command
+ * @return true if a poll is required to get the new status-value
+ */
+ public boolean shouldPollStatusAfterRegulatorLock(int firmwareVersion, boolean lockState) {
+ // LCN modules before 170206 will send an automatic status-message for "lock", but not for "unlock"
+ return !lockState && firmwareVersion < LcnBindingConstants.FIRMWARE_2013;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java
new file mode 100644
index 0000000000000..90c6bf83c8cb6
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/VariableValue.java
@@ -0,0 +1,99 @@
+/**
+ * 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.lcn.internal.common;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.types.State;
+
+/**
+ * A value of an LCN variable.
+ *
+ * It internally stores the native LCN value and allows to convert from/into other units.
+ * Some conversions allow to specify whether the source value is absolute or relative.
+ * Relative values are used to create {@link VariableValue}s that can be added/subtracted from
+ * other (absolute) {@link VariableValue}s.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+public class VariableValue {
+ private static final String SENSOR_DEFECTIVE_STATE = "DEFECTIVE";
+
+ /** The absolute, native LCN value. */
+ private final long nativeValue;
+
+ /**
+ * Constructor with native LCN value.
+ *
+ * @param nativeValue the native value
+ */
+ public VariableValue(long nativeValue) {
+ this.nativeValue = nativeValue;
+ }
+
+ /**
+ * Converts to native value. Mask locked bit.
+ *
+ * @return the converted value
+ */
+ public long toNative(boolean useSpecialValues) {
+ if (useSpecialValues) {
+ return nativeValue & 0x7fff;
+ } else {
+ return nativeValue;
+ }
+ }
+
+ /**
+ * Returns the lock state if value comes from a regulator set-point.
+ * If the variable type is not a regulator, the result is undefined.
+ *
+ * @return true if the regulator is locked
+ */
+ public boolean isRegulatorLocked() {
+ return (this.nativeValue & 0x8000) != 0;
+ }
+
+ /**
+ * Returns the defective state of the originating sensor for this variable.
+ *
+ * @return true if the sensor is defective
+ */
+ public boolean isSensorDefective() {
+ return nativeValue == 0x7f00;
+ }
+
+ /**
+ * Returns the configuration state of the variable.
+ *
+ * @return true if the variable is configured via LCN-PRO
+ */
+ public boolean isConfigured() {
+ return this.nativeValue != 0xFFFF;
+ }
+
+ public State getState(Variable variable) {
+ State stateValue;
+ if (variable.useLcnSpecialValues() && isSensorDefective()) {
+ stateValue = new StringType(SENSOR_DEFECTIVE_STATE);
+ } else if (variable.useLcnSpecialValues() && !isConfigured()) {
+ stateValue = new StringType("Not configured in LCN-PRO");
+ } else {
+ stateValue = new DecimalType(toNative(variable.useLcnSpecialValues()));
+ }
+ return stateValue;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java
new file mode 100644
index 0000000000000..13d001868090d
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionState.java
@@ -0,0 +1,100 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.io.IOException;
+import java.nio.channels.Channel;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnAddr;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+
+/**
+ * Base class representing LCN-PCK gateway connection states.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractConnectionState extends AbstractState {
+ /** The PCK gateway's Connection */
+ protected final Connection connection;
+
+ public AbstractConnectionState(ConnectionStateMachine context) {
+ super(context);
+ this.connection = context.getConnection();
+ }
+
+ /**
+ * Callback method when a PCK message has been received.
+ *
+ * @param data the received PCK message without line termination character
+ */
+ public abstract void onPckMessageReceived(String data);
+
+ /**
+ * Gets the framework's scheduler.
+ *
+ * @return the scheduler
+ */
+ public ScheduledExecutorService getScheduler() {
+ return context.getScheduler();
+ }
+
+ /**
+ * Enqueues a PCK message to be sent. When the connection is offline, the message will be buffered and sent when the
+ * connection is established. When the enqueued PCK message is too old, it will be discarded before a new connection
+ * is established.
+ *
+ * @param addr the module's address to which is message shall be sent
+ * @param wantsAck true, if the module shall respond with an Ack upon successful processing
+ * @param data the PCK message to be sent
+ */
+ public void queue(LcnAddr addr, boolean wantsAck, byte[] data) {
+ connection.queueOffline(addr, wantsAck, data);
+ }
+
+ /**
+ * Shuts the Connection down finally. A shut-down connection cannot re-used.
+ */
+ public void shutdownFinally() {
+ nextState(ConnectionStateShutdown::new);
+ }
+
+ /**
+ * Checks if the given PCK message is an LCN bus disconnect message. If so, openHAB will be informed and the
+ * Connection's State Machine waits for a re-connect.
+ *
+ * @param pck the PCK message to check
+ */
+ protected void parseLcnBusDiconnectMessage(String pck) {
+ if (pck.equals(LcnDefs.LCNCONNSTATE_DISCONNECTED)) {
+ connection.getCallback().onOffline("LCN bus not connected to LCN-PCHK/PKE");
+ nextState(ConnectionStateWaitForLcnBusConnectedAfterDisconnected::new);
+ }
+ }
+
+ /**
+ * Closes the Connection SocketChannel.
+ */
+ protected void closeSocketChannel() {
+ try {
+ Channel socketChannel = connection.getSocketChannel();
+ if (socketChannel != null) {
+ socketChannel.close();
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java
new file mode 100644
index 0000000000000..4037bf47e29ca
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractConnectionStateSendCredentials.java
@@ -0,0 +1,48 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Base class for sending username or password.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractConnectionStateSendCredentials extends AbstractConnectionState {
+ private static final int AUTH_TIMEOUT_SEC = 10;
+
+ public AbstractConnectionStateSendCredentials(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ addTimer(getScheduler().schedule(() -> nextState(ConnectionStateConnecting::new), AUTH_TIMEOUT_SEC,
+ TimeUnit.SECONDS));
+ }
+
+ /**
+ * Starts a timeout when the PCK gateway does not answer to the credentials.
+ */
+ protected void startTimeoutTimer() {
+ addTimer(getScheduler().schedule(
+ () -> context.handleConnectionFailed(
+ new LcnException("Network timeout in state " + getClass().getSimpleName())),
+ connection.getSettings().getTimeout(), TimeUnit.MILLISECONDS));
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java
new file mode 100644
index 0000000000000..0b8bbd5a5205b
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractState.java
@@ -0,0 +1,76 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ScheduledFuture;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Base class for all states used with {@link AbstractStateMachine}.
+ *
+ * @param type of the state machine implementation
+ * @param type of the state implementation
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractState, U extends AbstractState> {
+ private final List> usedTimers = Collections.synchronizedList(new ArrayList<>());
+ protected final T context;
+
+ public AbstractState(T context) {
+ this.context = context;
+ }
+
+ /**
+ * Invoked when the State shall start its operation.
+ */
+ protected abstract void startWorking();
+
+ /**
+ * Stops all timers, the State has been started.
+ */
+ protected void cancelAllTimers() {
+ synchronized (usedTimers) {
+ usedTimers.forEach(t -> t.cancel(true));
+ }
+ }
+
+ /**
+ * When a state starts a timer, its ScheduledFuture must be registered by this method. All timers added by this
+ * method, are canceled when the StateMachine leaves this State.
+ *
+ * @param timer the new timer
+ */
+ protected void addTimer(ScheduledFuture> timer) {
+ usedTimers.add(timer);
+ }
+
+ /**
+ * Sets a new State. The current state is torn down gracefully.
+ *
+ * @param newStateFactory the lambda returning the new State
+ */
+ protected void nextState(Function newStateFactory) {
+ synchronized (context) {
+ if (context.isStateActive(this)) {
+ context.setState(newStateFactory);
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java
new file mode 100644
index 0000000000000..fbf44830c440e
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/AbstractStateMachine.java
@@ -0,0 +1,64 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for state machines.
+ *
+ * @param type of the state machine implementation
+ * @param type of the state implementation
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public abstract class AbstractStateMachine, U extends AbstractState> {
+ private final Logger logger = LoggerFactory.getLogger(AbstractStateMachine.class);
+ /** The StateMachine's current state */
+ protected @Nullable volatile U state;
+
+ /**
+ * Sets the current state.
+ *
+ * @param newStateFactory the new state's factory
+ */
+ protected synchronized void setState(Function newStateFactory) {
+ @Nullable
+ U localState = state;
+ if (localState != null) {
+ localState.cancelAllTimers();
+ }
+
+ @SuppressWarnings("unchecked")
+ U newState = newStateFactory.apply((T) this);
+
+ if (localState != null) {
+ logger.debug("Changing state {} -> {}", localState.getClass().getSimpleName(),
+ newState.getClass().getSimpleName());
+ }
+
+ state = newState;
+
+ state.startWorking();
+ }
+
+ protected boolean isStateActive(AbstractState, ?> otherState) {
+ return state == otherState; // compare by identity
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java
new file mode 100644
index 0000000000000..f89a7051c1e8c
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/Connection.java
@@ -0,0 +1,474 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.Channel;
+import java.nio.channels.CompletionHandler;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.lcn.internal.common.LcnAddr;
+import org.openhab.binding.lcn.internal.common.LcnAddrGrp;
+import org.openhab.binding.lcn.internal.common.LcnAddrMod;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class represents a configured connection to one LCN-PCHK.
+ * It uses a {@link AsynchronousSocketChannel} to connect to LCN-PCHK.
+ * Included logic:
+ *
+ * Reconnection on connection loss
+ * Segment scan (to detect the local segment ID)
+ * Acknowledge handling
+ * Periodic value requests
+ * Caching of runtime data about the underlying LCN bus
+ *
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class Connection {
+ private final Logger logger = LoggerFactory.getLogger(Connection.class);
+ private static final int BROADCAST_MODULE_ID = 3;
+ private static final int BROADCAST_SEGMENT_ID = 3;
+ private final ConnectionSettings settings;
+ private final ConnectionCallback callback;
+ @Nullable
+ private AsynchronousSocketChannel channel;
+ /** The local segment id. -1 means "unknown". */
+ private int localSegId;
+ private final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
+ private final ByteArrayOutputStream sendBuffer = new ByteArrayOutputStream();
+ private final Queue<@Nullable SendData> sendQueue = new LinkedBlockingQueue<>();
+ private final BlockingQueue offlineSendQueue = new LinkedBlockingQueue<>();
+ private final Map modData = Collections.synchronizedMap(new HashMap<>());
+ private volatile boolean writeInProgress;
+ private final ScheduledExecutorService scheduler;
+ private final ConnectionStateMachine connectionStateMachine;
+
+ /**
+ * Constructs a clean (disconnected) connection with the given settings.
+ * This does not start the actual connection process.
+ *
+ * @param sets the settings to use for the new connection
+ * @param callback the callback to the owner
+ * @throws IOException
+ */
+ public Connection(ConnectionSettings sets, ScheduledExecutorService scheduler, ConnectionCallback callback) {
+ this.settings = sets;
+ this.callback = callback;
+ this.scheduler = scheduler;
+ this.clearRuntimeData();
+
+ connectionStateMachine = new ConnectionStateMachine(this, scheduler);
+ }
+
+ /** Clears all runtime data. */
+ void clearRuntimeData() {
+ this.channel = null;
+ this.localSegId = -1;
+ this.readBuffer.clear();
+ this.sendQueue.clear();
+ this.sendBuffer.reset();
+ }
+
+ /**
+ * Retrieves the settings for this connection (never changed).
+ *
+ * @return the settings
+ */
+ public ConnectionSettings getSettings() {
+ return this.settings;
+ }
+
+ private boolean isSocketConnected() {
+ try {
+ AsynchronousSocketChannel localChannel = channel;
+ return localChannel != null && localChannel.getRemoteAddress() != null;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Sets the local segment id.
+ *
+ * @param localSegId the new local segment id
+ */
+ public void setLocalSegId(int localSegId) {
+ this.localSegId = localSegId;
+ }
+
+ /**
+ * Called whenever an acknowledge is received.
+ *
+ * @param addr the source LCN module
+ * @param code the LCN internal code (-1 = "positive")
+ */
+ public void onAck(LcnAddrMod addr, int code) {
+ synchronized (modData) {
+ if (modData.containsKey(addr)) {
+ modData.get(addr).onAck(code, this, this.settings.getTimeout(), System.nanoTime());
+ }
+ }
+ }
+
+ /**
+ * Creates and/or returns cached data for the given LCN module.
+ *
+ * @param addr the module's address
+ * @return the data
+ */
+ public ModInfo updateModuleData(LcnAddrMod addr) {
+ return modData.computeIfAbsent(addr, ModInfo::new);
+ }
+
+ /**
+ * Reads and processes input from the underlying channel.
+ * Fragmented input is kept in {@link #readBuffer} and will be processed with the next call.
+ *
+ * @throws IOException if connection was closed or a generic channel error occurred
+ */
+ void readAndProcess() {
+ AsynchronousSocketChannel localChannel = channel;
+ if (localChannel != null && isSocketConnected()) {
+ localChannel.read(readBuffer, null, new CompletionHandler<@Nullable Integer, @Nullable Void>() {
+ @Override
+ public void completed(@Nullable Integer transmittedByteCount, @Nullable Void attachment) {
+ synchronized (Connection.this) {
+ if (transmittedByteCount == null || transmittedByteCount == -1) {
+ String msg = "Connection was closed by foreign host.";
+ connectionStateMachine.handleConnectionFailed(new LcnException(msg));
+ } else {
+ // read data chunks from socket and separate frames
+ readBuffer.flip();
+ int aPos = readBuffer.position(); // 0
+ String s = new String(readBuffer.array(), aPos, transmittedByteCount, LcnDefs.LCN_ENCODING);
+ int pos1 = 0, pos2 = s.indexOf(PckGenerator.TERMINATION, pos1);
+ while (pos2 != -1) {
+ String data = s.substring(pos1, pos2);
+ if (logger.isTraceEnabled()) {
+ logger.trace("Received: '{}'", data);
+ }
+ scheduler.submit(() -> {
+ connectionStateMachine.onInputReceived(data);
+ callback.onPckMessageReceived(data);
+ });
+ // Seek position in input array
+ aPos += s.substring(pos1, pos2 + 1).getBytes(LcnDefs.LCN_ENCODING).length;
+ // Next input
+ pos1 = pos2 + 1;
+ pos2 = s.indexOf(PckGenerator.TERMINATION, pos1);
+ }
+ readBuffer.limit(readBuffer.capacity());
+ readBuffer.position(transmittedByteCount - aPos); // Keeps fragments for the next call
+
+ if (isSocketConnected()) {
+ readAndProcess();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void failed(@Nullable Throwable e, @Nullable Void attachment) {
+ logger.debug("Lost connection");
+ connectionStateMachine.handleConnectionFailed(e);
+ }
+ });
+ } else {
+ connectionStateMachine.handleConnectionFailed(new LcnException("Socket not open"));
+ }
+ }
+
+ /**
+ * Writes all queued data.
+ * Will try to write all data at once to reduce overhead.
+ */
+ public synchronized void triggerWriteToSocket() {
+ AsynchronousSocketChannel localChannel = channel;
+ if (localChannel == null || !isSocketConnected() || writeInProgress) {
+ return;
+ }
+ sendBuffer.reset();
+ SendData item = sendQueue.poll();
+
+ if (item != null) {
+ try {
+ if (!item.write(sendBuffer, localSegId)) {
+ logger.warn("Data loss: Could not write packet into send buffer");
+ }
+
+ writeInProgress = true;
+ byte[] data = sendBuffer.toByteArray();
+ localChannel.write(ByteBuffer.wrap(data), null,
+ new CompletionHandler<@Nullable Integer, @Nullable Void>() {
+ @Override
+ public void completed(@Nullable Integer result, @Nullable Void attachment) {
+ synchronized (Connection.this) {
+ if (result != data.length) {
+ logger.warn("Data loss while writing to channel: {}", settings.getAddress());
+ } else {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Sent: {}", new String(data, 0, data.length));
+ }
+ }
+
+ writeInProgress = false;
+
+ if (sendQueue.size() > 0) {
+ /**
+ * This could lead to stack overflows, since the CompletionHandler may run in
+ * the same Thread as triggerWriteToSocket() is invoked (see
+ * {@link AsynchronousChannelGroup}/Threading), but we do not expect as much
+ * data in one chunk here, that the stack can be filled in a critical way.
+ */
+ triggerWriteToSocket();
+ }
+ }
+ }
+
+ @Override
+ public void failed(@Nullable Throwable exc, @Nullable Void attachment) {
+ synchronized (Connection.this) {
+ if (exc != null) {
+ logger.warn("Writing to channel \"{}\" failed: {}", settings.getAddress(),
+ exc.getMessage());
+ }
+ writeInProgress = false;
+ connectionStateMachine.handleConnectionFailed(new LcnException("write() failed"));
+ }
+ }
+ });
+ } catch (BufferOverflowException | IOException e) {
+ logger.warn("Sending failed: {}: {}: {}", item, e.getClass().getSimpleName(), e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Queues plain text to be sent to LCN-PCHK.
+ * Sending will be done the next time {@link #triggerWriteToSocket()} is called.
+ *
+ * @param plainText the text
+ */
+ public void queueDirectlyPlainText(String plainText) {
+ this.queueAndSend(new SendDataPlainText(plainText));
+ }
+
+ /**
+ * Queues a PCK command to be sent.
+ *
+ * @param addr the target LCN address
+ * @param wantsAck true to wait for acknowledge on receipt (should be false for group addresses)
+ * @param pck the pure PCK command (without address header)
+ */
+ void queueDirectly(LcnAddr addr, boolean wantsAck, String pck) {
+ this.queueDirectly(addr, wantsAck, pck.getBytes(LcnDefs.LCN_ENCODING));
+ }
+
+ /**
+ * Queues a PCK command for immediate sending, regardless of the Connection state. The PCK command is automatically
+ * re-sent if the destination is not a group, an Ack is requested and the module did not answer within the expected
+ * time.
+ *
+ * @param addr the target LCN address
+ * @param wantsAck true to wait for acknowledge on receipt (should be false for group addresses)
+ * @param data the pure PCK command (without address header)
+ */
+ void queueDirectly(LcnAddr addr, boolean wantsAck, byte[] data) {
+ if (!addr.isGroup() && wantsAck) {
+ this.updateModuleData((LcnAddrMod) addr).queuePckCommandWithAck(data, this, this.settings.getTimeout(),
+ System.nanoTime());
+ } else {
+ this.queueAndSend(new SendDataPck(addr, false, data));
+ }
+ }
+
+ /**
+ * Enqueues a raw PCK command and triggers the socket to start sending, if it does not already. Does not take care
+ * of any Acks.
+ *
+ * @param data raw PCK command
+ */
+ synchronized void queueAndSend(SendData data) {
+ this.sendQueue.add(data);
+
+ triggerWriteToSocket();
+ }
+
+ /**
+ * Enqueues a PCK command to the offline queue. Data will be sent when the Connection state will enter
+ * {@link ConnectionStateConnected}.
+ *
+ * @param addr LCN module address
+ * @param wantsAck true, if the LCN module shall respond with an Ack on successful processing
+ * @param data the pure PCK command (without address header)
+ */
+ void queueOffline(LcnAddr addr, boolean wantsAck, byte[] data) {
+ offlineSendQueue.add(new PckQueueItem(addr, wantsAck, data));
+ }
+
+ /**
+ * Enqueues a PCK command for sending. Takes care of the Connection state and buffers the command for a specific
+ * time if the Connection is not ready. If an Ack is requested, the PCK command is automatically
+ * re-sent, if the module did not answer in the expected time.
+ *
+ * @param addr LCN module address
+ * @param wantsAck true, if the LCN module shall respond with an Ack on successful processing
+ * @param pck the pure PCK command (without address header)
+ */
+ public void queue(LcnAddr addr, boolean wantsAck, String pck) {
+ this.queue(addr, wantsAck, pck.getBytes(LcnDefs.LCN_ENCODING));
+ }
+
+ /**
+ * Enqueues a PCK command for sending. Takes care of the Connection state and buffers the command for a specific
+ * time if the Connection is not ready. If an Ack is requested, the PCK command is automatically
+ * re-sent, if the module did not answer in the expected time.
+ *
+ * @param addr LCN module address
+ * @param wantsAck true, if the LCN module shall respond with an Ack on successful processing
+ * @param pck the pure PCK command (without address header)
+ */
+ public void queue(LcnAddr addr, boolean wantsAck, byte[] pck) {
+ connectionStateMachine.queue(addr, wantsAck, pck);
+ }
+
+ /**
+ * Process the offline PCK command queue. Does only send recently enqueued PCK commands, the rest is discarded.
+ */
+ void sendOfflineQueue() {
+ List allItems = new ArrayList<>(offlineSendQueue.size());
+ offlineSendQueue.drainTo(allItems);
+
+ allItems.forEach(item -> {
+ // only send messages that were enqueued recently, discard older messages
+ long timeout = settings.getTimeout();
+ if (item.getEnqueued().isAfter(Instant.now().minus(timeout * 4, ChronoUnit.MILLIS))) {
+ queueDirectly(item.getAddr(), item.isWantsAck(), item.getData());
+ }
+ });
+ }
+
+ /**
+ * Gets the Connection's callback.
+ *
+ * @return the callback
+ */
+ public ConnectionCallback getCallback() {
+ return callback;
+ }
+
+ /**
+ * Sets the SocketChannel of this Connection
+ *
+ * @param channel the new Channel
+ */
+ public void setSocketChannel(AsynchronousSocketChannel channel) {
+ this.channel = channel;
+ }
+
+ /**
+ * Gets the SocketChannel of the Connection.
+ *
+ * @returnthe socket channel
+ */
+ @Nullable
+ public Channel getSocketChannel() {
+ return channel;
+ }
+
+ /**
+ * Gets the local segment ID. When no segments are used, the local segment ID is 0.
+ *
+ * @return the local segment ID
+ */
+ public int getLocalSegId() {
+ return localSegId;
+ }
+
+ /**
+ * Runs the periodic updates on all ModInfos.
+ */
+ public void updateModInfos() {
+ synchronized (modData) {
+ modData.values().forEach(i -> i.update(this, settings.getTimeout(), System.nanoTime()));
+ }
+ }
+
+ /**
+ * Removes an LCN module from the ModData list.
+ *
+ * @param addr the module's address to be removed
+ */
+ public void removeLcnModule(LcnAddr addr) {
+ modData.remove(addr);
+ }
+
+ /**
+ * Invoked when this Connection shall be shut-down finally.
+ */
+ public void shutdown() {
+ connectionStateMachine.shutdownFinally();
+ }
+
+ /**
+ * Sends a broadcast to all LCN modules with a reuqest to respond with an Ack.
+ */
+ public void sendModuleDiscoveryCommand() {
+ queueAndSend(new SendDataPck(new LcnAddrGrp(BROADCAST_SEGMENT_ID, BROADCAST_MODULE_ID), true,
+ PckGenerator.nullCommand().getBytes(LcnDefs.LCN_ENCODING)));
+ queueAndSend(new SendDataPck(new LcnAddrGrp(0, BROADCAST_MODULE_ID), true,
+ PckGenerator.nullCommand().getBytes(LcnDefs.LCN_ENCODING)));
+ }
+
+ /**
+ * Requests the serial number and the firmware version of the given LCN module.
+ *
+ * @param addr module's address
+ */
+ public void sendSerialNumberRequest(LcnAddrMod addr) {
+ queueDirectly(addr, false, PckGenerator.requestSn());
+ }
+
+ /**
+ * Requests theprogrammed name of the given LCN module.
+ *
+ * @param addr module's address
+ */
+ public void sendModuleNameRequest(LcnAddrMod addr) {
+ queueDirectly(addr, false, PckGenerator.requestModuleName(0));
+ queueDirectly(addr, false, PckGenerator.requestModuleName(1));
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java
new file mode 100644
index 0000000000000..0a2b116250453
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionCallback.java
@@ -0,0 +1,44 @@
+/**
+ * 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.lcn.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Handles events from the connection to the LCN-PCK gateway.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+public interface ConnectionCallback {
+ /**
+ * Invoked when the Connection to the PCK gateway is established and the LCN bus is connected to the PCK gateway.
+ */
+ void onOnline();
+
+ /**
+ * Invoked when the Connection to the PCK gateway has been closed or when the LCN bus is disconnected from the PCK
+ * gateway.
+ *
+ * @param errorMessage the reason
+ */
+ void onOffline(String errorMessage);
+
+ /**
+ * Invoked when a PCK message has been reived from the PCK gateway.
+ *
+ * @param message the received message
+ */
+ void onPckMessageReceived(String message);
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java
new file mode 100644
index 0000000000000..dae04ef589fe6
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionSettings.java
@@ -0,0 +1,158 @@
+/**
+ * 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.lcn.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+
+/**
+ * Settings for a connection to LCN-PCHK.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionSettings {
+
+ /** Unique identifier for this connection. */
+ private final String id;
+
+ /** The user name for authentication. */
+ private final String username;
+
+ /** The password for authentication. */
+ private final String password;
+
+ /** The TCP/IP address or IP of the connection. */
+ private final String address;
+
+ /** The TCP/IP port of the connection. */
+ private final int port;
+
+ /** The dimming mode to use. */
+ private final LcnDefs.OutputPortDimMode dimMode;
+
+ /** The status-messages mode to use. */
+ private final LcnDefs.OutputPortStatusMode statusMode;
+
+ /** Timeout for requests. */
+ private final long timeoutMSec;
+
+ /**
+ * Constructor.
+ *
+ * @param id the connnection's unique identifier
+ * @param address the connection's TCP/IP address or IP
+ * @param port the connection's TCP/IP port
+ * @param username the user name for authentication
+ * @param password the password for authentication
+ * @param dimMode the dimming mode
+ * @param statusMode the status-messages mode
+ * @param timeout the request timeout
+ */
+ public ConnectionSettings(String id, String address, int port, String username, String password,
+ LcnDefs.OutputPortDimMode dimMode, LcnDefs.OutputPortStatusMode statusMode, int timeout) {
+ this.id = id;
+ this.address = address;
+ this.port = port;
+ this.username = username;
+ this.password = password;
+ this.dimMode = dimMode;
+ this.statusMode = statusMode;
+ this.timeoutMSec = timeout;
+ }
+
+ /**
+ * Gets the unique identifier for the connection.
+ *
+ * @return the unique identifier
+ */
+ public String getId() {
+ return this.id;
+ }
+
+ /**
+ * Gets the user name used for authentication.
+ *
+ * @return the user name
+ */
+ public String getUsername() {
+ return this.username;
+ }
+
+ /**
+ * Gets the password used for authentication.
+ *
+ * @return the password
+ */
+ public String getPassword() {
+ return this.password;
+ }
+
+ /**
+ * Gets the TCP/IP address or IP of the connection.
+ *
+ * @return the address or IP
+ */
+ public String getAddress() {
+ return this.address;
+ }
+
+ /**
+ * Gets the TCP/IP port of the connection.
+ *
+ * @return the port
+ */
+ public int getPort() {
+ return this.port;
+ }
+
+ /**
+ * Gets the dimming mode to use for the connection.
+ *
+ * @return the dimming mode
+ */
+ public LcnDefs.OutputPortDimMode getDimMode() {
+ return this.dimMode;
+ }
+
+ /**
+ * Gets the status-messages mode to use for the connection.
+ *
+ * @return the status-messages mode
+ */
+ public LcnDefs.OutputPortStatusMode getStatusMode() {
+ return this.statusMode;
+ }
+
+ /**
+ * Gets the request timeout.
+ *
+ * @return the timeout in milliseconds
+ */
+ public long getTimeout() {
+ return this.timeoutMSec;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (!(o instanceof ConnectionSettings)) {
+ return false;
+ }
+ ConnectionSettings other = (ConnectionSettings) o;
+ return this.id.equals(other.id) && this.address.equals(other.address) && this.port == other.port
+ && this.username.equals(other.username) && this.password.equals(other.password)
+ && this.dimMode == other.dimMode && this.statusMode == other.statusMode
+ && this.timeoutMSec == other.timeoutMSec;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java
new file mode 100644
index 0000000000000..55acca37974b0
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnected.java
@@ -0,0 +1,58 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnAddr;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+
+/**
+ * This state is active when the connection to the LCN bus has been established successfully and data can be sent and
+ * retrieved.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateConnected extends AbstractConnectionState {
+ private static final int PING_INTERVAL_SEC = 60;
+ private int pingCounter;
+
+ public ConnectionStateConnected(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ // send periodic keep-alives to keep the connection open
+ addTimer(getScheduler().scheduleWithFixedDelay(
+ () -> connection.queueDirectlyPlainText(PckGenerator.ping(++pingCounter)), PING_INTERVAL_SEC,
+ PING_INTERVAL_SEC, TimeUnit.SECONDS));
+
+ // run ModInfo.update() for every LCN module
+ addTimer(getScheduler().scheduleWithFixedDelay(connection::updateModInfos, 0, 1, TimeUnit.SECONDS));
+
+ connection.sendOfflineQueue();
+ }
+
+ @Override
+ public void queue(LcnAddr addr, boolean wantsAck, byte[] data) {
+ connection.queueDirectly(addr, wantsAck, data);
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ parseLcnBusDiconnectMessage(data);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java
new file mode 100644
index 0000000000000..35f54d8b22795
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateConnecting.java
@@ -0,0 +1,97 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.StandardSocketOptions;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.CompletionHandler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This state is active during the socket creation, host name resolving and waiting for the TCP connection to become
+ * established.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateConnecting extends AbstractConnectionState {
+ private final Logger logger = LoggerFactory.getLogger(ConnectionStateConnecting.class);
+
+ public ConnectionStateConnecting(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ connection.clearRuntimeData();
+
+ logger.debug("Connecting to {}:{} ...", connection.getSettings().getAddress(),
+ connection.getSettings().getPort());
+
+ try {
+ // Open Channel by using the system-wide default AynchronousChannelGroup.
+ // So, Threads are used or re-used on demand by the JVM.
+ AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
+ // Do not wait until some buffer is filled, send PCK commands immediately
+ channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
+ connection.setSocketChannel(channel);
+
+ InetSocketAddress address = new InetSocketAddress(connection.getSettings().getAddress(),
+ connection.getSettings().getPort());
+
+ if (address.isUnresolved()) {
+ throw new LcnException("Could not resolve hostname");
+ }
+
+ channel.connect(address, null, new CompletionHandler<@Nullable Void, @Nullable Void>() {
+ @Override
+ public void completed(@Nullable Void result, @Nullable Void attachment) {
+ connection.readAndProcess();
+ nextState(ConnectionStateSendUsername::new);
+ }
+
+ @Override
+ public void failed(@Nullable Throwable e, @Nullable Void attachment) {
+ handleConnectionFailure(e);
+ }
+ });
+ } catch (IOException | LcnException e) {
+ handleConnectionFailure(e);
+ }
+ }
+
+ private void handleConnectionFailure(@Nullable Throwable e) {
+ String message;
+ if (e != null) {
+ logger.warn("Could not connect to {}:{}: {}", connection.getSettings().getAddress(),
+ connection.getSettings().getPort(), e.getMessage());
+ message = e.getMessage();
+ } else {
+ message = "";
+ }
+ connection.getCallback().onOffline(message);
+ context.handleConnectionFailed(e);
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ // nothing
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java
new file mode 100644
index 0000000000000..e6d05cba3ff49
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateGracePeriodBeforeReconnect.java
@@ -0,0 +1,45 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This state is active when the connection failed. A grace period is enforced to prevent fast cycling through the
+ * states.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateGracePeriodBeforeReconnect extends AbstractConnectionState {
+ private static final int RECONNECT_GRACE_PERIOD_SEC = 5;
+
+ public ConnectionStateGracePeriodBeforeReconnect(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ closeSocketChannel();
+
+ addTimer(getScheduler().schedule(() -> nextState(ConnectionStateConnecting::new), RECONNECT_GRACE_PERIOD_SEC,
+ TimeUnit.SECONDS));
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ // nothing
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java
new file mode 100644
index 0000000000000..a4c3790ee96a2
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateInit.java
@@ -0,0 +1,37 @@
+/**
+ * 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.lcn.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This is the initial state of the {@link ConnectionStateMachine}.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateInit extends AbstractConnectionState {
+ public ConnectionStateInit(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ nextState(ConnectionStateConnecting::new);
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ // nothing
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java
new file mode 100644
index 0000000000000..9e83f7ffeaa3b
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateMachine.java
@@ -0,0 +1,107 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.lcn.internal.common.LcnAddr;
+
+/**
+ * Implements a state machine for managing the connection to the LCN-PCK gateway. Setting states is thread-safe.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateMachine extends AbstractStateMachine {
+ private final Connection connection;
+ final ScheduledExecutorService scheduler;
+
+ public ConnectionStateMachine(Connection connection, ScheduledExecutorService scheduler) {
+ this.connection = connection;
+ this.scheduler = scheduler;
+
+ setState(ConnectionStateInit::new);
+ }
+
+ /**
+ * Gets the framework's scheduler.
+ *
+ * @return the scheduler
+ */
+ protected ScheduledExecutorService getScheduler() {
+ return scheduler;
+ }
+
+ /**
+ * Gets the PCHK Connection object.
+ *
+ * @return the connection
+ */
+ public Connection getConnection() {
+ return connection;
+ }
+
+ /**
+ * Enqueues a PCK command. Implementation is state dependent.
+ *
+ * @param addr the destination address
+ * @param wantsAck true, if the module shall respond with an Ack
+ * @param data the data
+ */
+ public void queue(LcnAddr addr, boolean wantsAck, byte[] data) {
+ AbstractConnectionState localState = state;
+ if (localState != null) {
+ localState.queue(addr, wantsAck, data);
+ }
+ }
+
+ /**
+ * Invoked by any state, if the connection fails.
+ *
+ * @param e the cause
+ */
+ public void handleConnectionFailed(@Nullable Throwable e) {
+ if (!(state instanceof ConnectionStateShutdown)) {
+ if (e != null) {
+ connection.getCallback().onOffline(e.getMessage());
+ } else {
+ connection.getCallback().onOffline("");
+ }
+ setState(ConnectionStateGracePeriodBeforeReconnect::new);
+ }
+ }
+
+ /**
+ * Processes a received PCK message by passing it to the current State.
+ *
+ * @param data the PCK message
+ */
+ public void onInputReceived(String data) {
+ AbstractConnectionState localState = state;
+ if (localState != null) {
+ localState.onPckMessageReceived(data);
+ }
+ }
+
+ /**
+ * Shuts the StateMachine down finally. A shut-down StateMachine cannot be re-used.
+ */
+ public void shutdownFinally() {
+ AbstractConnectionState localState = state;
+ if (localState != null) {
+ localState.shutdownFinally();
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java
new file mode 100644
index 0000000000000..0942f777d85b9
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSegmentScan.java
@@ -0,0 +1,82 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnAddrGrp;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This state discovers the LCN segment couplers.
+ *
+ * After the authorization against the LCN-PCK gateway was successful, the LCN segment couplers are discovery, to
+ * retrieve the segment ID of the local segment. When no segment couplers were found, a timeout sets the local segment
+ * ID to 0.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateSegmentScan extends AbstractConnectionState {
+ private final Logger logger = LoggerFactory.getLogger(ConnectionStateSegmentScan.class);
+ public static final Pattern PATTERN_SK_RESPONSE = Pattern
+ .compile("=M(?\\d{3})(?\\d{3})\\.SK(?\\d+)");
+ private final RequestStatus statusSegmentScan = new RequestStatus(-1, 3, "Segment Scan");
+
+ public ConnectionStateSegmentScan(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ statusSegmentScan.refresh();
+ addTimer(getScheduler().scheduleWithFixedDelay(this::update, 0, 500, TimeUnit.MILLISECONDS));
+ }
+
+ private void update() {
+ long currTime = System.nanoTime();
+ try {
+ if (statusSegmentScan.shouldSendNextRequest(connection.getSettings().getTimeout(), currTime)) {
+ connection.queueDirectly(new LcnAddrGrp(3, 3), false, PckGenerator.segmentCouplerScan());
+ statusSegmentScan.onRequestSent(currTime);
+ }
+ } catch (LcnException e) {
+ // Give up. Probably no segments available.
+ connection.setLocalSegId(0);
+ logger.debug("No segment couplers detected");
+ nextState(ConnectionStateConnected::new);
+ }
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ Matcher matcher = PATTERN_SK_RESPONSE.matcher(data);
+
+ if (matcher.matches()) {
+ // any segment coupler answered
+ if (Integer.parseInt(matcher.group("segId")) == 0) {
+ // local segment coupler answered
+ connection.setLocalSegId(Integer.parseInt(matcher.group("id")));
+ logger.debug("Local segment ID is {}", connection.getLocalSegId());
+ nextState(ConnectionStateConnected::new);
+ }
+ }
+ parseLcnBusDiconnectMessage(data);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java
new file mode 100644
index 0000000000000..bfcf89f37a766
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendDimMode.java
@@ -0,0 +1,41 @@
+/**
+ * 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.lcn.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+
+/**
+ * Sets the dimming mode range (0-50 or 0-200) in the LCN-PCK for this connection, as configured by the user.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateSendDimMode extends AbstractConnectionState {
+ public ConnectionStateSendDimMode(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ connection.queueDirectlyPlainText(PckGenerator.setOperationMode(connection.getSettings().getDimMode(),
+ connection.getSettings().getStatusMode()));
+
+ nextState(ConnectionStateSegmentScan::new);
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ // nothing
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java
new file mode 100644
index 0000000000000..01ae13bd3fe72
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendPassword.java
@@ -0,0 +1,41 @@
+/**
+ * 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.lcn.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+
+/**
+ * This state sends the password during the authentication with the LCN-PCK gateway.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateSendPassword extends AbstractConnectionStateSendCredentials {
+ public ConnectionStateSendPassword(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ startTimeoutTimer();
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ if (data.equals(LcnDefs.AUTH_PASSWORD)) {
+ connection.queueDirectlyPlainText(connection.getSettings().getPassword());
+ nextState(ConnectionStateWaitForLcnBusConnected::new);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java
new file mode 100644
index 0000000000000..a04f1d341a3c3
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateSendUsername.java
@@ -0,0 +1,41 @@
+/**
+ * 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.lcn.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+
+/**
+ * This state sends the username during the authentication with the LCN-PCK gateway.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateSendUsername extends AbstractConnectionStateSendCredentials {
+ public ConnectionStateSendUsername(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ startTimeoutTimer();
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ if (data.equals(LcnDefs.AUTH_USERNAME)) {
+ connection.queueDirectlyPlainText(connection.getSettings().getUsername());
+ nextState(ConnectionStateSendPassword::new);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java
new file mode 100644
index 0000000000000..67c7ff2100e25
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateShutdown.java
@@ -0,0 +1,45 @@
+/**
+ * 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.lcn.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnAddr;
+
+/**
+ * This state is entered when the connection shall be shut-down finally. This happens when Thing.dispose() is called.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateShutdown extends AbstractConnectionState {
+ public ConnectionStateShutdown(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ closeSocketChannel();
+
+ // end state
+ }
+
+ @Override
+ public void queue(LcnAddr addr, boolean wantsAck, byte[] data) {
+ // nothing
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ // nothing
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java
new file mode 100644
index 0000000000000..8882042cebed4
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnected.java
@@ -0,0 +1,68 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * This state waits for the status answer of the LCN-PCK gateway after connection establishment, rather the LCN bus is
+ * connected.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateWaitForLcnBusConnected extends AbstractConnectionState {
+ private @Nullable ScheduledFuture> legacyTimer;
+
+ public ConnectionStateWaitForLcnBusConnected(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ // Legacy support for LCN-PCHK 2.2 and earlier:
+ // There was no explicit "LCN connected" notification after successful authentication.
+ // Only "LCN disconnected" would be reported immediately. That means "LCN connected" used to be the default.
+ ScheduledFuture> localLegacyTimer = legacyTimer = getScheduler().schedule(() -> {
+ connection.getCallback().onOnline();
+ nextState(ConnectionStateSendDimMode::new);
+ }, connection.getSettings().getTimeout(), TimeUnit.MILLISECONDS);
+ addTimer(localLegacyTimer);
+ }
+
+ @Override
+ public void onPckMessageReceived(String data) {
+ ScheduledFuture> localLegacyTimer = legacyTimer;
+ if (data.equals(LcnDefs.LCNCONNSTATE_DISCONNECTED)) {
+ if (localLegacyTimer != null) {
+ localLegacyTimer.cancel(true);
+ }
+ connection.getCallback().onOffline("LCN bus not connected to LCN-PCHK/PKE");
+ } else if (data.equals(LcnDefs.LCNCONNSTATE_CONNECTED)) {
+ if (localLegacyTimer != null) {
+ localLegacyTimer.cancel(true);
+ }
+ connection.getCallback().onOnline();
+ nextState(ConnectionStateSendDimMode::new);
+ } else if (data.equals(LcnDefs.INSUFFICIENT_LICENSES)) {
+ context.handleConnectionFailed(
+ new LcnException("LCN-PCHK/PKE has not enough licenses to handle this connection"));
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java
new file mode 100644
index 0000000000000..8174ada6eecb7
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ConnectionStateWaitForLcnBusConnectedAfterDisconnected.java
@@ -0,0 +1,33 @@
+/**
+ * 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.lcn.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This state is entered when the LCN-PCK gateway sent a message, that the connection to the LCN bus was lost. This can
+ * happen if the user plugs the USB cable to the PC coupler.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class ConnectionStateWaitForLcnBusConnectedAfterDisconnected extends ConnectionStateWaitForLcnBusConnected {
+ public ConnectionStateWaitForLcnBusConnectedAfterDisconnected(ConnectionStateMachine context) {
+ super(context);
+ }
+
+ @Override
+ public void startWorking() {
+ // nothing, don't start legacy timer
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java
new file mode 100644
index 0000000000000..a7799240c2e3f
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/ModInfo.java
@@ -0,0 +1,500 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.common.LcnAddr;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.common.Variable;
+import org.openhab.binding.lcn.internal.common.VariableValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Holds data of an LCN module.
+ *
+ * Stores the module's firmware version (if requested)
+ * Manages the scheduling of status-requests
+ * Manages the scheduling of acknowledged commands
+ *
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+public class ModInfo {
+ private final Logger logger = LoggerFactory.getLogger(ModInfo.class);
+ /** Total number of request to sent before going into failed-state. */
+ private static final int NUM_TRIES = 3;
+
+ /** Poll interval for status values that automatically send their values on change. */
+ private static final int MAX_STATUS_EVENTBASED_VALUEAGE_MSEC = 600000;
+
+ /** Poll interval for status values that do not send their values on change (always polled). */
+ private static final int MAX_STATUS_POLLED_VALUEAGE_MSEC = 30000;
+
+ /** Status request delay after a command has been send which potentially changed that status. */
+ private static final int STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC = 2000;
+
+ /** The LCN module's address. */
+ private final LcnAddr addr;
+
+ /** Firmware date of the LCN module. -1 means "unknown". */
+ private int firmwareVersion = -1;
+
+ /** Firmware version request status. */
+ private final RequestStatus requestFirmwareVersion = new RequestStatus(-1, NUM_TRIES, "Firmware Version");
+
+ /** Output-port request status (0..3). */
+ private final RequestStatus[] requestStatusOutputs = new RequestStatus[LcnChannelGroup.OUTPUT.getCount()];
+
+ /** Relays request status (all 8). */
+ private final RequestStatus requestStatusRelays = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES,
+ "Relays");
+
+ /** Binary-sensors request status (all 8). */
+ private final RequestStatus requestStatusBinSensors = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC,
+ NUM_TRIES, "Binary Sensors");
+
+ /**
+ * Variables request status.
+ * Lazy initialization: Will be filled once the firmware version is known.
+ */
+ private final Map requestStatusVars = new HashMap<>();
+
+ /**
+ * Caches the values of the variables, needed for changing the values.
+ */
+ private final Map variableValue = new HashMap<>();
+
+ /** LEDs and logic-operations request status (all 12+4). */
+ private final RequestStatus requestStatusLedsAndLogicOps = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC,
+ NUM_TRIES, "LEDs and Logic");
+
+ /** Key lock-states request status (all tables, A-D). */
+ private final RequestStatus requestStatusLockedKeys = new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES,
+ "Key Locks");
+
+ /**
+ * Holds the last LCN variable requested whose response will not contain the variable's type.
+ * {@link Variable#UNKNOWN} means there is currently no such request.
+ */
+ private Variable lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
+
+ /**
+ * List of queued PCK commands to be acknowledged by the LCN module.
+ * Commands are always without address header.
+ * Note that the first one might currently be "in progress".
+ */
+ private final Queue pckCommandsWithAck = new ConcurrentLinkedQueue<>();
+
+ /** Status data for the currently processed {@link PckCommandWithAck}. */
+ private final RequestStatus requestCurrentPckCommandWithAck = new RequestStatus(-1, NUM_TRIES, "Commands with Ack");
+
+ /**
+ * Constructor.
+ *
+ * @param addr the module's address
+ */
+ public ModInfo(LcnAddr addr) {
+ this.addr = addr;
+ for (int i = 0; i < LcnChannelGroup.OUTPUT.getCount(); ++i) {
+ requestStatusOutputs[i] = new RequestStatus(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC, NUM_TRIES,
+ "Output " + (i + 1));
+ }
+
+ for (Variable var : Variable.values()) {
+ if (var != Variable.UNKNOWN) {
+ this.requestStatusVars.put(var, new RequestStatus(MAX_STATUS_POLLED_VALUEAGE_MSEC, NUM_TRIES,
+ var.getType() + " " + (var.getNumber() + 1)));
+ }
+ }
+ }
+
+ /**
+ * Gets the last requested variable whose response will not contain the variables type.
+ *
+ * @return the "typeless" variable
+ */
+ public Variable getLastRequestedVarWithoutTypeInResponse() {
+ return this.lastRequestedVarWithoutTypeInResponse;
+ }
+
+ /**
+ * Sets the last requested variable whose response will not contain the variables type.
+ *
+ * @param var the "typeless" variable
+ */
+ public void setLastRequestedVarWithoutTypeInResponse(Variable var) {
+ this.lastRequestedVarWithoutTypeInResponse = var;
+ }
+
+ /**
+ * Queues a PCK command to be sent.
+ * It will request an acknowledge from the LCN module on receipt.
+ * If there is no response within the request timeout, the command is retried.
+ *
+ * @param data the PCK command to send (without address header)
+ * @param timeoutMSec the time to wait for a response before retrying a request
+ * @param currTime the current time stamp
+ */
+ public void queuePckCommandWithAck(byte[] data, Connection conn, long timeoutMSec, long currTime) {
+ this.pckCommandsWithAck.add(data);
+ // Try to process the new acknowledged command. Will do nothing if another one is still in progress.
+ this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
+ }
+
+ /**
+ * Called whenever an acknowledge is received from the LCN module.
+ *
+ * @param code the LCN internal code. -1 means "positive" acknowledge
+ * @param timeoutMSec the time to wait for a response before retrying a request
+ * @param currTime the current time stamp
+ */
+ public void onAck(int code, Connection conn, long timeoutMSec, long currTime) {
+ if (this.requestCurrentPckCommandWithAck.isActive()) { // Check if we wait for an ack.
+ this.pckCommandsWithAck.poll();
+ this.requestCurrentPckCommandWithAck.reset();
+ // Try to process next acknowledged command
+ this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
+ }
+ }
+
+ /**
+ * Sends the next acknowledged command from the queue.
+ *
+ * @param conn the {@link Connection} belonging to this {@link ModInfo}
+ * @param timeoutMSec the time to wait for a response before retrying a request
+ * @param currTime the current time stamp
+ * @return true if a new command was sent
+ * @throws LcnException when a command response timed out
+ */
+ private boolean tryProcessNextCommandWithAck(Connection conn, long timeoutMSec, long currTime) {
+ // Use the chance to remove a failed command first
+ if (this.requestCurrentPckCommandWithAck.isFailed(timeoutMSec, currTime)) {
+ byte[] failedCommand = this.pckCommandsWithAck.poll();
+ this.requestCurrentPckCommandWithAck.reset();
+
+ if (failedCommand != null) {
+ logger.warn("{}: Module did not respond to command: {}", addr,
+ new String(failedCommand, LcnDefs.LCN_ENCODING));
+ }
+ }
+ // Peek new command
+ if (!this.pckCommandsWithAck.isEmpty() && !this.requestCurrentPckCommandWithAck.isActive()) {
+ this.requestCurrentPckCommandWithAck.nextRequestIn(0, currTime);
+ }
+ byte[] command = this.pckCommandsWithAck.peek();
+ if (command == null) {
+ return false;
+ }
+ try {
+ if (requestCurrentPckCommandWithAck.shouldSendNextRequest(timeoutMSec, currTime)) {
+ conn.queueAndSend(new SendDataPck(addr, true, command));
+ this.requestCurrentPckCommandWithAck.onRequestSent(currTime);
+ }
+ } catch (LcnException e) {
+ logger.warn("{}: Could not send command: {}: {}", addr, new String(command, LcnDefs.LCN_ENCODING),
+ e.getMessage());
+ }
+ return true;
+ }
+
+ /**
+ * Triggers a request to retrieve the firmware version of the LCN module, if it is not known, yet.
+ */
+ public void requestFirmwareVersion() {
+ if (firmwareVersion == -1) {
+ requestFirmwareVersion.refresh();
+ }
+ }
+
+ /**
+ * Used to check if the module has the measurement processing firmware (since Feb. 2013).
+ *
+ * @return if the module has at least 4 threshold registers and 12 variables
+ */
+ public boolean hasExtendedMeasurementProcessing() {
+ if (firmwareVersion == -1) {
+ logger.warn("LCN module firmware version unknown");
+ return false;
+ }
+ return firmwareVersion >= LcnBindingConstants.FIRMWARE_2013;
+ }
+
+ private boolean update(Connection conn, long timeoutMSec, long currTime, RequestStatus requestStatus, String pck)
+ throws LcnException {
+ if (requestStatus.shouldSendNextRequest(timeoutMSec, currTime)) {
+ conn.queue(this.addr, false, pck);
+ requestStatus.onRequestSent(currTime);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Keeps the request logic active.
+ * Must be called periodically.
+ *
+ * @param conn the {@link Connection} belonging to this {@link ModInfo}
+ * @param timeoutMSec the time to wait for a response before retrying a request
+ * @param currTime the current time stamp
+ */
+ void update(Connection conn, long timeoutMSec, long currTime) {
+ try {
+ if (update(conn, timeoutMSec, currTime, requestFirmwareVersion, PckGenerator.requestSn())) {
+ return;
+ }
+
+ for (int i = 0; i < LcnChannelGroup.OUTPUT.getCount(); ++i) {
+ if (update(conn, timeoutMSec, currTime, requestStatusOutputs[i], PckGenerator.requestOutputStatus(i))) {
+ return;
+ }
+ }
+
+ if (update(conn, timeoutMSec, currTime, requestStatusRelays, PckGenerator.requestRelaysStatus())) {
+ return;
+ }
+ if (update(conn, timeoutMSec, currTime, requestStatusBinSensors, PckGenerator.requestBinSensorsStatus())) {
+ return;
+ }
+
+ // Variable requests
+ if (this.firmwareVersion != -1) { // Firmware version is required
+ // Use the chance to remove a failed "typeless variable" request
+ if (lastRequestedVarWithoutTypeInResponse != Variable.UNKNOWN) {
+ RequestStatus requestStatus = requestStatusVars.get(lastRequestedVarWithoutTypeInResponse);
+ if (requestStatus != null && requestStatus.isTimeout(timeoutMSec, currTime)) {
+ lastRequestedVarWithoutTypeInResponse = Variable.UNKNOWN;
+ }
+ }
+ // Variables
+ for (Map.Entry kv : this.requestStatusVars.entrySet()) {
+ RequestStatus requestStatus = kv.getValue();
+ if (requestStatus != null && requestStatus.shouldSendNextRequest(timeoutMSec, currTime)) {
+ // Detect if we can send immediately or if we have to wait for a "typeless" request first
+ boolean hasTypeInResponse = kv.getKey().hasTypeInResponse(this.firmwareVersion);
+ if (hasTypeInResponse || this.lastRequestedVarWithoutTypeInResponse == Variable.UNKNOWN) {
+ try {
+ conn.queue(this.addr, false,
+ PckGenerator.requestVarStatus(kv.getKey(), this.firmwareVersion));
+ requestStatus.onRequestSent(currTime);
+ if (!hasTypeInResponse) {
+ this.lastRequestedVarWithoutTypeInResponse = kv.getKey();
+ }
+ return;
+ } catch (LcnException ex) {
+ requestStatus.reset();
+ }
+ }
+ }
+ }
+ }
+
+ if (update(conn, timeoutMSec, currTime, requestStatusLedsAndLogicOps,
+ PckGenerator.requestLedsAndLogicOpsStatus())) {
+ return;
+ }
+
+ if (update(conn, timeoutMSec, currTime, requestStatusLockedKeys, PckGenerator.requestKeyLocksStatus())) {
+ return;
+ }
+
+ // Try to send next acknowledged command. Will also detect failed ones.
+ this.tryProcessNextCommandWithAck(conn, timeoutMSec, currTime);
+ } catch (LcnException e) {
+ logger.warn("{}: Failed to receive status message: {}", addr, e.getMessage());
+ }
+ }
+
+ /**
+ * Gets the LCN module's firmware date.
+ *
+ * @return the date
+ */
+ public int getFirmwareVersion() {
+ return this.firmwareVersion;
+ }
+
+ /**
+ * Sets the LCN module's firmware date.
+ *
+ * @param firmwareVersion the date
+ */
+ public void setFirmwareVersion(int firmwareVersion) {
+ this.firmwareVersion = firmwareVersion;
+
+ requestFirmwareVersion.onResponseReceived();
+
+ // increase poll interval, if the LCN module sends status updates of a variable event-based
+ requestStatusVars.entrySet().stream().filter(e -> e.getKey().isEventBased(firmwareVersion)).forEach(e -> {
+ RequestStatus value = e.getValue();
+ if (value != null) {
+ value.setMaxAgeMSec(MAX_STATUS_EVENTBASED_VALUEAGE_MSEC);
+ }
+ });
+ }
+
+ /**
+ * Updates the variable value cache.
+ *
+ * @param variable the variable to update
+ * @param value the new value
+ */
+ public void updateVariableValue(Variable variable, VariableValue value) {
+ variableValue.put(variable, value);
+ }
+
+ /**
+ * Gets the current value of a variable from the cache.
+ *
+ * @param variable the variable to retrieve the value for
+ * @return the value of the variable
+ * @throws LcnException when the variable is not in the cache
+ */
+ public long getVariableValue(Variable variable) throws LcnException {
+ return Optional.ofNullable(variableValue.get(variable)).map(v -> v.toNative(variable.useLcnSpecialValues()))
+ .orElseThrow(() -> new LcnException("Current variable value unknown"));
+ }
+
+ /**
+ * Requests the current value of all dimmer outputs.
+ */
+ public void refreshAllOutputs() {
+ Arrays.stream(requestStatusOutputs).forEach(RequestStatus::refresh);
+ }
+
+ /**
+ * Requests the current value of the given dimmer output.
+ *
+ * @param number 0..3
+ */
+ public void refreshOutput(int number) {
+ requestStatusOutputs[number].refresh();
+ }
+
+ /**
+ * Requests the current value of all relays.
+ */
+ public void refreshRelays() {
+ requestStatusRelays.refresh();
+ }
+
+ /**
+ * Requests the current value of all binary sensor.
+ */
+ public void refreshBinarySensors() {
+ requestStatusBinSensors.refresh();
+ }
+
+ /**
+ * Requests the current value of the given variable.
+ *
+ * @param variable the variable to request
+ */
+ public void refreshVariable(Variable variable) {
+ RequestStatus requestStatus = requestStatusVars.get(variable);
+ if (requestStatus != null) {
+ requestStatus.refresh();
+ }
+ }
+
+ /**
+ * Requests the current value of all LEDs and logic operations.
+ */
+ public void refreshLedsAndLogic() {
+ requestStatusLedsAndLogicOps.refresh();
+ }
+
+ /**
+ * Requests the current value of all LEDs and logic operations, after a LED has been changed by openHAB.
+ */
+ public void refreshStatusLedsAnLogicAfterChange() {
+ requestStatusLedsAndLogicOps.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime());
+ }
+
+ /**
+ * Requests the current locking states of all keys.
+ */
+ public void refreshStatusLockedKeys() {
+ requestStatusLockedKeys.refresh();
+ }
+
+ /**
+ * Requests the current locking states of all keys, after a lock state has been changed by openHAB.
+ */
+ public void refreshStatusStatusLockedKeysAfterChange() {
+ requestStatusLockedKeys.nextRequestIn(STATUS_REQUEST_DELAY_AFTER_COMMAND_MSEC, System.nanoTime());
+ }
+
+ /**
+ * Resets the value request logic, when a requested value has been received from the LCN module: Dimmer Output
+ *
+ * @param outputId 0..3
+ */
+ public void onOutputResponseReceived(int outputId) {
+ requestStatusOutputs[outputId].onResponseReceived();
+ }
+
+ /**
+ * Resets the value request logic, when a requested value has been received from the LCN module: Relay
+ */
+ public void onRelayResponseReceived() {
+ requestStatusRelays.onResponseReceived();
+ }
+
+ /**
+ * Resets the value request logic, when a requested value has been received from the LCN module: Binary Sensor
+ */
+ public void onBinarySensorsResponseReceived() {
+ requestStatusBinSensors.onResponseReceived();
+ }
+
+ /**
+ * Resets the value request logic, when a requested value has been received from the LCN module: Variable
+ *
+ * @param variable the received variable type
+ */
+ public void onVariableResponseReceived(Variable variable) {
+ RequestStatus requestStatus = requestStatusVars.get(variable);
+ if (requestStatus != null) {
+ requestStatus.onResponseReceived();
+ }
+ }
+
+ /**
+ * Resets the value request logic, when a requested value has been received from the LCN module: LEDs and logic
+ */
+ public void onLedsAndLogicResponseReceived() {
+ requestStatusLedsAndLogicOps.onResponseReceived();
+ }
+
+ /**
+ * Resets the value request logic, when a requested value has been received from the LCN module: Keys lock state
+ */
+ public void onLockedKeysResponseReceived() {
+ requestStatusLockedKeys.onResponseReceived();
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java
new file mode 100644
index 0000000000000..f65ac8fa9ce9e
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/PckQueueItem.java
@@ -0,0 +1,74 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.time.Instant;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnAddr;
+
+/**
+ * Holds data of one PCK command with the target address and the date when the item has been enqueued.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class PckQueueItem {
+ private final Instant enqueued;
+ private final LcnAddr addr;
+ private final boolean wantsAck;
+ private final byte[] data;
+
+ public PckQueueItem(LcnAddr addr, boolean wantsAck, byte[] data) {
+ this.enqueued = Instant.now();
+ this.addr = addr;
+ this.wantsAck = wantsAck;
+ this.data = data;
+ }
+
+ /**
+ * Gets the time when this message has been enqueued.
+ *
+ * @return the Instant
+ */
+ public Instant getEnqueued() {
+ return enqueued;
+ }
+
+ /**
+ * Gets the address of the destination LCN module.
+ *
+ * @return the address
+ */
+ public LcnAddr getAddr() {
+ return addr;
+ }
+
+ /**
+ * Checks whether an Ack is requested.
+ *
+ * @return true, if an Ack is requested
+ */
+ public boolean isWantsAck() {
+ return wantsAck;
+ }
+
+ /**
+ * Gets the raw PCK message to be sent.
+ *
+ * @return message as ByteBuffer
+ */
+ public byte[] getData() {
+ return data;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java
new file mode 100644
index 0000000000000..e5b95e876bda8
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/RequestStatus.java
@@ -0,0 +1,195 @@
+/**
+ * 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.lcn.internal.connection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages timeout and retry logic for an LCN request.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+public class RequestStatus {
+ private final Logger logger = LoggerFactory.getLogger(RequestStatus.class);
+ /** Interval for forced updates. -1 if not used. */
+ private volatile long maxAgeMSec;
+
+ /** Tells how often a request will be sent if no response was received. */
+ private final int numTries;
+
+ /** true if request logic is activated. */
+ private volatile boolean isActive;
+
+ /** The time the current request was sent out or 0. */
+ private volatile long currRequestTimeStamp;
+
+ /** The time stamp of the next scheduled request or 0. */
+ private volatile long nextRequestTimeStamp;
+
+ /** Number of retries left until the request is marked as failed. */
+ private volatile int numRetriesLeft;
+ private final String label;
+
+ /**
+ * Constructor.
+ *
+ * @param maxAgeMSec the forced-updates interval (-1 if not used)
+ * @param numTries the maximum number of tries until the request is marked as failed
+ */
+ RequestStatus(long maxAgeMSec, int numTries, String label) {
+ this.maxAgeMSec = maxAgeMSec;
+ this.numTries = numTries;
+ this.label = label;
+ this.reset();
+ }
+
+ /** Resets the runtime data to the initial states. */
+ public synchronized void reset() {
+ this.isActive = false;
+ this.currRequestTimeStamp = 0;
+ this.nextRequestTimeStamp = 0;
+ this.numRetriesLeft = 0;
+ }
+
+ /**
+ * Checks whether the request logic is active.
+ *
+ * @return true if active
+ */
+ public boolean isActive() {
+ return this.isActive;
+ }
+
+ /**
+ * Checks whether a request is waiting for a response.
+ *
+ * @return true if waiting for a response
+ */
+ boolean isPending() {
+ return this.currRequestTimeStamp != 0;
+ }
+
+ /**
+ * Checks whether the request is active and ran into timeout while waiting for a response.
+ *
+ * @param timeoutMSec the timeout in milliseconds
+ * @param currTime the current time stamp
+ * @return true if request timed out
+ */
+ synchronized boolean isTimeout(long timeoutMSec, long currTime) {
+ return this.isPending() && currTime - this.currRequestTimeStamp >= timeoutMSec * 1000000L;
+ }
+
+ /**
+ * Checks for failed requests (active and out of retries).
+ *
+ * @param timeoutMSec the timeout in milliseconds
+ * @param currTime the current time stamp
+ * @return true if no response was received and no retries are left
+ */
+ synchronized boolean isFailed(long timeoutMSec, long currTime) {
+ return this.isTimeout(timeoutMSec, currTime) && this.numRetriesLeft == 0;
+ }
+
+ /**
+ * Schedules the next request.
+ *
+ * @param delayMSec the delay in milliseconds
+ * @param currTime the current time stamp
+ */
+ public synchronized void nextRequestIn(long delayMSec, long currTime) {
+ this.isActive = true;
+ this.nextRequestTimeStamp = currTime + delayMSec * 1000000L;
+ }
+
+ /**
+ * Schedules a request to retrieve the current value.
+ */
+ public synchronized void refresh() {
+ nextRequestIn(0, System.nanoTime());
+ this.numRetriesLeft = this.numTries;
+ }
+
+ /**
+ * Checks whether sending a new request is required (should be called periodically).
+ *
+ * @param timeoutMSec the time to wait for a response before retrying the request
+ * @param currTime the current time stamp
+ * @return true to indicate a new request should be sent
+ * @throws LcnException when a status request timed out
+ */
+ synchronized boolean shouldSendNextRequest(long timeoutMSec, long currTime) throws LcnException {
+ if (this.isActive) {
+ if (this.nextRequestTimeStamp != 0 && currTime >= this.nextRequestTimeStamp) {
+ return true;
+ }
+ // Retry of current request (after no response was received)
+ if (this.isTimeout(timeoutMSec, currTime)) {
+ if (this.numRetriesLeft > 0) {
+ return true;
+ } else if (isPending()) {
+ currRequestTimeStamp = 0;
+ throw new LcnException(label + ": Failed finally after " + numTries + " tries");
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Must be called right after a new request has been sent.
+ * Must be activated first.
+ *
+ * @param currTime the current time stamp
+ */
+ public synchronized void onRequestSent(long currTime) {
+ if (!this.isActive) {
+ logger.warn("Tried to send a request which is not active");
+ }
+ // Updates retry counter
+ if (this.currRequestTimeStamp == 0) {
+ this.numRetriesLeft = this.numTries - 1;
+ } else if (this.numRetriesLeft > 0) { // Should not happen if used correctly
+ --this.numRetriesLeft;
+ }
+ // Mark request as pending
+ this.currRequestTimeStamp = currTime;
+ // Schedule next request
+ if (this.maxAgeMSec != -1) {
+ this.nextRequestIn(this.maxAgeMSec, currTime);
+ } else {
+ this.nextRequestTimeStamp = 0;
+ }
+ }
+
+ /** Must be called when a response (requested or not) has been received. */
+ public synchronized void onResponseReceived() {
+ if (this.isActive) {
+ this.currRequestTimeStamp = 0; // Mark request (if any) as successful
+ }
+ }
+
+ /**
+ * Sets the timeout of this RequestStatus.
+ *
+ * @param maxAgeMSec the timeout in ms
+ */
+ public void setMaxAgeMSec(long maxAgeMSec) {
+ this.maxAgeMSec = maxAgeMSec;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java
new file mode 100644
index 0000000000000..a271f6a7902c8
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendData.java
@@ -0,0 +1,38 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Base class for a packet to be send to LCN-PCHK.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+public abstract class SendData {
+ /**
+ * Writes the packet's data into the given buffer.
+ * Called right before the packet is actually sent to LCN-PCHK.
+ *
+ * @param buffer the target buffer
+ * @param localSegId the local segment id
+ * @return true if everything was set-up correctly and data was written
+ * @throws IOException if an I/O error occurs
+ */
+ abstract boolean write(OutputStream buffer, int localSegId) throws IOException;
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java
new file mode 100644
index 0000000000000..f04b9688d8489
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPck.java
@@ -0,0 +1,77 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.BufferOverflowException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnAddr;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+
+/**
+ * A PCK command to be send to LCN-PCHK.
+ * It is already encoded as bytes to allow different text-encodings (ANSI, UTF-8).
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+class SendDataPck extends SendData {
+ /** The target LCN address. */
+ private final LcnAddr addr;
+
+ /** true to acknowledge the command on receipt. */
+ private final boolean wantsAck;
+
+ /** PCK command (without address header) encoded as bytes. */
+ private final byte[] data;
+
+ /**
+ * Constructor.
+ *
+ * @param addr the target LCN address
+ * @param wantsAck true to claim receipt
+ * @param data the PCK command encoded as bytes
+ */
+ SendDataPck(LcnAddr addr, boolean wantsAck, byte[] data) {
+ this.addr = addr;
+ this.wantsAck = wantsAck;
+ this.data = data;
+ }
+
+ /**
+ * Gets the PCK command.
+ *
+ * @return the PCK command encoded as bytes
+ */
+ byte[] getData() {
+ return this.data;
+ }
+
+ @Override
+ boolean write(OutputStream buffer, int localSegId) throws BufferOverflowException, IOException {
+ buffer.write(PckGenerator.generateAddressHeader(this.addr, localSegId == -1 ? 0 : localSegId, this.wantsAck)
+ .getBytes(LcnDefs.LCN_ENCODING));
+ buffer.write(this.data);
+ buffer.write(PckGenerator.TERMINATION.getBytes(LcnDefs.LCN_ENCODING));
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "Addr: " + addr + ": " + new String(data, 0, data.length, LcnDefs.LCN_ENCODING);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java
new file mode 100644
index 0000000000000..65e31e8f2d903
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/connection/SendDataPlainText.java
@@ -0,0 +1,61 @@
+/**
+ * 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.lcn.internal.connection;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+
+/**
+ * A plain text to be send to LCN-PCHK.
+ *
+ * @author Tobias Jüttner - Initial Contribution
+ * @author Fabian Wolter - Migration to OH2
+ */
+@NonNullByDefault
+class SendDataPlainText extends SendData {
+ /** The text. */
+ private final String text;
+
+ /**
+ * Constructor.
+ *
+ * @param text the text
+ */
+ SendDataPlainText(String text) {
+ this.text = text;
+ }
+
+ /**
+ * Gets the text.
+ *
+ * @return the text
+ */
+ String getText() {
+ return this.text;
+ }
+
+ @Override
+ boolean write(OutputStream buffer, int localSegId) throws IOException {
+ buffer.write((this.text + PckGenerator.TERMINATION).getBytes(LcnDefs.LCN_ENCODING));
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return text;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java
new file mode 100644
index 0000000000000..456bbe847da48
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converter.java
@@ -0,0 +1,118 @@
+/**
+ * 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.lcn.internal.converter;
+
+import java.util.function.Function;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.QuantityType;
+import org.eclipse.smarthome.core.types.State;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for all LCN variable value converters.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class Converter {
+ private final Logger logger = LoggerFactory.getLogger(Converter.class);
+ private @Nullable final Unit> unit;
+ private final Function toHuman;
+ private final Function toNative;
+
+ public Converter(@Nullable Unit> unit, Function toHuman, Function toNative) {
+ this.unit = unit;
+ this.toHuman = toHuman;
+ this.toNative = toNative;
+ }
+
+ /**
+ * Converts the given human readable value into the native LCN value.
+ *
+ * @param humanReadableValue the value to convert
+ * @return the native value
+ */
+ protected long toNative(double humanReadableValue) {
+ return toNative.apply(humanReadableValue);
+ }
+
+ /**
+ * Converts the given native LCN value into a human readable value.
+ *
+ * @param nativeValue the value to convert
+ * @return the human readable value
+ */
+ protected double toHumanReadable(long nativeValue) {
+ return toHuman.apply(nativeValue);
+ }
+
+ /**
+ * Converts a human readable value into LCN native value.
+ *
+ * @param humanReadable value to convert
+ * @return the native LCN value
+ */
+ public DecimalType onCommandFromItem(double humanReadable) {
+ return new DecimalType(toNative(humanReadable));
+ }
+
+ /**
+ * Converts a human readable value into LCN native value.
+ *
+ * @param humanReadable value to convert
+ * @return the native LCN value
+ * @throws LcnException when the value could not be converted to the base unit
+ */
+ public DecimalType onCommandFromItem(QuantityType> quantityType) throws LcnException {
+ Unit> localUnit = unit;
+ if (localUnit == null) {
+ return onCommandFromItem(quantityType.doubleValue());
+ }
+
+ QuantityType> quantityInBaseUnit = quantityType.toUnit(localUnit);
+
+ if (quantityInBaseUnit != null) {
+ return onCommandFromItem(quantityInBaseUnit.doubleValue());
+ } else {
+ throw new LcnException(quantityType + ": Incompatible Channel unit configured: " + localUnit);
+ }
+ }
+
+ /**
+ * Converts a state update from the Thing into a human readable unit.
+ *
+ * @param state from the Thing
+ * @return human readable State
+ */
+ public State onStateUpdateFromHandler(State state) {
+ State result = state;
+
+ if (state instanceof DecimalType) {
+ Unit> localUnit = unit;
+ if (localUnit != null) {
+ result = QuantityType.valueOf(toHumanReadable(((DecimalType) state).longValue()), localUnit);
+ }
+ } else {
+ logger.warn("Unexpected state type: {}", state.getClass().getSimpleName());
+ }
+
+ return result;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java
new file mode 100644
index 0000000000000..3a8c5fff7aa04
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/Converters.java
@@ -0,0 +1,62 @@
+/**
+ * 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.lcn.internal.converter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.unit.SIUnits;
+import org.eclipse.smarthome.core.library.unit.SmartHomeUnits;
+
+/**
+ * Holds all Converter objects.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class Converters {
+ public static final Converter TEMPERATURE;
+ public static final Converter LIGHT;
+ public static final Converter CO2;
+ public static final Converter CURRENT;
+ public static final Converter VOLTAGE;
+ public static final Converter ANGLE;
+ public static final Converter WINDSPEED;
+ public static final Converter IDENTITY;
+
+ static {
+ TEMPERATURE = new Converter(SIUnits.CELSIUS, n -> (n - 1000) / 10d, h -> Math.round(h * 10) + 1000);
+ LIGHT = new Converter(SmartHomeUnits.LUX, Converters::lightToHumanReadable, Converters::lightToNative);
+ CO2 = new Converter(SmartHomeUnits.PARTS_PER_MILLION, n -> (double) n, Math::round);
+ CURRENT = new Converter(SmartHomeUnits.AMPERE, n -> n / 100d, h -> Math.round(h * 100));
+ VOLTAGE = new Converter(SmartHomeUnits.VOLT, n -> n / 400d, h -> Math.round(h * 400));
+ ANGLE = new Converter(SmartHomeUnits.DEGREE_ANGLE, n -> (n - 1000) / 10d, Converters::angleToNative);
+ WINDSPEED = new Converter(SmartHomeUnits.METRE_PER_SECOND, n -> n / 10d, h -> Math.round(h * 10));
+ IDENTITY = new Converter(null, n -> (double) n, Math::round);
+ }
+
+ private static long lightToNative(double value) {
+ return Math.round(Math.log(value) * 100);
+ }
+
+ private static double lightToHumanReadable(long value) {
+ // Max. value hardware can deliver is 100klx. Apply hard limit, because higher native values lead to very big
+ // lux values.
+ if (value > lightToNative(100e3)) {
+ return Double.NaN;
+ }
+ return Math.exp(value / 100d);
+ }
+
+ private static long angleToNative(double h) {
+ return (Math.round(h * 10) + 1000);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java
new file mode 100644
index 0000000000000..b2b4989a668c8
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/converter/S0Converter.java
@@ -0,0 +1,55 @@
+/**
+ * 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.lcn.internal.converter;
+
+import java.math.BigDecimal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.library.unit.SmartHomeUnits;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for S0 counter value converters.
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+public class S0Converter extends Converter {
+ private final Logger logger = LoggerFactory.getLogger(S0Converter.class);
+ protected double pulsesPerKwh;
+
+ public S0Converter(@Nullable Object parameter) {
+ super(SmartHomeUnits.WATT, n -> 0d, h -> 0L);
+
+ if (parameter == null) {
+ pulsesPerKwh = 1000;
+ logger.debug("Pulses per kWh not set. Assuming 1000 imp./kWh.");
+ } else if (parameter instanceof BigDecimal) {
+ pulsesPerKwh = ((BigDecimal) parameter).doubleValue();
+ } else {
+ logger.warn("Could not parse 'pulses', unexpected type, should be float or integer: {}", parameter);
+ }
+ }
+
+ @Override
+ public long toNative(double value) {
+ return Math.round(value * pulsesPerKwh / 1000);
+ }
+
+ @Override
+ public double toHumanReadable(long value) {
+ return value / pulsesPerKwh * 1000;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java
new file mode 100644
index 0000000000000..1cf6f92bd24f0
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtService.java
@@ -0,0 +1,39 @@
+/**
+ * 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.lcn.internal.pchkdiscovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+import com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter;
+
+/**
+ * Used for deserializing the XML response of the LCN-PCHK discovery protocol.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+@XStreamConverter(value = ToAttributedValueConverter.class, strings = { "content" })
+public class ExtService {
+ private final int localPort;
+ @SuppressWarnings("unused")
+ private final String content = "";
+
+ public ExtService(int localPort) {
+ this.localPort = localPort;
+ }
+
+ public int getLocalPort() {
+ return localPort;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java
new file mode 100644
index 0000000000000..da2ec561faa8c
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ExtServices.java
@@ -0,0 +1,33 @@
+/**
+ * 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.lcn.internal.pchkdiscovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Used for deserializing the XML response of the LCN-PCHK discovery protocol.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class ExtServices {
+ private final ExtService ExtService;
+
+ public ExtServices(ExtService extService) {
+ ExtService = extService;
+ }
+
+ public ExtService getExtService() {
+ return ExtService;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java
new file mode 100644
index 0000000000000..be8bb3fbc0925
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryService.java
@@ -0,0 +1,161 @@
+/**
+ * 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.lcn.internal.pchkdiscovery;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.net.MulticastSocket;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
+import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
+import org.eclipse.smarthome.config.discovery.DiscoveryService;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.io.xml.StaxDriver;
+
+/**
+ * Discovers LCN-PCK gateways, such as LCN-PCHK.
+ *
+ * Scan approach:
+ * 1. Determines all local network interfaces
+ * 2. Send a multicast message on each interface to the PCHK multicast address 234.5.6.7 (not configurable by user).
+ * 3. Evaluate multicast responses of PCK gateways in the network
+ *
+ * @author Fabian Wolter - Initial Contribution
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.lcn")
+public class LcnPchkDiscoveryService extends AbstractDiscoveryService {
+ private final Logger logger = LoggerFactory.getLogger(LcnPchkDiscoveryService.class);
+ private static final String HOSTNAME = "hostname";
+ private static final String PORT = "port";
+ private static final String MAC_ADDRESS = "macAddress";
+ private static final String PCHK_DISCOVERY_MULTICAST_ADDRESS = "234.5.6.7";
+ private static final int PCHK_DISCOVERY_PORT = 4220;
+ private static final int INTERFACE_TIMEOUT_SEC = 2;
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Collections
+ .unmodifiableSet(Stream.of(LcnBindingConstants.THING_TYPE_PCK_GATEWAY).collect(Collectors.toSet()));
+ private static final String DISCOVER_REQUEST = "openHAB ";
+
+ public LcnPchkDiscoveryService() throws IllegalArgumentException {
+ super(SUPPORTED_THING_TYPES_UIDS, 0, false);
+ }
+
+ private List getLocalAddresses() {
+ List result = new LinkedList<>();
+ try {
+ for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
+ try {
+ if (networkInterface.isUp() && !networkInterface.isLoopback()
+ && !networkInterface.isPointToPoint()) {
+ result.addAll(Collections.list(networkInterface.getInetAddresses()));
+ }
+ } catch (SocketException exception) {
+ // ignore
+ }
+ }
+ } catch (SocketException exception) {
+ return Collections.emptyList();
+ }
+ return result;
+ }
+
+ @Override
+ protected void startScan() {
+ try {
+ InetAddress multicastAddress = InetAddress.getByName(PCHK_DISCOVERY_MULTICAST_ADDRESS);
+
+ getLocalAddresses().forEach(localInterfaceAddress -> {
+ logger.debug("Searching on {} ...", localInterfaceAddress.getHostAddress());
+ try (MulticastSocket socket = new MulticastSocket(PCHK_DISCOVERY_PORT)) {
+ socket.setInterface(localInterfaceAddress);
+ socket.setReuseAddress(true);
+ socket.setSoTimeout(INTERFACE_TIMEOUT_SEC * 1000);
+ socket.joinGroup(multicastAddress);
+
+ byte[] requestData = DISCOVER_REQUEST.getBytes(LcnDefs.LCN_ENCODING);
+ DatagramPacket request = new DatagramPacket(requestData, requestData.length, multicastAddress,
+ PCHK_DISCOVERY_PORT);
+ socket.send(request);
+
+ do {
+ byte[] rxbuf = new byte[8192];
+ DatagramPacket packet = new DatagramPacket(rxbuf, rxbuf.length);
+ socket.receive(packet);
+
+ InetAddress addr = packet.getAddress();
+ String response = new String(packet.getData(), LcnDefs.LCN_ENCODING);
+
+ if (response.contains("ServicesRequest")) {
+ continue;
+ }
+
+ ServicesResponse deserialized = xmlToServiceResponse(response);
+
+ String macAddress = deserialized.getServer().getMachineId().replace(":", "");
+ ThingUID thingUid = new ThingUID(LcnBindingConstants.THING_TYPE_PCK_GATEWAY, macAddress);
+
+ Map properties = new HashMap<>(3);
+ properties.put(HOSTNAME, addr.getHostAddress());
+ properties.put(PORT, deserialized.getExtServices().getExtService().getLocalPort());
+ properties.put(MAC_ADDRESS, macAddress);
+
+ DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUid)
+ .withProperties(properties).withRepresentationProperty(MAC_ADDRESS)
+ .withLabel(deserialized.getServer().getContent() + " ("
+ + deserialized.getServer().getMachineName() + ")");
+
+ thingDiscovered(discoveryResult.build());
+ } while (true); // left by SocketTimeoutException
+ } catch (IOException e) {
+ logger.debug("Discovery failed for {}: {}", localInterfaceAddress, e.getMessage());
+ }
+ });
+ } catch (UnknownHostException e) {
+ logger.warn("Discovery failed: {}", e.getMessage());
+ }
+ }
+
+ ServicesResponse xmlToServiceResponse(String response) {
+ XStream xstream = new XStream(new StaxDriver());
+ xstream.setClassLoader(getClass().getClassLoader());
+ xstream.autodetectAnnotations(true);
+ xstream.alias("ServicesResponse", ServicesResponse.class);
+ xstream.alias("Server", Server.class);
+ xstream.alias("Version", Server.class);
+ xstream.alias("ExtServices", ExtServices.class);
+ xstream.alias("ExtService", ExtService.class);
+
+ return (ServicesResponse) xstream.fromXML(response);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java
new file mode 100644
index 0000000000000..ee39fa6ab7254
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Server.java
@@ -0,0 +1,73 @@
+/**
+ * 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.lcn.internal.pchkdiscovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+import com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter;
+
+/**
+ * Used for deserializing the XML response of the LCN-PCHK discovery protocol.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+@XStreamConverter(value = ToAttributedValueConverter.class, strings = { "content" })
+public class Server {
+ @XStreamAsAttribute
+ private final int requestId;
+ @XStreamAsAttribute
+ private final String machineId;
+ @XStreamAsAttribute
+ private final String machineName;
+ @XStreamAsAttribute
+ private final String osShort;
+ @XStreamAsAttribute
+ private final String osLong;
+ private final String content;
+
+ public Server(int requestId, String machineId, String machineName, String osShort, String osLong, String content) {
+ this.requestId = requestId;
+ this.machineId = machineId;
+ this.machineName = machineName;
+ this.osShort = osShort;
+ this.osLong = osLong;
+ this.content = content;
+ }
+
+ public int getRequestId() {
+ return requestId;
+ }
+
+ public String getMachineId() {
+ return machineId;
+ }
+
+ public String getOsShort() {
+ return osShort;
+ }
+
+ public String getOsLong() {
+ return osLong;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public Object getMachineName() {
+ return machineName;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java
new file mode 100644
index 0000000000000..e2a29e2434405
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/ServicesResponse.java
@@ -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.lcn.internal.pchkdiscovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Used for deserializing the XML response of the LCN-PCHK discovery protocol.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class ServicesResponse {
+ private final Version Version;
+ private final Server Server;
+ private final ExtServices ExtServices;
+ @SuppressWarnings("unused")
+ private final Object Services = new Object();
+
+ public ServicesResponse(Version version, Server server, ExtServices extServices) {
+ this.Version = version;
+ this.Server = server;
+ this.ExtServices = extServices;
+ }
+
+ public Server getServer() {
+ return Server;
+ }
+
+ public Version getVersion() {
+ return Version;
+ }
+
+ public ExtServices getExtServices() {
+ return ExtServices;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java
new file mode 100644
index 0000000000000..6c406662474ae
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/pchkdiscovery/Version.java
@@ -0,0 +1,43 @@
+/**
+ * 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.lcn.internal.pchkdiscovery;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
+
+/**
+ * Used for deserializing the XML response of the LCN-PCHK discovery protocol.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class Version {
+ @XStreamAsAttribute
+ private final int major;
+ @XStreamAsAttribute
+ private final int minor;
+
+ public Version(int major, int minor) {
+ this.major = major;
+ this.minor = minor;
+ }
+
+ public int getMajor() {
+ return major;
+ }
+
+ public int getMinor() {
+ return minor;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java
new file mode 100644
index 0000000000000..3988283d50305
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleSubHandler.java
@@ -0,0 +1,178 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.regex.Matcher;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.HSBType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.eclipse.smarthome.core.library.types.StopMoveType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.State;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnDefs.RelayStateModifier;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.Variable;
+import org.openhab.binding.lcn.internal.common.VariableValue;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for LCN module Thing sub handlers.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractLcnModuleSubHandler implements ILcnModuleSubHandler {
+ private final Logger logger = LoggerFactory.getLogger(AbstractLcnModuleSubHandler.class);
+ protected final LcnModuleHandler handler;
+ protected final ModInfo info;
+
+ public AbstractLcnModuleSubHandler(LcnModuleHandler handler, ModInfo info) {
+ this.handler = handler;
+ this.info = info;
+ }
+
+ @Override
+ public void handleRefresh(String groupId) {
+ // can be overwritten by subclasses.
+ }
+
+ @Override
+ public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
+ unsupportedCommand(command);
+ }
+
+ @Override
+ public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ unsupportedCommand(command);
+ }
+
+ @Override
+ public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, String idWithoutGroup)
+ throws LcnException {
+ unsupportedCommand(command);
+ }
+
+ @Override
+ public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ unsupportedCommand(command);
+ }
+
+ @Override
+ public void handleCommandDimmerOutput(DimmerOutputCommand command, int number) throws LcnException {
+ unsupportedCommand(command);
+ }
+
+ @Override
+ public void handleCommandString(StringType command, int number) throws LcnException {
+ unsupportedCommand(command);
+ }
+
+ @Override
+ public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException {
+ unsupportedCommand(command);
+ }
+
+ @Override
+ public void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ unsupportedCommand(command);
+ }
+
+ @Override
+ public void handleCommandHsb(HSBType command, String groupId) throws LcnException {
+ unsupportedCommand(command);
+ }
+
+ private void unsupportedCommand(Command command) {
+ logger.warn("Unsupported command: {}: {}", getClass().getSimpleName(), command.getClass().getSimpleName());
+ }
+
+ /**
+ * Tries to parses the given PCK message. Fails silently to let another sub handler give the chance to process the
+ * message.
+ *
+ * @param pck the message to process
+ * @return true, if the message could be processed successfully
+ */
+ public boolean tryParse(String pck) {
+ Optional firstSuccessfulMatcher = getPckStatusMessagePatterns().stream().map(p -> p.matcher(pck))
+ .filter(Matcher::matches).filter(m -> handler.isMyAddress(m.group("segId"), m.group("modId")))
+ .findAny();
+
+ firstSuccessfulMatcher.ifPresent(matcher -> {
+ try {
+ handleStatusMessage(matcher);
+ } catch (LcnException e) {
+ logger.warn("Parse error: {}", e.getMessage());
+ }
+ });
+
+ return firstSuccessfulMatcher.isPresent();
+ }
+
+ /**
+ * Creates a RelayStateModifier array with all elements set to NOCHANGE.
+ *
+ * @return the created array
+ */
+ protected RelayStateModifier[] createRelayStateModifierArray() {
+ RelayStateModifier[] ret = new LcnDefs.RelayStateModifier[LcnChannelGroup.RELAY.getCount()];
+ Arrays.fill(ret, LcnDefs.RelayStateModifier.NOCHANGE);
+ return ret;
+ }
+
+ /**
+ * Updates the state of the LCN module.
+ *
+ * @param type the channel type which shall be updated
+ * @param number the Channel's number within the channel type, zero-based
+ * @param state the new state
+ */
+ protected void fireUpdate(LcnChannelGroup type, int number, State state) {
+ handler.updateChannel(type, (number + 1) + "", state);
+ }
+
+ /**
+ * Fires the current state of a Variable to openHAB. Resets running value request logic.
+ *
+ * @param matcher the pre-matched matcher
+ * @param channelId the Channel's ID to update
+ * @param variable the Variable to update
+ * @return the new variable's value
+ */
+ protected VariableValue fireUpdateAndReset(Matcher matcher, String channelId, Variable variable) {
+ VariableValue value = new VariableValue(Long.parseLong(matcher.group("value" + channelId)));
+
+ info.updateVariableValue(variable, value);
+ info.onVariableResponseReceived(variable);
+
+ fireUpdate(variable.getChannelType(), variable.getThresholdNumber().orElse(variable.getNumber()),
+ value.getState(variable));
+ return value;
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java
new file mode 100644
index 0000000000000..5589d1d258197
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/AbstractLcnModuleVariableSubHandler.java
@@ -0,0 +1,140 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.Variable;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for LCN module Thing sub handlers processing variables.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public abstract class AbstractLcnModuleVariableSubHandler extends AbstractLcnModuleSubHandler {
+ private final Logger logger = LoggerFactory.getLogger(AbstractLcnModuleVariableSubHandler.class);
+
+ public AbstractLcnModuleVariableSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ requestVariable(info, channelGroup, number);
+ info.requestFirmwareVersion();
+ }
+
+ /**
+ * Requests the current state of the given Channel.
+ *
+ * @param info the modules ModInfo cache
+ * @param channelGroup the Channel group
+ * @param number the Channel's number within the Channel group
+ */
+ protected void requestVariable(ModInfo info, LcnChannelGroup channelGroup, int number) {
+ try {
+ Variable var = getVariable(channelGroup, number);
+ info.refreshVariable(var);
+ } catch (IllegalArgumentException e) {
+ logger.warn("Could not parse variable name: {}{}", channelGroup, (number + 1));
+ }
+ }
+
+ /**
+ * Gets a Variable from the given parameters.
+ *
+ * @param channelGroup the Channel group the Variable is in
+ * @param number the number of the Variable's Channel
+ * @return the Variable
+ * @throws IllegalArgumentException when the Channel group and number do not exist
+ */
+ protected Variable getVariable(LcnChannelGroup channelGroup, int number) throws IllegalArgumentException {
+ return Variable.valueOf(channelGroup.name() + (number + 1));
+ }
+
+ /**
+ * Calculates the relative change between the current and the demanded value of a Variable.
+ *
+ * @param command the requested value
+ * @param variable the Variable type
+ * @return the difference
+ * @throws LcnException when the difference is too big
+ */
+ protected int getRelativeChange(DecimalType command, Variable variable) throws LcnException {
+ // LCN doesn't support setting thresholds or variables with absolute values. So, calculate the relative change.
+ int relativeVariableChange = (int) (command.longValue() - info.getVariableValue(variable));
+
+ int result;
+ if (relativeVariableChange > 0) {
+ result = Math.min(relativeVariableChange, getMaxAbsChange(variable));
+ } else {
+ result = Math.max(relativeVariableChange, -getMaxAbsChange(variable));
+ }
+ if (result != relativeVariableChange) {
+ logger.warn("Relative change of {} too big, limiting: {}", variable, relativeVariableChange);
+ }
+ return result;
+ }
+
+ private int getMaxAbsChange(Variable variable) {
+ switch (variable) {
+ case RVARSETPOINT1:
+ case RVARSETPOINT2:
+ case THRESHOLDREGISTER11:
+ case THRESHOLDREGISTER12:
+ case THRESHOLDREGISTER13:
+ case THRESHOLDREGISTER14:
+ case THRESHOLDREGISTER15:
+ case THRESHOLDREGISTER21:
+ case THRESHOLDREGISTER22:
+ case THRESHOLDREGISTER23:
+ case THRESHOLDREGISTER24:
+ case THRESHOLDREGISTER31:
+ case THRESHOLDREGISTER32:
+ case THRESHOLDREGISTER33:
+ case THRESHOLDREGISTER34:
+ case THRESHOLDREGISTER41:
+ case THRESHOLDREGISTER42:
+ case THRESHOLDREGISTER43:
+ case THRESHOLDREGISTER44:
+ return 1000;
+ case VARIABLE1:
+ case VARIABLE2:
+ case VARIABLE3:
+ case VARIABLE4:
+ case VARIABLE5:
+ case VARIABLE6:
+ case VARIABLE7:
+ case VARIABLE8:
+ case VARIABLE9:
+ case VARIABLE10:
+ case VARIABLE11:
+ case VARIABLE12:
+ return 4000;
+ case UNKNOWN:
+ case S0INPUT1:
+ case S0INPUT2:
+ case S0INPUT3:
+ case S0INPUT4:
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java
new file mode 100644
index 0000000000000..aff33cc1f0ebd
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/ILcnModuleSubHandler.java
@@ -0,0 +1,155 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.HSBType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.eclipse.smarthome.core.library.types.StopMoveType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Interface for LCN module Thing sub handlers processing variables.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public interface ILcnModuleSubHandler {
+ /**
+ * Gets the Patterns, the sub handler is capable to process.
+ *
+ * @return the Patterns
+ */
+ Collection getPckStatusMessagePatterns();
+
+ /**
+ * Processes the payload of a pre-matched PCK message.
+ *
+ * @param matcher the pre-matched matcher.
+ * @throws LcnException when the message cannot be processed
+ */
+ void handleStatusMessage(Matcher matcher) throws LcnException;
+
+ /**
+ * Processes a refresh request from openHAB.
+ *
+ * @param channelGroup the Channel group that shall be refreshed
+ * @param number the Channel number within the Channel group
+ */
+ void handleRefresh(LcnChannelGroup channelGroup, int number);
+
+ /**
+ * Processes a refresh request from openHAB.
+ *
+ * @param groupId the Channel ID that shall be refreshed
+ */
+ void handleRefresh(String groupId);
+
+ /**
+ * Handles a Command from openHAB.
+ *
+ * @param command the command to handle
+ * @param channelGroup the addressed Channel group
+ * @param number the Channel's number within the Channel group
+ * @throws LcnException when the command could not processed
+ */
+ void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException;
+
+ /**
+ * Handles a Command from openHAB.
+ *
+ * @param command the command to handle
+ * @param channelGroup the addressed Channel group
+ * @param number the Channel's number within the Channel group
+ * @throws LcnException when the command could not processed
+ */
+ void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number) throws LcnException;
+
+ /**
+ * Handles a Command from openHAB.
+ *
+ * @param command the command to handle
+ * @param channelGroup the addressed Channel group
+ * @param idWithoutGroup the Channel's name within the Channel group
+ * @throws LcnException when the command could not processed
+ */
+ void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, String idWithoutGroup)
+ throws LcnException;
+
+ /**
+ * Handles a Command from openHAB.
+ *
+ * @param command the command to handle
+ * @param channelGroup the addressed Channel group
+ * @param number the Channel's number within the Channel group
+ * @throws LcnException when the command could not processed
+ */
+ void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number) throws LcnException;
+
+ /**
+ * Handles a Command from openHAB.
+ *
+ * @param command the command to handle
+ * @param number the Channel's number within the Channel group
+ * @throws LcnException when the command could not processed
+ */
+ void handleCommandDimmerOutput(DimmerOutputCommand command, int number) throws LcnException;
+
+ /**
+ * Handles a Command from openHAB.
+ *
+ * @param command the command to handle
+ * @param number the Channel's number within the Channel group
+ * @throws LcnException when the command could not processed
+ */
+ void handleCommandString(StringType command, int number) throws LcnException;
+
+ /**
+ * Handles a Command from openHAB.
+ *
+ * @param command the command to handle
+ * @param channelGroup the addressed Channel group
+ * @param number the Channel's number within the Channel group
+ * @throws LcnException when the command could not processed
+ */
+ void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException;
+
+ /**
+ * Handles a Command from openHAB.
+ *
+ * @param command the command to handle
+ * @param channelGroup the addressed Channel group
+ * @param number the Channel's number within the Channel group
+ * @throws LcnException when the command could not processed
+ */
+ void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number) throws LcnException;
+
+ /**
+ * Handles a Command from openHAB.
+ *
+ * @param command the command to handle
+ * @param groupId the Channel's name within the Channel group
+ * @throws LcnException when the command could not processed
+ */
+ void handleCommandHsb(HSBType command, String groupId) throws LcnException;
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java
new file mode 100644
index 0000000000000..a911662d25cb0
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandler.java
@@ -0,0 +1,62 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.IntStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OpenClosedType;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles State changes of binary sensors of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleBinarySensorSubHandler extends AbstractLcnModuleSubHandler {
+ private static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "Bx(?\\d+)");
+
+ public LcnModuleBinarySensorSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ info.refreshBinarySensors();
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ info.onBinarySensorsResponseReceived();
+
+ boolean[] states = LcnDefs.getBooleanValue(Integer.parseInt(matcher.group("byteValue")));
+
+ IntStream.range(0, LcnChannelGroup.BINARYSENSOR.getCount())
+ .forEach(i -> fireUpdate(LcnChannelGroup.BINARYSENSOR, i,
+ states[i] ? OpenClosedType.OPEN : OpenClosedType.CLOSED));
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.singleton(PATTERN);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java
new file mode 100644
index 0000000000000..69a9102013a56
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleCodeSubHandler.java
@@ -0,0 +1,108 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles State changes of transponders and remote controls of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleCodeSubHandler extends AbstractLcnModuleSubHandler {
+ private static final Pattern TRANSPONDER_PATTERN = Pattern
+ .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.ZT(?\\d{3})(?\\d{3})(?\\d{3})");
+ private static final Pattern REMOTE_CONTROL_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX
+ + "\\.ZI(?\\d{3})(?\\d{3})(?\\d{3})(?\\d{3})(?\\d{3})");
+
+ public LcnModuleCodeSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ // nothing
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ String code = String.format("%02X%02X%02X", Integer.parseInt(matcher.group("byte0")),
+ Integer.parseInt(matcher.group("byte1")), Integer.parseInt(matcher.group("byte2")));
+
+ if (matcher.pattern() == TRANSPONDER_PATTERN) {
+ handler.triggerChannel(LcnChannelGroup.CODE, "transponder", code);
+ } else if (matcher.pattern() == REMOTE_CONTROL_PATTERN) {
+ int keyNumber = Integer.parseInt(matcher.group("key"));
+ String keyLayer;
+
+ if (keyNumber > 30) {
+ keyLayer = "D";
+ keyNumber -= 30;
+ } else if (keyNumber > 20) {
+ keyLayer = "C";
+ keyNumber -= 20;
+ } else if (keyNumber > 10) {
+ keyLayer = "B";
+ keyNumber -= 10;
+ } else if (keyNumber > 0) {
+ keyLayer = "A";
+ } else {
+ return;
+ }
+
+ int action = Integer.parseInt(matcher.group("action"));
+
+ if (action > 10) {
+ handler.triggerChannel(LcnChannelGroup.CODE, "remotecontrolbatterylow", code);
+ action -= 10;
+ }
+
+ LcnDefs.SendKeyCommand actionType;
+ switch (action) {
+ case 1:
+ actionType = LcnDefs.SendKeyCommand.HIT;
+ break;
+ case 2:
+ actionType = LcnDefs.SendKeyCommand.MAKE;
+ break;
+ case 3:
+ actionType = LcnDefs.SendKeyCommand.BREAK;
+ break;
+ default:
+ return;
+ }
+
+ handler.triggerChannel(LcnChannelGroup.CODE, "remotecontrolkey",
+ keyLayer + keyNumber + ":" + actionType.name());
+
+ handler.triggerChannel(LcnChannelGroup.CODE, "remotecontrolcode",
+ code + ":" + keyLayer + keyNumber + ":" + actionType.name());
+ }
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Arrays.asList(TRANSPONDER_PATTERN, REMOTE_CONTROL_PATTERN);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java
new file mode 100644
index 0000000000000..b05116476e3cd
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandler.java
@@ -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.lcn.internal.subhandler;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.IntStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnDefs.KeyLockStateModifier;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles Commands and State changes of key table locks of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleKeyLockTableSubHandler extends AbstractLcnModuleSubHandler {
+ private final Logger logger = LoggerFactory.getLogger(LcnModuleKeyLockTableSubHandler.class);
+ private static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX
+ + "\\.TX(?\\d{3})(?\\d{3})(?\\d{3})((?\\d{3}))?");
+
+ public LcnModuleKeyLockTableSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ info.refreshStatusLockedKeys();
+ }
+
+ @Override
+ public void handleRefresh(String groupId) {
+ // nothing
+ }
+
+ @Override
+ public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
+ KeyLockStateModifier[] keyLockStateModifiers = new LcnDefs.KeyLockStateModifier[channelGroup.getCount()];
+ Arrays.fill(keyLockStateModifiers, LcnDefs.KeyLockStateModifier.NOCHANGE);
+ keyLockStateModifiers[number] = command == OnOffType.ON ? LcnDefs.KeyLockStateModifier.ON
+ : LcnDefs.KeyLockStateModifier.OFF;
+ int tableId = channelGroup.ordinal() - LcnChannelGroup.KEYLOCKTABLEA.ordinal();
+ handler.sendPck(PckGenerator.lockKeys(tableId, keyLockStateModifiers));
+ info.refreshStatusStatusLockedKeysAfterChange();
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ info.onLockedKeysResponseReceived();
+
+ IntStream.range(0, LcnDefs.KEY_TABLE_COUNT).forEach(tableId -> {
+ String stateString = matcher.group(String.format("table%d", tableId));
+ if (stateString != null) {
+ boolean[] states = LcnDefs.getBooleanValue(Integer.parseInt(stateString));
+ try {
+ LcnChannelGroup channelGroup = LcnChannelGroup.fromTableId(tableId);
+ for (int i = 0; i < states.length; i++) {
+ fireUpdate(channelGroup, i, states[i] ? OnOffType.ON : OnOffType.OFF);
+ }
+ } catch (LcnException e) {
+ logger.warn("Failed to set key table lock state: {}", e.getMessage());
+ }
+ }
+ });
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.singleton(PATTERN);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java
new file mode 100644
index 0000000000000..6fd5d95cefa9f
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandler.java
@@ -0,0 +1,66 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles Commands and State changes of LEDs of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleLedSubHandler extends AbstractLcnModuleSubHandler {
+ public LcnModuleLedSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ info.refreshLedsAndLogic();
+ }
+
+ @Override
+ public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
+ handleCommandString(new StringType(command.toString()), number);
+ }
+
+ @Override
+ public void handleCommandString(StringType command, int number) throws LcnException {
+ handler.sendPck(PckGenerator.controlLed(number, LcnDefs.LedStatus.valueOf(command.toString())));
+ info.refreshStatusLedsAnLogicAfterChange();
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ /** Status messages are handled in {@link LcnModuleLogicSubHandler}. */
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.emptyList();
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java
new file mode 100644
index 0000000000000..21b5403ae16b1
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandler.java
@@ -0,0 +1,126 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.IntStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnDefs.LogicOpStatus;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles State changes of logic operations of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleLogicSubHandler extends AbstractLcnModuleSubHandler {
+ private final Logger logger = LoggerFactory.getLogger(LcnModuleLogicSubHandler.class);
+ private static final Pattern PATTERN_SINGLE_LOGIC = Pattern
+ .compile(LcnBindingConstants.ADDRESS_REGEX + "S(?\\d{1})(?\\d{3})");
+ private static final Pattern PATTERN_ALL = Pattern
+ .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.TL(?[AEBF]{12})(?[NTV]{4})");
+
+ public LcnModuleLogicSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ info.refreshLedsAndLogic();
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ info.onLedsAndLogicResponseReceived();
+
+ if (matcher.pattern() == PATTERN_ALL) {
+ IntStream.range(0, LcnChannelGroup.LED.getCount()).forEach(i -> {
+ switch (matcher.group("ledStates").toUpperCase().charAt(i)) {
+ case 'A':
+ fireLed(i, LcnDefs.LedStatus.OFF);
+ break;
+ case 'E':
+ fireLed(i, LcnDefs.LedStatus.ON);
+ break;
+ case 'B':
+ fireLed(i, LcnDefs.LedStatus.BLINK);
+ break;
+ case 'F':
+ fireLed(i, LcnDefs.LedStatus.FLICKER);
+ break;
+ default:
+ logger.warn("Failed to parse LED state: {}", matcher.group("ledStates"));
+ }
+ });
+ IntStream.range(0, LcnChannelGroup.LOGIC.getCount()).forEach(i -> {
+ switch (matcher.group("logicOpStates").toUpperCase().charAt(i)) {
+ case 'N':
+ fireLogic(i, LcnDefs.LogicOpStatus.NOT);
+ break;
+ case 'T':
+ fireLogic(i, LcnDefs.LogicOpStatus.OR);
+ break;
+ case 'V':
+ fireLogic(i, LcnDefs.LogicOpStatus.AND);
+ break;
+ default:
+ logger.warn("Failed to parse logic state: {}", matcher.group("logicOpStates"));
+ }
+ });
+ } else if (matcher.pattern() == PATTERN_SINGLE_LOGIC) {
+ String rawState = matcher.group("logicOpState");
+
+ LogicOpStatus state;
+ switch (rawState) {
+ case "000":
+ state = LcnDefs.LogicOpStatus.NOT;
+ break;
+ case "025":
+ state = LcnDefs.LogicOpStatus.OR;
+ break;
+ case "050":
+ state = LcnDefs.LogicOpStatus.AND;
+ break;
+ default:
+ logger.warn("Failed to parse logic state: {}", rawState);
+ return;
+ }
+ fireLogic(Integer.parseInt(matcher.group("id")) - 1, state);
+ }
+ }
+
+ private void fireLed(int number, LcnDefs.LedStatus status) {
+ fireUpdate(LcnChannelGroup.LED, number, new StringType(status.toString()));
+ }
+
+ private void fireLogic(int number, LcnDefs.LogicOpStatus status) {
+ fireUpdate(LcnChannelGroup.LOGIC, number, new StringType(status.toString()));
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Arrays.asList(PATTERN_ALL, PATTERN_SINGLE_LOGIC);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java
new file mode 100644
index 0000000000000..6ced1c8c35699
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaAckSubHandler.java
@@ -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.lcn.internal.subhandler;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handle Acks received from an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleMetaAckSubHandler extends AbstractLcnModuleSubHandler {
+ private final Logger logger = LoggerFactory.getLogger(LcnModuleMetaAckSubHandler.class);
+ /** The pattern for the Ack PCK message */
+ public static final Pattern PATTERN_POS = Pattern.compile("-M(?\\d{3})(?\\d{3})!");
+ private static final Pattern PATTERN_NEG = Pattern.compile("-M(?\\d{3})(?\\d{3})(?\\d+)");
+
+ public LcnModuleMetaAckSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ // nothing
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ if (matcher.pattern() == PATTERN_POS) {
+ handler.onAckRceived();
+ } else if (matcher.pattern() == PATTERN_NEG) {
+ logger.warn("{}: NACK received: {}", handler.getStatusMessageAddress(),
+ codeToString(Integer.parseInt(matcher.group("code"))));
+ }
+ }
+
+ private String codeToString(int code) {
+ switch (code) {
+ case LcnBindingConstants.CODE_ACK:
+ return "ACK";
+ case 5:
+ return "Unknown command";
+ case 6:
+ return "Invalid parameter count";
+ case 7:
+ return "Invalid parameter";
+ case 8:
+ return "Command not allowed (e.g. output locked)";
+ case 9:
+ return "Command not allowed by module's configuration";
+ case 10:
+ return "Module not capable";
+ case 11:
+ return "Periphery missing";
+ case 12:
+ return "Programming mode necessary";
+ case 14:
+ return "Mains fuse blown";
+ default:
+ return "Unknown";
+ }
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Arrays.asList(PATTERN_POS, PATTERN_NEG);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java
new file mode 100644
index 0000000000000..59621e8e1261f
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleMetaFirmwareSubHandler.java
@@ -0,0 +1,55 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles serial number and firmware versions received from an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleMetaFirmwareSubHandler extends AbstractLcnModuleSubHandler {
+ /** The pattern for the serial number and firmware PCK message */
+ public static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX
+ + "\\.SN(?[0-9|A-F]{10})(?[0-9|A-F]{2})FW(?[0-9|A-F]{6})HW(?\\d+)");
+
+ public LcnModuleMetaFirmwareSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ // nothing
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ info.setFirmwareVersion(Integer.parseInt(matcher.group("firmwareVersion"), 16));
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.singleton(PATTERN);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java
new file mode 100644
index 0000000000000..d7f5d1fcb179c
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandler.java
@@ -0,0 +1,182 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.HSBType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles Commands and State changes of dimmer outputs of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleOutputSubHandler extends AbstractLcnModuleSubHandler {
+ private final Logger logger = LoggerFactory.getLogger(LcnModuleOutputSubHandler.class);
+ private static final int COLOR_RAMP_MS = 1000;
+ private static final String OUTPUT_COLOR = "color";
+ private static final Pattern PERCENT_PATTERN;
+ private static final Pattern NATIVE_PATTERN;
+ private volatile HSBType currentColor = new HSBType();
+ private volatile PercentType output4 = new PercentType();
+
+ public LcnModuleOutputSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ static {
+ PERCENT_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "A(?\\d)(?\\d+)");
+ NATIVE_PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "O(?\\d)(?\\d+)");
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Arrays.asList(NATIVE_PATTERN, PERCENT_PATTERN);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ info.refreshOutput(number);
+ }
+
+ @Override
+ public void handleRefresh(String groupId) {
+ if (OUTPUT_COLOR.equals(groupId)) {
+ info.refreshAllOutputs();
+ }
+ }
+
+ @Override
+ public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
+ // don't use OnOffType.as() here, because it returns @Nullable
+ handler.sendPck(PckGenerator.dimOutput(number, command == OnOffType.ON ? 100 : 0, 0));
+ }
+
+ @Override
+ public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ handler.sendPck(PckGenerator.dimOutput(number, command.doubleValue(), 0));
+ }
+
+ @Override
+ public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, String idWithoutGroup)
+ throws LcnException {
+ if (!OUTPUT_COLOR.equals(idWithoutGroup)) {
+ throw new LcnException("Unknown group ID: " + idWithoutGroup);
+ }
+ updateAndSendColor(new HSBType(currentColor.getHue(), currentColor.getSaturation(), command));
+ }
+
+ @Override
+ public void handleCommandHsb(HSBType command, String groupId) throws LcnException {
+ if (!OUTPUT_COLOR.equals(groupId)) {
+ throw new LcnException("Unknown group ID: " + groupId);
+ }
+ updateAndSendColor(command);
+ }
+
+ private synchronized void updateAndSendColor(HSBType hsbType) throws LcnException {
+ currentColor = hsbType;
+ handler.updateChannel(LcnChannelGroup.OUTPUT, OUTPUT_COLOR, currentColor);
+
+ if (info.getFirmwareVersion() >= LcnBindingConstants.FIRMWARE_2014) {
+ handler.sendPck(PckGenerator.dimAllOutputs(currentColor.getRed().doubleValue(),
+ currentColor.getGreen().doubleValue(), currentColor.getBlue().doubleValue(), output4.doubleValue(),
+ COLOR_RAMP_MS));
+ } else {
+ handler.sendPck(PckGenerator.dimOutput(0, currentColor.getRed().doubleValue(), COLOR_RAMP_MS));
+ handler.sendPck(PckGenerator.dimOutput(1, currentColor.getGreen().doubleValue(), COLOR_RAMP_MS));
+ handler.sendPck(PckGenerator.dimOutput(2, currentColor.getBlue().doubleValue(), COLOR_RAMP_MS));
+ }
+ }
+
+ @Override
+ public void handleCommandDimmerOutput(DimmerOutputCommand command, int number) throws LcnException {
+ int rampMs = command.getRampMs();
+ if (command.isControlAllOutputs()) { // control all dimmer outputs
+ if (rampMs == LcnDefs.FIXED_RAMP_MS) {
+ // compatibility command
+ handler.sendPck(PckGenerator.controlAllOutputs(command.intValue()));
+ } else {
+ // command since firmware 180501
+ handler.sendPck(PckGenerator.dimAllOutputs(command.doubleValue(), command.doubleValue(),
+ command.doubleValue(), command.doubleValue(), rampMs));
+ }
+ } else if (command.isControlOutputs12()) { // control dimmer outputs 1+2
+ if (command.intValue() == 0 || command.intValue() == 100) {
+ handler.sendPck(PckGenerator.controlOutputs12(command.intValue() > 0, rampMs >= LcnDefs.FIXED_RAMP_MS));
+ } else {
+ // ignore ramp when dimming
+ handler.sendPck(PckGenerator.dimOutputs12(command.doubleValue()));
+ }
+ } else {
+ handler.sendPck(PckGenerator.dimOutput(number, command.doubleValue(), rampMs));
+ }
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ int outputId = Integer.parseInt(matcher.group("outputId")) - 1;
+
+ if (!LcnChannelGroup.OUTPUT.isValidId(outputId)) {
+ logger.warn("outputId out of range: {}", outputId);
+ return;
+ }
+ double percent;
+ if (matcher.pattern() == PERCENT_PATTERN) {
+ percent = Integer.parseInt(matcher.group("percent"));
+ } else if (matcher.pattern() == NATIVE_PATTERN) {
+ percent = (double) Integer.parseInt(matcher.group("value")) / 2;
+ } else {
+ logger.warn("Unexpected pattern: {}", matcher.pattern());
+ return;
+ }
+
+ info.onOutputResponseReceived(outputId);
+
+ percent = Math.min(100, Math.max(0, percent));
+
+ PercentType percentType = new PercentType((int) Math.round(percent));
+ fireUpdate(LcnChannelGroup.OUTPUT, outputId, percentType);
+
+ if (outputId == 3) {
+ output4 = percentType;
+ }
+
+ if (percent > 0) {
+ if (outputId == 0) {
+ fireUpdate(LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, UpDownType.UP);
+ } else if (outputId == 1) {
+ fireUpdate(LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0, UpDownType.DOWN);
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java
new file mode 100644
index 0000000000000..d2b166cb67adc
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandler.java
@@ -0,0 +1,86 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.IntStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnDefs.RelayStateModifier;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles Commands and State changes of Relays of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRelaySubHandler extends AbstractLcnModuleSubHandler {
+ private static final Pattern PATTERN = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX + "Rx(?\\d+)");
+
+ public LcnModuleRelaySubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ info.refreshRelays();
+ }
+
+ @Override
+ public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
+ RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray();
+ relayStateModifiers[number] = command == OnOffType.ON ? LcnDefs.RelayStateModifier.ON
+ : LcnDefs.RelayStateModifier.OFF;
+ handler.sendPck(PckGenerator.controlRelays(relayStateModifiers));
+ }
+
+ @Override
+ public void handleCommandPercent(PercentType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ // don't use OnOffType.as(), because it returns @Nullable
+ handleCommandOnOff(command.intValue() > 0 ? OnOffType.ON : OnOffType.OFF, channelGroup, number);
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ info.onRelayResponseReceived();
+
+ boolean[] states = LcnDefs.getBooleanValue(Integer.parseInt(matcher.group("byteValue")));
+
+ IntStream.range(0, LcnChannelGroup.RELAY.getCount())
+ .forEach(i -> fireUpdate(LcnChannelGroup.RELAY, i, OnOffType.from(states[i])));
+
+ IntStream.range(0, LcnChannelGroup.ROLLERSHUTTERRELAY.getCount()).forEach(i -> {
+ UpDownType state = states[i * 2 + 1] ? UpDownType.DOWN : UpDownType.UP;
+ fireUpdate(LcnChannelGroup.ROLLERSHUTTERRELAY, i, state);
+ });
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.singleton(PATTERN);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java
new file mode 100644
index 0000000000000..71b7521ebcd93
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandler.java
@@ -0,0 +1,82 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.StopMoveType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles Commands and State changes of roller shutters connected to dimmer outputs of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRollershutterOutputSubHandler extends AbstractLcnModuleSubHandler {
+ public LcnModuleRollershutterOutputSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ info.refreshOutput(number);
+ }
+
+ @Override
+ public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException {
+ // When configured as shutter in LCN-PRO, an output gets switched off, when the other is
+ // switched on and vice versa.
+ if (command == UpDownType.UP) {
+ // first output: 100%
+ handler.sendPck(PckGenerator.dimOutput(0, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS));
+ } else {
+ // second output: 100%
+ handler.sendPck(PckGenerator.dimOutput(1, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS));
+ }
+ }
+
+ @Override
+ public void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ if (command == StopMoveType.STOP) {
+ // both outputs off
+ handler.sendPck(PckGenerator.dimOutput(0, 0, 0));
+ handler.sendPck(PckGenerator.dimOutput(1, 0, 0));
+ } else {
+ // roller shutters on outputs are stateless, assume always down when MOVE is sent
+ // second output: 100%
+ handler.sendPck(PckGenerator.dimOutput(1, 100, LcnDefs.ROLLER_SHUTTER_RAMP_MS));
+ }
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ // status messages of roller shutters on dimmer outputs are handled in the dimmer output sub handler
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.emptyList();
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java
new file mode 100644
index 0000000000000..ef46add8a5f01
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandler.java
@@ -0,0 +1,77 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.StopMoveType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnDefs.RelayStateModifier;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles Commands and State changes of roller shutters connected to relays of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRollershutterRelaySubHandler extends AbstractLcnModuleSubHandler {
+ public LcnModuleRollershutterRelaySubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ info.refreshRelays();
+ }
+
+ @Override
+ public void handleCommandUpDown(UpDownType command, LcnChannelGroup channelGroup, int number) throws LcnException {
+ RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray();
+ // direction relay
+ relayStateModifiers[number * 2 + 1] = command == UpDownType.DOWN ? LcnDefs.RelayStateModifier.ON
+ : LcnDefs.RelayStateModifier.OFF;
+ // power relay
+ relayStateModifiers[number * 2] = LcnDefs.RelayStateModifier.ON;
+ handler.sendPck(PckGenerator.controlRelays(relayStateModifiers));
+ }
+
+ @Override
+ public void handleCommandStopMove(StopMoveType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ RelayStateModifier[] relayStateModifiers = createRelayStateModifierArray();
+ // power relay
+ relayStateModifiers[number * 2] = command == StopMoveType.MOVE ? LcnDefs.RelayStateModifier.ON
+ : LcnDefs.RelayStateModifier.OFF;
+ handler.sendPck(PckGenerator.controlRelays(relayStateModifiers));
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ // status messages of roller shutters on relays are handled in the relay sub handler
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.emptyList();
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java
new file mode 100644
index 0000000000000..a2611cc38213c
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandler.java
@@ -0,0 +1,66 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.common.Variable;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles Commands and State changes of regulator locks of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRvarLockSubHandler extends AbstractLcnModuleVariableSubHandler {
+ public LcnModuleRvarLockSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleRefresh(LcnChannelGroup channelGroup, int number) {
+ super.handleRefresh(LcnChannelGroup.RVARSETPOINT, number);
+ }
+
+ @Override
+ public void handleCommandOnOff(OnOffType command, LcnChannelGroup channelGroup, int number) throws LcnException {
+ boolean locked = command == OnOffType.ON;
+ handler.sendPck(PckGenerator.lockRegulator(number, locked));
+
+ // request new lock state, if the module doesn't send it on itself
+ Variable variable = getVariable(LcnChannelGroup.RVARSETPOINT, number);
+ if (variable.shouldPollStatusAfterRegulatorLock(info.getFirmwareVersion(), locked)) {
+ info.refreshVariable(variable);
+ }
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ // status messages are handled in the RVar setpoint sub handler
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.emptyList();
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java
new file mode 100644
index 0000000000000..5bf2d0c29c79d
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandler.java
@@ -0,0 +1,79 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.common.Variable;
+import org.openhab.binding.lcn.internal.common.VariableValue;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles Commands and State changes of regulator setpoints of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRvarSetpointSubHandler extends AbstractLcnModuleVariableSubHandler {
+ private static final Pattern PATTERN = Pattern
+ .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.S(?\\d)(?\\d+)");
+
+ public LcnModuleRvarSetpointSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ Variable variable = getVariable(channelGroup, number);
+
+ if (info.hasExtendedMeasurementProcessing()) {
+ handler.sendPck(PckGenerator.setSetpointAbsolute(number, command.intValue()));
+ } else {
+ try {
+ int relativeVariableChange = getRelativeChange(command, variable);
+ handler.sendPck(
+ PckGenerator.setSetpointRelative(number, LcnDefs.RelVarRef.CURRENT, relativeVariableChange));
+ } catch (LcnException e) {
+ // current value unknown for some reason, refresh it in case we come again here
+ info.refreshVariable(variable);
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) throws LcnException {
+ Variable variable = Variable.setPointIdToVar(Integer.parseInt(matcher.group("id")) - 1);
+ VariableValue value = fireUpdateAndReset(matcher, "", variable);
+
+ fireUpdate(LcnChannelGroup.RVARLOCK, variable.getNumber(), OnOffType.from(value.isRegulatorLocked()));
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.singleton(PATTERN);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java
new file mode 100644
index 0000000000000..428b8352f822c
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandler.java
@@ -0,0 +1,58 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.Variable;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Handles Commands and State changes of S0 counter inputs of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleS0CounterSubHandler extends AbstractLcnModuleVariableSubHandler {
+ private static final Pattern PATTERN = Pattern
+ .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.C(?\\d)(?\\d+)");
+
+ public LcnModuleS0CounterSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ throw new LcnException("Setting S0 counters is not supported");
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) throws LcnException {
+ fireUpdateAndReset(matcher, "", Variable.s0IdToVar(Integer.parseInt(matcher.group("id")) - 1));
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Collections.singleton(PATTERN);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java
new file mode 100644
index 0000000000000..744b61db88f9f
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandler.java
@@ -0,0 +1,105 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.IntStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.common.Variable;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles Commands and State changes of thresholds of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleThresholdSubHandler extends AbstractLcnModuleVariableSubHandler {
+ private final Logger logger = LoggerFactory.getLogger(LcnModuleThresholdSubHandler.class);
+ private static final Pattern PATTERN = Pattern
+ .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.T(?\\d)(?\\d)(?\\d+)");
+ private static final Pattern PATTERN_BEFORE_2013 = Pattern.compile(LcnBindingConstants.ADDRESS_REGEX
+ + "\\.S1(?\\d{5})(?\\d{5})(?\\d{5})(?\\d{5})(?\\d{5})(?\\d{5})");
+
+ public LcnModuleThresholdSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ Variable variable = getVariable(channelGroup, number);
+ try {
+ int relativeChange = getRelativeChange(command, variable);
+ handler.sendPck(PckGenerator.setThresholdRelative(variable, LcnDefs.RelVarRef.CURRENT, relativeChange,
+ info.hasExtendedMeasurementProcessing()));
+
+ // request new value, if the module doesn't send it on itself
+ if (variable.shouldPollStatusAfterCommand(info.getFirmwareVersion())) {
+ info.refreshVariable(variable);
+ }
+ } catch (LcnException e) {
+ // current value unknown for some reason, refresh it in case we come again here
+ info.refreshVariable(variable);
+ throw e;
+ }
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) {
+ IntStream stream;
+ Optional groupSuffix;
+ int registerNumber;
+ if (matcher.pattern() == PATTERN) {
+ int thresholdId = Integer.parseInt(matcher.group("thresholdId")) - 1;
+ registerNumber = Integer.parseInt(matcher.group("registerId")) - 1;
+ stream = IntStream.rangeClosed(thresholdId, thresholdId);
+ groupSuffix = Optional.of("");
+ } else if (matcher.pattern() == PATTERN_BEFORE_2013) {
+ stream = IntStream.range(0, LcnDefs.THRESHOLD_COUNT_BEFORE_2013);
+ groupSuffix = Optional.empty();
+ registerNumber = 0;
+ } else {
+ logger.warn("Unexpected pattern: {}", matcher.pattern());
+ return;
+ }
+
+ stream.forEach(i -> {
+ try {
+ fireUpdateAndReset(matcher, groupSuffix.orElse(String.valueOf(i)),
+ Variable.thrsIdToVar(registerNumber, i));
+ } catch (LcnException e) {
+ logger.warn("Parse error: {}", e.getMessage());
+ }
+ });
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Arrays.asList(PATTERN, PATTERN_BEFORE_2013);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java
new file mode 100644
index 0000000000000..ac1ce6f3cc945
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandler.java
@@ -0,0 +1,88 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.openhab.binding.lcn.internal.LcnBindingConstants;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.PckGenerator;
+import org.openhab.binding.lcn.internal.common.Variable;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles Commands and State changes of variables of an LCN module.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleVariableSubHandler extends AbstractLcnModuleVariableSubHandler {
+ private final Logger logger = LoggerFactory.getLogger(LcnModuleVariableSubHandler.class);
+ private static final Pattern PATTERN = Pattern
+ .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.A(?\\d{3})(?\\d+)");
+ private static final Pattern PATTERN_LEGACY = Pattern
+ .compile(LcnBindingConstants.ADDRESS_REGEX + "\\.(?\\d+)");
+
+ public LcnModuleVariableSubHandler(LcnModuleHandler handler, ModInfo info) {
+ super(handler, info);
+ }
+
+ @Override
+ public void handleCommandDecimal(DecimalType command, LcnChannelGroup channelGroup, int number)
+ throws LcnException {
+ Variable variable = getVariable(channelGroup, number);
+ try {
+ int relativeChange = getRelativeChange(command, variable);
+ handler.sendPck(PckGenerator.setVariableRelative(variable, LcnDefs.RelVarRef.CURRENT, relativeChange));
+
+ // request new value, if the module doesn't send it on itself
+ if (variable.shouldPollStatusAfterCommand(info.getFirmwareVersion())) {
+ info.refreshVariable(variable);
+ }
+ } catch (LcnException e) {
+ // current value unknown for some reason, refresh it in case we come again here
+ info.refreshVariable(variable);
+ throw e;
+ }
+ }
+
+ @Override
+ public void handleStatusMessage(Matcher matcher) throws LcnException {
+ Variable variable;
+ if (matcher.pattern() == PATTERN) {
+ variable = Variable.varIdToVar(Integer.parseInt(matcher.group("id")) - 1);
+ } else if (matcher.pattern() == PATTERN_LEGACY) {
+ variable = info.getLastRequestedVarWithoutTypeInResponse();
+ info.setLastRequestedVarWithoutTypeInResponse(Variable.UNKNOWN); // Reset
+ } else {
+ logger.warn("Unexpected pattern: {}", matcher.pattern());
+ return;
+ }
+ fireUpdateAndReset(matcher, "", variable);
+ }
+
+ @Override
+ public Collection getPckStatusMessagePatterns() {
+ return Arrays.asList(PATTERN, PATTERN_LEGACY);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..c494e66acc295
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ LCN Binding
+ This is the binding for Local Control Network (LCN)
+ Fabian Wolter
+
+
diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml
new file mode 100644
index 0000000000000..439256829226b
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/config/config.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+ Hostname
+ The hostname or the IP address of the PCK gateway
+ network-address
+ true
+
+
+ Port
+ The IP port of the PCK gateway
+ 4114
+ true
+
+
+ Username
+ The login username of the PCK gateway
+ true
+
+
+ Password
+ The login password of the PCK gateway
+ password
+
+
+ Dimming Mode
+ IMPORTANT: Dimming range of all modules. Must be the same value as configured in LCN-PRO (Options/Settings/Expert Settings). If you only have modules with firmware newer than Feb. 2013, you probably want to choose 0 - 200.]]>
+ native200
+
+ 0 - 50
+ 0 - 200
+
+ true
+
+
+ Connection Timeout
+ Period after which an LCN command is resent, when no acknowledge has been received (in ms).
+ 3500
+ true
+
+
+
+
+
+ Module ID
+ The module ID, configured in LCN-PRO
+
+
+ Segment ID
+ The segment ID the module is in (0 if no segments are present)
+
+
+
+
+
+ Group Number
+ The group number, configured in LCN-PRO
+
+
+ Module ID
+ The module ID of any module in the group. The state of this module is used for visualization of the
+ group as representative for all modules.
+
+
+ Segment ID
+ The segment ID of all modules in this group (0 if no segments are present)
+ 0
+
+
+
+
+
+ Unit
+ Unit of the sensor
+ native
+
+ LCN Native
+ Temperature (°C or °F)
+ Light (Lux)
+ CO₂ (ppm)
+ Power (W)
+ Energy (kWh)
+ Current (mA)
+ Voltage (V)
+ Angle (°)
+ Windspeed (m/s)
+
+ true
+
+
+ Pulses per kWh
+ Only for S0 counters (power or energy)
+ 1000
+
+
+
diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties
new file mode 100644
index 0000000000000..29ca7960c205e
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/i18n/lcn_de.properties
@@ -0,0 +1,171 @@
+# binding
+binding.lcn.name = LCN Binding
+binding.lcn.description = Binding f�r Local Control Network (LCN)
+
+# thing types
+thing-type.lcn.pckGateway.label = LCN-PCK-Koppler
+thing-type.lcn.pckGateway.description = z.B. die LCN-PCHK-Software oder das Hutschienenmodul LCN-PKE
+thing-type.lcn.module.label = LCN-Modul
+thing-type.lcn.module.description = z.B. LCN-UPP, LCN-SH, LCN-HU
+thing-type.lcn.group.label = LCN-Gruppe
+thing-type.lcn.group.description = Eine Gruppe mit mehreren Modulen, wie in LCN-PRO parametriert
+
+# thing type config description
+thing-type.config.lcn.pckGateway.hostname.description = Hostname oder die IP-Adresse des PCK-Kopplers
+thing-type.config.lcn.pckGateway.port.description = Netzwerk-Port auf dem der PCK-Koppler l�uft
+thing-type.config.lcn.pckGateway.username.description = Benutzername vom PCK-Koppler
+thing-type.config.lcn.pckGateway.password.description = Login-Passwort vom PCK-Koppler
+thing-type.config.lcn.pckGateway.mode.description = WICHTIG: Der Dimmbereich von allen LCN-Modulen. Muss der gleiche Wert, wie in LCN-PRO sein (Optionen/Einstellungen/Experteneinstellungen). Wenn nur Module �lter als 2013 im Bus vorhanden sind, muss hier wahrscheinlich 0 - 200 ausgew�hlt werden.
+thing-type.config.lcn.pckGateway.timeoutMs.description = Zeit nach der eine PCK-Nachricht erneut gesendet wird, wenn vom Modul keine positive Quittung empfangen wurde.
+
+thing-type.config.lcn.module.moduleId.label = Modul-ID
+thing-type.config.lcn.module.moduleId.description = Modul-ID, wie in LCN-PRO parametriert
+thing-type.config.lcn.module.segmentId.label = Segment-ID
+thing-type.config.lcn.module.segmentId.description = ID des Segments, in dem sich das Modul befindet (0 wenn keine Segmente vorhanden sind)
+
+thing-type.config.lcn.group.groupId.label = Gruppennummer
+thing-type.config.lcn.group.groupId.description = Nummer der Gruppe, wie in LCN-PRO parametriert
+thing-type.config.lcn.group.moduleId.label = Modul-ID eines Moduls aus der Gruppe
+thing-type.config.lcn.group.moduleId.description = Der Zustand dieses Moduls wird zur Visualisierung der Gruppe, stellvertretend f�r alle Module, genutzt
+thing-type.config.lcn.group.segmentId.label = Segment-ID
+thing-type.config.lcn.group.segmentId.description = Segment-ID in dem sich die Module der Gruppe befinden (0 wenn keine Segmente vorhanden sind)
+
+# channel type config description
+channel-type.config.lcn.variable.unit.label = Einheit
+channel-type.config.lcn.variable.unit.description = Einheit des Sensors
+channel-type.config.lcn.variable.unit.option.native = LCN-Wert
+channel-type.config.lcn.variable.unit.option.temperature = Temperatur (�C)
+channel-type.config.lcn.variable.unit.option.light = Licht (Lux)
+channel-type.config.lcn.variable.unit.option.co2 = CO\u2082 (ppm)
+channel-type.config.lcn.variable.unit.option.power = Leistung (W)
+channel-type.config.lcn.variable.unit.option.energy = Z�hlerstand (kWh)
+channel-type.config.lcn.variable.unit.option.current = Strom (mA)
+channel-type.config.lcn.variable.unit.option.voltage = Spannung (V)
+channel-type.config.lcn.variable.unit.option.angle = Winkel (�)
+channel-type.config.lcn.variable.unit.option.windspeed = Windgeschwindigkeit (m/s)
+channel-type.config.lcn.variable.parameter.label = Impulse pro kWh
+channel-type.config.lcn.variable.parameter.description = Nur f�r S0-Z�hler
+
+# channel types
+channel-group-type.lcn.outputs.label = Ausg�nge
+channel-group-type.lcn.outputs.channel.1.label = Ausgang 1
+channel-group-type.lcn.outputs.channel.2.label = Ausgang 2
+channel-group-type.lcn.outputs.channel.3.label = Ausgang 3
+channel-group-type.lcn.outputs.channel.4.label = Ausgang 4
+channel-group-type.lcn.outputs.channel.color.label = RGB-Steuerung f�r Ausg�nge 1-3
+channel-group-type.lcn.rollershutteroutputs.label = Rolll�den an Ausg�ngen
+channel-group-type.lcn.rollershutteroutputs.channel.1.label = Rolll�den an Ausg�ngen 1+2
+channel-group-type.lcn.relays.label = Relais
+channel-group-type.lcn.relays.channel.1.label = Relais 1
+channel-group-type.lcn.relays.channel.2.label = Relais 2
+channel-group-type.lcn.relays.channel.3.label = Relais 3
+channel-group-type.lcn.relays.channel.4.label = Relais 4
+channel-group-type.lcn.relays.channel.5.label = Relais 5
+channel-group-type.lcn.relays.channel.6.label = Relais 6
+channel-group-type.lcn.relays.channel.7.label = Relais 7
+channel-group-type.lcn.relays.channel.8.label = Relais 8
+channel-group-type.lcn.rollershutterrelays.label = Rolll�den an Relais
+channel-group-type.lcn.rollershutterrelays.channel.1.label = Rolll�den an Relais 1+2
+channel-group-type.lcn.rollershutterrelays.channel.2.label = Rolll�den an Relais 3+4
+channel-group-type.lcn.rollershutterrelays.channel.3.label = Rolll�den an Relais 5+6
+channel-group-type.lcn.rollershutterrelays.channel.4.label = Rolll�den an Relais 7+8
+channel-group-type.lcn.logics.label = Logik-Funktionen
+channel-group-type.lcn.logics.channel.1.label = Logik-Funktion 1
+channel-group-type.lcn.logics.channel.2.label = Logik-Funktion 2
+channel-group-type.lcn.logics.channel.3.label = Logik-Funktion 3
+channel-group-type.lcn.logics.channel.4.label = Logik-Funktion 4
+channel-group-type.lcn.binarysensors.label = Bin�rsensoren
+channel-group-type.lcn.binarysensors.channel.1.label = Bin�rsensor 1
+channel-group-type.lcn.binarysensors.channel.2.label = Bin�rsensor 2
+channel-group-type.lcn.binarysensors.channel.3.label = Bin�rsensor 3
+channel-group-type.lcn.binarysensors.channel.4.label = Bin�rsensor 4
+channel-group-type.lcn.binarysensors.channel.5.label = Bin�rsensor 5
+channel-group-type.lcn.binarysensors.channel.6.label = Bin�rsensor 6
+channel-group-type.lcn.binarysensors.channel.7.label = Bin�rsensor 7
+channel-group-type.lcn.binarysensors.channel.8.label = Bin�rsensor 8
+channel-group-type.lcn.variables.label = Variablen
+channel-group-type.lcn.variables.channel.1.label = Variable 1
+channel-group-type.lcn.variables.channel.2.label = Variable 2
+channel-group-type.lcn.variables.channel.3.label = Variable 3
+channel-group-type.lcn.variables.channel.4.label = Variable 4
+channel-group-type.lcn.variables.channel.5.label = Variable 5
+channel-group-type.lcn.variables.channel.6.label = Variable 6
+channel-group-type.lcn.variables.channel.7.label = Variable 7
+channel-group-type.lcn.variables.channel.8.label = Variable 8
+channel-group-type.lcn.variables.channel.9.label = Variable 9
+channel-group-type.lcn.variables.channel.10.label = Variable 10
+channel-group-type.lcn.variables.channel.11.label = Variable 11
+channel-group-type.lcn.variables.channel.12.label = Variable 12
+channel-group-type.lcn.rvarsetpoints.label = Regler
+channel-group-type.lcn.rvarsetpoints.channel.1.label = Regler 1 Sollwert
+channel-group-type.lcn.rvarsetpoints.channel.2.label = Regler 2 Sollwert
+channel-group-type.lcn.rvarlocks.label = Regler Sperren
+channel-group-type.lcn.rvarlocks.channel.1.label = Regler 1 Sperre
+channel-group-type.lcn.rvarlocks.channel.2.label = Regler 2 Sperre
+channel-group-type.lcn.thresholdregisters1.label = Schwellwertregister 1
+channel-group-type.lcn.thresholdregisters1.channel.1.label = Schwellwert 1
+channel-group-type.lcn.thresholdregisters1.channel.2.label = Schwellwert 2
+channel-group-type.lcn.thresholdregisters1.channel.3.label = Schwellwert 3
+channel-group-type.lcn.thresholdregisters1.channel.4.label = Schwellwert 4
+channel-group-type.lcn.thresholdregisters1.channel.5.label = Schwellwert 5
+channel-group-type.lcn.thresholdregisters2.label = Schwellwertregister 2
+channel-group-type.lcn.thresholdregisters2.channel.1.label = Schwellwert 1
+channel-group-type.lcn.thresholdregisters2.channel.2.label = Schwellwert 2
+channel-group-type.lcn.thresholdregisters2.channel.3.label = Schwellwert 3
+channel-group-type.lcn.thresholdregisters2.channel.4.label = Schwellwert 4
+channel-group-type.lcn.thresholdregisters3.label = Schwellwertregister 3
+channel-group-type.lcn.thresholdregisters3.channel.1.label = Schwellwert 1
+channel-group-type.lcn.thresholdregisters3.channel.2.label = Schwellwert 2
+channel-group-type.lcn.thresholdregisters3.channel.3.label = Schwellwert 3
+channel-group-type.lcn.thresholdregisters3.channel.4.label = Schwellwert 4
+channel-group-type.lcn.thresholdregisters4.label = Schwellwertregister 4
+channel-group-type.lcn.thresholdregisters4.channel.1.label = Schwellwert 1
+channel-group-type.lcn.thresholdregisters4.channel.2.label = Schwellwert 2
+channel-group-type.lcn.thresholdregisters4.channel.3.label = Schwellwert 3
+channel-group-type.lcn.thresholdregisters4.channel.4.label = Schwellwert 4
+channel-group-type.lcn.s0inputs.label = S0-Z�hler
+channel-group-type.lcn.s0inputs.channel.1.label = S0-Z�hler 1
+channel-group-type.lcn.s0inputs.channel.2.label = S0-Z�hler 2
+channel-group-type.lcn.s0inputs.channel.3.label = S0-Z�hler 3
+channel-group-type.lcn.s0inputs.channel.4.label = S0-Z�hler 4
+channel-group-type.lcn.keyslocktablea.label = Tastensperren Tabelle A
+channel-group-type.lcn.keyslocktablea.channel.1.label = A1 Sperre
+channel-group-type.lcn.keyslocktablea.channel.2.label = A2 Sperre
+channel-group-type.lcn.keyslocktablea.channel.3.label = A3 Sperre
+channel-group-type.lcn.keyslocktablea.channel.4.label = A4 Sperre
+channel-group-type.lcn.keyslocktablea.channel.5.label = A5 Sperre
+channel-group-type.lcn.keyslocktablea.channel.6.label = A6 Sperre
+channel-group-type.lcn.keyslocktablea.channel.7.label = A7 Sperre
+channel-group-type.lcn.keyslocktablea.channel.8.label = A8 Sperre
+channel-group-type.lcn.keyslocktableb.label = Tastensperren Tabelle B
+channel-group-type.lcn.keyslocktableb.channel.1.label = B1 Sperre
+channel-group-type.lcn.keyslocktableb.channel.2.label = B2 Sperre
+channel-group-type.lcn.keyslocktableb.channel.3.label = B3 Sperre
+channel-group-type.lcn.keyslocktableb.channel.4.label = B4 Sperre
+channel-group-type.lcn.keyslocktableb.channel.5.label = B5 Sperre
+channel-group-type.lcn.keyslocktableb.channel.6.label = B6 Sperre
+channel-group-type.lcn.keyslocktableb.channel.7.label = B7 Sperre
+channel-group-type.lcn.keyslocktableb.channel.8.label = B8 Sperre
+channel-group-type.lcn.keyslocktablec.label = Tastensperren Tabelle C
+channel-group-type.lcn.keyslocktablec.channel.1.label = C1 Sperre
+channel-group-type.lcn.keyslocktablec.channel.2.label = C2 Sperre
+channel-group-type.lcn.keyslocktablec.channel.3.label = C3 Sperre
+channel-group-type.lcn.keyslocktablec.channel.4.label = C4 Sperre
+channel-group-type.lcn.keyslocktablec.channel.5.label = C5 Sperre
+channel-group-type.lcn.keyslocktablec.channel.6.label = C6 Sperre
+channel-group-type.lcn.keyslocktablec.channel.7.label = C7 Sperre
+channel-group-type.lcn.keyslocktablec.channel.8.label = C8 Sperre
+channel-group-type.lcn.keyslocktabled.label = Tastensperren Tabelle D
+channel-group-type.lcn.keyslocktabled.channel.1.label = D1 Sperre
+channel-group-type.lcn.keyslocktabled.channel.2.label = D2 Sperre
+channel-group-type.lcn.keyslocktabled.channel.3.label = D3 Sperre
+channel-group-type.lcn.keyslocktabled.channel.4.label = D4 Sperre
+channel-group-type.lcn.keyslocktabled.channel.5.label = D5 Sperre
+channel-group-type.lcn.keyslocktabled.channel.6.label = D6 Sperre
+channel-group-type.lcn.keyslocktabled.channel.7.label = D7 Sperre
+channel-group-type.lcn.keyslocktabled.channel.8.label = D8 Sperre
+channel-group-type.lcn.codes.label = Transponder & Fernbedienungen
+channel-group-type.lcn.codes.channel.transponder.label = Transponder-Code
+channel-group-type.lcn.codes.channel.remotecontrolkey.label = Fernbedienung Tasten
+channel-group-type.lcn.codes.channel.remotecontrolcode.label = Fernbedienung Tasten mit Zutrittscode
+channel-group-type.lcn.codes.channel.remotecontrolbatterylow.label = Fernbedienung Batterie leer
diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..7921cd5534715
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/main/resources/ESH-INF/thing/thing-types.xml
@@ -0,0 +1,634 @@
+
+
+
+
+ LCN-PCHK Gateway
+ An LCN gateway speaking the PCK language. E.g. LCN-PCHK software or the DIN rail device LCN-PKE.
+
+
+
+
+
+
+
+
+
+ LCN Module
+ An LCN bus module, e.g. LCN-UPP, LCN-SH, LCN-HU
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LCN Group
+ An LCN group with multiple modules, configured in LCN-PRO
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Dimmer
+ Output
+ veto
+
+
+
+ Color
+ Color
+ veto
+
+
+
+ Dimmer Outputs
+
+
+ Output 1
+
+
+ Output 2
+
+
+ Output 3
+
+
+ Output 4
+
+
+ RGB Color Control (Outputs 1-3)
+
+
+
+
+
+ Switch
+ Relay
+ veto
+
+
+
+ Relays
+
+
+ Relay 1
+
+
+ Relay 2
+
+
+ Relay 3
+
+
+ Relay 4
+
+
+ Relay 5
+
+
+ Relay 6
+
+
+ Relay 7
+
+
+ Relay 8
+
+
+
+
+
+ Rollershutter
+ Roller Shutter
+ veto
+
+
+
+ Roller Shutter on Relays
+
+
+ Shutter 1-2
+
+
+ Shutter 3-4
+
+
+ Shutter 5-6
+
+
+ Shutter 7-8
+
+
+
+
+
+ Roller Shutter on Dimmer Outputs
+
+
+ Shutter 1-2
+
+
+
+
+
+ String
+ LED
+
+
+ Off
+ On
+ Blink
+ Flicker
+
+
+
+
+
+ LEDs
+
+
+ LED 1
+
+
+ LED 2
+
+
+ LED 3
+
+
+ LED 4
+
+
+ LED 5
+
+
+ LED 6
+
+
+ LED 7
+
+
+ LED 8
+
+
+ LED 9
+
+
+ LED 10
+
+
+ LED 11
+
+
+ LED 12
+
+
+
+
+
+ String
+ Logic Operation
+
+
+ Not (not fulfilled)
+ Or (partly fulfilled)
+ And (fulfilled)
+
+
+
+
+
+ Logic Operations
+
+
+ Logic Operation 1
+
+
+ Logic Operation 2
+
+
+ Logic Operation 3
+
+
+ Logic Operation 4
+
+
+
+
+
+ Contact
+ Binary Sensor
+ veto
+
+
+
+ Binary Sensors
+
+
+ Binary Sensor 1
+
+
+ Binary Sensor 2
+
+
+ Binary Sensor 3
+
+
+ Binary Sensor 4
+
+
+ Binary Sensor 5
+
+
+ Binary Sensor 6
+
+
+ Binary Sensor 7
+
+
+ Binary Sensor 8
+
+
+
+
+
+ Number
+ Variable
+
+
+
+
+ Variables
+
+
+ Variable 1 or TVar
+
+
+ Variable 2
+
+
+ Variable 3
+
+
+ Variable 4
+
+
+ Variable 5
+
+
+ Variable 6
+
+
+ Variable 7
+
+
+ Variable 8
+
+
+ Variable 9
+
+
+ Variable 10
+
+
+ Variable 11
+
+
+ Variable 12
+
+
+
+
+
+ RVar Setpoints
+
+
+ R1Var Setpoint
+
+
+ R2Var Setpoint
+
+
+
+
+
+ Switch
+ RVar Lock State
+ veto
+
+
+
+ RVar Lock State
+
+
+ R1Var Lock
+
+
+ R2Var Lock
+
+
+
+
+
+ Threshold Register 1
+
+
+ Threshold 1
+
+
+ Threshold 2
+
+
+ Threshold 3
+
+
+ Threshold 4
+
+
+ Threshold 5
+ Only before Feb. 2013
+
+
+
+
+
+ Threshold Register 2
+
+
+ Threshold 1
+
+
+ Threshold 2
+
+
+ Threshold 3
+
+
+ Threshold 4
+
+
+
+
+
+ Threshold Register 3
+
+
+ Threshold 1
+
+
+ Threshold 2
+
+
+ Threshold 3
+
+
+ Threshold 4
+
+
+
+
+
+ Threshold Register 4
+
+
+ Threshold 1
+
+
+ Threshold 2
+
+
+ Threshold 3
+
+
+ Threshold 4
+
+
+
+
+
+ S0 Counters
+
+
+ S0 Counter 1
+
+
+ S0 Counter 2
+
+
+ S0 Counter 3
+
+
+ S0 Counter 4
+
+
+
+
+
+ Switch
+ Keys Lock State
+
+
+
+ Keys Lock State of Table A
+
+
+ Key 1
+
+
+ Key 2
+
+
+ Key 3
+
+
+ Key 4
+
+
+ Key 5
+
+
+ Key 6
+
+
+ Key 7
+
+
+ Key 8
+
+
+
+
+
+ Keys Lock State of Table B
+
+
+ Key 1
+
+
+ Key 2
+
+
+ Key 3
+
+
+ Key 4
+
+
+ Key 5
+
+
+ Key 6
+
+
+ Key 7
+
+
+ Key 8
+
+
+
+
+
+ Keys Lock State of Table C
+
+
+ Key 1
+
+
+ Key 2
+
+
+ Key 3
+
+
+ Key 4
+
+
+ Key 5
+
+
+ Key 6
+
+
+ Key 7
+
+
+ Key 8
+
+
+
+
+
+ Keys Lock State of Table D
+
+
+ Key 1
+
+
+ Key 2
+
+
+ Key 3
+
+
+ Key 4
+
+
+ Key 5
+
+
+ Key 6
+
+
+ Key 7
+
+
+ Key 8
+
+
+
+
+
+ Transponder & Remote Control
+
+
+
+
+
+
+
+
+
+ trigger
+ Transponder Codes
+
+
+
+
+ trigger
+ Remote Control Keys
+
+
+
+
+ trigger
+ Remote Control with Access Control Code
+
+
+
+
+ trigger
+ Remote Control Low Battery
+
+
+
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java
new file mode 100644
index 0000000000000..2867607cc6d90
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/ModuleActionsTest.java
@@ -0,0 +1,199 @@
+/**
+ * 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.lcn.internal;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.MockitoAnnotations;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Test class for {@link LcnModuleActions}.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class ModuleActionsTest {
+ private LcnModuleActions a = new LcnModuleActions();
+ private final LcnModuleHandler handler = mock(LcnModuleHandler.class);
+ @Captor
+ private @NonNullByDefault({}) ArgumentCaptor byteBufferCaptor;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ a = new LcnModuleActions();
+ a.setThingHandler(handler);
+ }
+
+ private byte[] stringToByteBuffer(String string) {
+ return string.getBytes(LcnDefs.LCN_ENCODING);
+ }
+
+ @Test
+ public void testSendDynamicText1CharRow1() throws LcnException {
+ a.sendDynamicText(1, "a");
+
+ verify(handler).sendPck(stringToByteBuffer("GTDT11a\0\0\0\0\0\0\0\0\0\0\0"));
+ }
+
+ @Test
+ public void testSendDynamicText1ChunkRow1() throws LcnException {
+ a.sendDynamicText(1, "abcdfghijklm");
+
+ verify(handler).sendPck(stringToByteBuffer("GTDT11abcdfghijklm"));
+ }
+
+ @Test
+ public void testSendDynamicText1Chunk1CharRow1() throws LcnException {
+ a.sendDynamicText(1, "abcdfghijklmn");
+
+ verify(handler, times(2)).sendPck(byteBufferCaptor.capture());
+
+ assertThat(byteBufferCaptor.getAllValues(), contains(stringToByteBuffer("GTDT11abcdfghijklm"),
+ stringToByteBuffer("GTDT12n\0\0\0\0\0\0\0\0\0\0\0")));
+ }
+
+ @Test
+ public void testSendDynamicText5ChunksRow1() throws LcnException {
+ a.sendDynamicText(1, "abcdfghijklmnopqrstuvwxyzabcdfghijklmnopqrstuvwxyzabcdfghijk");
+
+ verify(handler, times(5)).sendPck(byteBufferCaptor.capture());
+
+ assertThat(byteBufferCaptor.getAllValues(),
+ containsInAnyOrder(stringToByteBuffer("GTDT11abcdfghijklm"), stringToByteBuffer("GTDT12nopqrstuvwxy"),
+ stringToByteBuffer("GTDT13zabcdfghijkl"), stringToByteBuffer("GTDT14mnopqrstuvwx"),
+ stringToByteBuffer("GTDT15yzabcdfghijk")));
+ }
+
+ @Test
+ public void testSendDynamicText5Chunks1CharRow1Truncated() throws LcnException {
+ a.sendDynamicText(1, "abcdfghijklmnopqrstuvwxyzabcdfghijklmnopqrstuvwxyzabcdfghijkl");
+
+ verify(handler, times(5)).sendPck(byteBufferCaptor.capture());
+
+ assertThat(byteBufferCaptor.getAllValues(),
+ containsInAnyOrder(stringToByteBuffer("GTDT11abcdfghijklm"), stringToByteBuffer("GTDT12nopqrstuvwxy"),
+ stringToByteBuffer("GTDT13zabcdfghijkl"), stringToByteBuffer("GTDT14mnopqrstuvwx"),
+ stringToByteBuffer("GTDT15yzabcdfghijk")));
+ }
+
+ @Test
+ public void testSendDynamicText5Chunks1UmlautRow1Truncated() throws LcnException {
+ a.sendDynamicText(1, "äcdfghijklmnopqrstuvwxyzabcdfghijklmnopqrstuvwxyzabcdfghijkl");
+
+ verify(handler, times(5)).sendPck(byteBufferCaptor.capture());
+
+ assertThat(byteBufferCaptor.getAllValues(),
+ containsInAnyOrder(stringToByteBuffer("GTDT11äcdfghijklm"), stringToByteBuffer("GTDT12nopqrstuvwxy"),
+ stringToByteBuffer("GTDT13zabcdfghijkl"), stringToByteBuffer("GTDT14mnopqrstuvwx"),
+ stringToByteBuffer("GTDT15yzabcdfghijk")));
+ }
+
+ @Test
+ public void testSendDynamicTextRow4() throws LcnException {
+ a.sendDynamicText(4, "abcdfghijklmn");
+
+ verify(handler, times(2)).sendPck(byteBufferCaptor.capture());
+
+ assertThat(byteBufferCaptor.getAllValues(), contains(stringToByteBuffer("GTDT41abcdfghijklm"),
+ stringToByteBuffer("GTDT42n\0\0\0\0\0\0\0\0\0\0\0")));
+ }
+
+ @Test
+ public void testSendDynamicTextSplitInCharacter() throws LcnException {
+ a.sendDynamicText(4, "Test 123 öäüß");
+
+ verify(handler, times(2)).sendPck(byteBufferCaptor.capture());
+
+ String string1 = "GTDT41Test 123 ö";
+ ByteBuffer chunk1 = ByteBuffer.allocate(stringToByteBuffer(string1).length + 1);
+ chunk1.put(stringToByteBuffer(string1));
+ chunk1.put((byte) -61); // first byte of ä
+
+ ByteBuffer chunk2 = ByteBuffer.allocate(18);
+ chunk2.put(stringToByteBuffer("GTDT42"));
+ chunk2.put((byte) -92); // second byte of ä
+ chunk2.put(stringToByteBuffer("üß\0\0\0\0\0\0"));
+
+ assertThat(byteBufferCaptor.getAllValues(), contains(chunk1.array(), chunk2.array()));
+ }
+
+ @Test
+ public void testSendKeysInvalidTable() throws LcnException {
+ a.hitKey("E", 3, "MAKE");
+ verify(handler, times(0)).sendPck(anyString());
+ }
+
+ @Test
+ public void testSendKeysNullTable() throws LcnException {
+ a.hitKey(null, 3, "MAKE");
+ verify(handler, times(0)).sendPck(anyString());
+ }
+
+ @Test
+ public void testSendKeysNullAction() throws LcnException {
+ a.hitKey("D", 3, null);
+ verify(handler, times(0)).sendPck(anyString());
+ }
+
+ @Test
+ public void testSendKeysInvalidKey0() throws LcnException {
+ a.hitKey("D", 0, "MAKE");
+ verify(handler, times(0)).sendPck(anyString());
+ }
+
+ @Test
+ public void testSendKeysInvalidKey9() throws LcnException {
+ a.hitKey("D", 9, "MAKE");
+ verify(handler, times(0)).sendPck(anyString());
+ }
+
+ @Test
+ public void testSendKeysInvalidAction() throws LcnException {
+ a.hitKey("D", 8, "invalid");
+ verify(handler, times(0)).sendPck(anyString());
+ }
+
+ @Test
+ public void testSendKeysA1Hit() throws LcnException {
+ a.hitKey("a", 1, "HIT");
+
+ verify(handler).sendPck("TSK--10000000");
+ }
+
+ @Test
+ public void testSendKeysC8Hit() throws LcnException {
+ a.hitKey("C", 8, "break");
+
+ verify(handler).sendPck("TS--O00000001");
+ }
+
+ @Test
+ public void testSendKeysD3Make() throws LcnException {
+ a.hitKey("D", 3, "MAKE");
+
+ verify(handler).sendPck("TS---L00100000");
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java
new file mode 100644
index 0000000000000..38feefcc26362
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/pchkdiscovery/LcnPchkDiscoveryServiceTest.java
@@ -0,0 +1,58 @@
+/**
+ * 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.lcn.internal.pchkdiscovery;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for {@link LcnPchkDiscoveryService}.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnPchkDiscoveryServiceTest {
+ private LcnPchkDiscoveryService s = new LcnPchkDiscoveryService();
+ private ServicesResponse r = s.xmlToServiceResponse(RESPONSE);
+ private static final String RESPONSE = "LCN-PCHK 3.2.2 running on Unix/Linux PCHK 3.2.2 bus ";
+
+ @Before
+ public void setUp() {
+ s = new LcnPchkDiscoveryService();
+ r = s.xmlToServiceResponse(RESPONSE);
+ }
+
+ @Test
+ public void testXmlMachineId() {
+ assertThat(r.getServer().getMachineId(), is("b8:27:eb:fe:a4:bb"));
+ }
+
+ @Test
+ public void testXmlMachineName() {
+ assertThat(r.getServer().getMachineName(), is("raspberrypi"));
+ }
+
+ @Test
+ public void testXmlServerContent() {
+ assertThat(r.getServer().getContent(), is("LCN-PCHK 3.2.2 running on Unix/Linux"));
+ }
+
+ @Test
+ public void testXmlPort() {
+ assertThat(r.getExtServices().getExtService().getLocalPort(), is(4114));
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java
new file mode 100644
index 0000000000000..7cee0058d48b5
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/AbstractTestLcnModuleSubHandler.java
@@ -0,0 +1,43 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.when;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.openhab.binding.lcn.internal.LcnModuleHandler;
+import org.openhab.binding.lcn.internal.connection.ModInfo;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class AbstractTestLcnModuleSubHandler {
+ @Mock
+ protected @NonNullByDefault({}) LcnModuleHandler handler;
+ @Mock
+ protected @NonNullByDefault({}) ModInfo info;
+
+ public AbstractTestLcnModuleSubHandler() {
+ setUp();
+ }
+
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(handler.isMyAddress("000", "005")).thenReturn(true);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java
new file mode 100644
index 0000000000000..ba6f4a2581cf8
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleBinarySensorSubHandlerTest.java
@@ -0,0 +1,77 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OpenClosedType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleBinarySensorSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleBinarySensorSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleBinarySensorSubHandler(handler, info);
+ }
+
+ @Test
+ public void testStatusAllClosed() {
+ l.tryParse("=M000005Bx000");
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "1", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "2", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "3", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "4", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "5", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "6", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "7", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "8", OpenClosedType.CLOSED);
+ }
+
+ @Test
+ public void testStatusAllOpen() {
+ l.tryParse("=M000005Bx255");
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "1", OpenClosedType.OPEN);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "2", OpenClosedType.OPEN);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "3", OpenClosedType.OPEN);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "5", OpenClosedType.OPEN);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "6", OpenClosedType.OPEN);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "7", OpenClosedType.OPEN);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "8", OpenClosedType.OPEN);
+ }
+
+ @Test
+ public void testStatus1And7Closed() {
+ l.tryParse("=M000005Bx065");
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "1", OpenClosedType.OPEN);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "2", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "3", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "4", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "5", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "6", OpenClosedType.CLOSED);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "7", OpenClosedType.OPEN);
+ verify(handler).updateChannel(LcnChannelGroup.BINARYSENSOR, "8", OpenClosedType.CLOSED);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java
new file mode 100644
index 0000000000000..011bc56801970
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleKeyLockTableSubHandlerTest.java
@@ -0,0 +1,173 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleKeyLockTableSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleKeyLockTableSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleKeyLockTableSubHandler(handler, info);
+ }
+
+ @Test
+ public void testStatus() {
+ l.tryParse("=M000005.TX098036000255");
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "1", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "2", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "3", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "4", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "5", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "6", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "7", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEA, "8", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "1", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "2", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "3", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "4", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "5", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "6", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "7", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEB, "8", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "1", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "2", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "3", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "4", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "5", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "6", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "7", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLEC, "8", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "1", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "2", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "3", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "4", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "5", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "6", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "7", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.KEYLOCKTABLED, "8", OnOffType.ON);
+ }
+
+ @Test
+ public void testHandleCommandA1Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEA, 0);
+ verify(handler).sendPck("TXA0-------");
+ }
+
+ @Test
+ public void testHandleCommandA1On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEA, 0);
+ verify(handler).sendPck("TXA1-------");
+ }
+
+ @Test
+ public void testHandleCommandA8Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEA, 7);
+ verify(handler).sendPck("TXA-------0");
+ }
+
+ @Test
+ public void testHandleCommandA8On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEA, 7);
+ verify(handler).sendPck("TXA-------1");
+ }
+
+ @Test
+ public void testHandleCommandB1Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEB, 0);
+ verify(handler).sendPck("TXB0-------");
+ }
+
+ @Test
+ public void testHandleCommandB1On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEB, 0);
+ verify(handler).sendPck("TXB1-------");
+ }
+
+ @Test
+ public void testHandleCommandB8Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEB, 7);
+ verify(handler).sendPck("TXB-------0");
+ }
+
+ @Test
+ public void testHandleCommandB8On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEB, 7);
+ verify(handler).sendPck("TXB-------1");
+ }
+
+ @Test
+ public void testHandleCommandC1Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEC, 0);
+ verify(handler).sendPck("TXC0-------");
+ }
+
+ @Test
+ public void testHandleCommandC1On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEC, 0);
+ verify(handler).sendPck("TXC1-------");
+ }
+
+ @Test
+ public void testHandleCommandC8Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLEC, 7);
+ verify(handler).sendPck("TXC-------0");
+ }
+
+ @Test
+ public void testHandleCommandC8On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLEC, 7);
+ verify(handler).sendPck("TXC-------1");
+ }
+
+ @Test
+ public void testHandleCommandD1Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLED, 0);
+ verify(handler).sendPck("TXD0-------");
+ }
+
+ @Test
+ public void testHandleCommandD1On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLED, 0);
+ verify(handler).sendPck("TXD1-------");
+ }
+
+ @Test
+ public void testHandleCommandD8Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.KEYLOCKTABLED, 7);
+ verify(handler).sendPck("TXD-------0");
+ }
+
+ @Test
+ public void testHandleCommandD8On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.KEYLOCKTABLED, 7);
+ verify(handler).sendPck("TXD-------1");
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java
new file mode 100644
index 0000000000000..aea07c1f19923
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLedSubHandlerTest.java
@@ -0,0 +1,84 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleLedSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleLedSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleLedSubHandler(handler, info);
+ }
+
+ @Test
+ public void testHandleCommandLed1Off() throws LcnException {
+ l.handleCommandString(new StringType(LcnDefs.LedStatus.OFF.name()), 0);
+ verify(handler).sendPck("LA001A");
+ }
+
+ @Test
+ public void testHandleCommandLed1On() throws LcnException {
+ l.handleCommandString(new StringType(LcnDefs.LedStatus.ON.name()), 0);
+ verify(handler).sendPck("LA001E");
+ }
+
+ @Test
+ public void testHandleCommandLed1Blink() throws LcnException {
+ l.handleCommandString(new StringType(LcnDefs.LedStatus.BLINK.name()), 0);
+ verify(handler).sendPck("LA001B");
+ }
+
+ @Test
+ public void testHandleCommandLed1Flicker() throws LcnException {
+ l.handleCommandString(new StringType(LcnDefs.LedStatus.FLICKER.name()), 0);
+ verify(handler).sendPck("LA001F");
+ }
+
+ @Test
+ public void testHandleCommandLed12On() throws LcnException {
+ l.handleCommandString(new StringType(LcnDefs.LedStatus.ON.name()), 11);
+ verify(handler).sendPck("LA012E");
+ }
+
+ @Test
+ public void testHandleOnOffCommandLed1Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.LED, 0);
+ verify(handler).sendPck("LA001A");
+ }
+
+ @Test
+ public void testHandleOnOffCommandLed1On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.LED, 0);
+ verify(handler).sendPck("LA001E");
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java
new file mode 100644
index 0000000000000..106fd18f6d45d
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleLogicSubHandlerTest.java
@@ -0,0 +1,106 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleLogicSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private static final StringType ON = new StringType("ON");
+ private static final StringType OFF = new StringType("OFF");
+ private static final StringType BLINK = new StringType("BLINK");
+ private static final StringType FLICKER = new StringType("FLICKER");
+ private static final StringType NOT = new StringType("NOT");
+ private static final StringType OR = new StringType("OR");
+ private static final StringType AND = new StringType("AND");
+ private @NonNullByDefault({}) LcnModuleLogicSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleLogicSubHandler(handler, info);
+ }
+
+ @Test
+ public void testStatusLedOffLogicNot() {
+ l.tryParse("=M000005.TLAAAAAAAAAAAANNNN");
+ verify(handler).updateChannel(LcnChannelGroup.LED, "1", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "2", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "3", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "4", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "5", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "6", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "7", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "8", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "9", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "10", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "11", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "12", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "1", NOT);
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "2", NOT);
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "3", NOT);
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "4", NOT);
+ }
+
+ @Test
+ public void testStatusMixed() {
+ l.tryParse("=M000005.TLAEBFAAAAAAAFNVNT");
+ verify(handler).updateChannel(LcnChannelGroup.LED, "1", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "2", ON);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "3", BLINK);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "4", FLICKER);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "5", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "6", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "7", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "8", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "9", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "10", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "11", OFF);
+ verify(handler).updateChannel(LcnChannelGroup.LED, "12", FLICKER);
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "1", NOT);
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "2", AND);
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "3", NOT);
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "4", OR);
+ }
+
+ @Test
+ public void testStatusSingleLogic1Not() {
+ l.tryParse("=M000005S1000");
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "1", NOT);
+ }
+
+ @Test
+ public void testStatusSingleLogic4Or() {
+ l.tryParse("=M000005S4025");
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "4", OR);
+ }
+
+ @Test
+ public void testStatusSingleLogic3And() {
+ l.tryParse("=M000005S3050");
+ verify(handler).updateChannel(LcnChannelGroup.LOGIC, "3", AND);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java
new file mode 100644
index 0000000000000..e18eebff250cc
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleOutputSubHandlerTest.java
@@ -0,0 +1,198 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import java.math.BigDecimal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.DimmerOutputCommand;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnDefs;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleOutputSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleOutputSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleOutputSubHandler(handler, info);
+ }
+
+ @Test
+ public void testStatusOutput1OffPercent() {
+ l.tryParse("=M000005A1000");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(0));
+ }
+
+ @Test
+ public void testStatusOutput2OffPercent() {
+ l.tryParse("=M000005A2000");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(0));
+ }
+
+ @Test
+ public void testStatusOutput1OffNative() {
+ l.tryParse("=M000005O1000");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(0));
+ }
+
+ @Test
+ public void testStatusOutput2OffNative() {
+ l.tryParse("=M000005O2000");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(0));
+ }
+
+ @Test
+ public void testStatusOutput1OnPercent() {
+ l.tryParse("=M000005A1100");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(100));
+ }
+
+ @Test
+ public void testStatusOutput2OnPercent() {
+ l.tryParse("=M000005A2100");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(100));
+ }
+
+ @Test
+ public void testStatusOutput1OnNative() {
+ l.tryParse("=M000005O1200");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(100));
+ }
+
+ @Test
+ public void testStatusOutput2OnNative() {
+ l.tryParse("=M000005O2200");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(100));
+ }
+
+ @Test
+ public void testStatusOutput2On50Percent() {
+ l.tryParse("=M000005A2050");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "2", new PercentType(50));
+ }
+
+ @Test
+ public void testStatusOutput1On50Native() {
+ l.tryParse("=M000005O1100");
+ verify(handler).updateChannel(LcnChannelGroup.OUTPUT, "1", new PercentType(50));
+ }
+
+ @Test
+ public void testHandleCommandOutput1On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.OUTPUT, 0);
+ verify(handler).sendPck("A1DI100000");
+ }
+
+ @Test
+ public void testHandleCommandOutput2On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.OUTPUT, 1);
+ verify(handler).sendPck("A2DI100000");
+ }
+
+ @Test
+ public void testHandleCommandOutput1Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.OUTPUT, 0);
+ verify(handler).sendPck("A1DI000000");
+ }
+
+ @Test
+ public void testHandleCommandOutput2Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.OUTPUT, 1);
+ verify(handler).sendPck("A2DI000000");
+ }
+
+ @Test
+ public void testHandleCommandOutput1Percent10() throws LcnException {
+ l.handleCommandPercent(new PercentType(99), LcnChannelGroup.OUTPUT, 0);
+ verify(handler).sendPck("A1DI099000");
+ }
+
+ @Test
+ public void testHandleCommandOutput2Percent1() throws LcnException {
+ l.handleCommandPercent(new PercentType(1), LcnChannelGroup.OUTPUT, 1);
+ verify(handler).sendPck("A2DI001000");
+ }
+
+ @Test
+ public void testHandleCommandOutput1Percent995() throws LcnException {
+ l.handleCommandPercent(new PercentType(BigDecimal.valueOf(99.5)), LcnChannelGroup.OUTPUT, 0);
+ verify(handler).sendPck("O1DI199000");
+ }
+
+ @Test
+ public void testHandleCommandOutput2Percent05() throws LcnException {
+ l.handleCommandPercent(new PercentType(BigDecimal.valueOf(0.5)), LcnChannelGroup.OUTPUT, 1);
+ verify(handler).sendPck("O2DI001000");
+ }
+
+ @Test
+ public void testHandleCommandDimmerOutputAll60FixedRamp() throws LcnException {
+ l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(60), true, false, LcnDefs.FIXED_RAMP_MS),
+ 0);
+ verify(handler).sendPck("AH060");
+ }
+
+ @Test
+ public void testHandleCommandDimmerOutputAll40CustomRamp() throws LcnException {
+ l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(40), true, false, 1000), 0);
+ verify(handler).sendPck("OY080080080080004");
+ }
+
+ @Test
+ public void testHandleCommandDimmerOutput12Value100FixedRamp() throws LcnException {
+ l.handleCommandDimmerOutput(
+ new DimmerOutputCommand(BigDecimal.valueOf(100), false, true, LcnDefs.FIXED_RAMP_MS), 0);
+ verify(handler).sendPck("X2001200200");
+ }
+
+ @Test
+ public void testHandleCommandDimmerOutput12Value0FixedRamp() throws LcnException {
+ l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(0), false, true, LcnDefs.FIXED_RAMP_MS),
+ 0);
+ verify(handler).sendPck("X2001000000");
+ }
+
+ @Test
+ public void testHandleCommandDimmerOutput12Value100NoRamp() throws LcnException {
+ l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(100), false, true, 0), 0);
+ verify(handler).sendPck("X2001253253");
+ }
+
+ @Test
+ public void testHandleCommandDimmerOutput12Value0NoRamp() throws LcnException {
+ l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(0), false, true, 0), 0);
+ verify(handler).sendPck("X2001252252");
+ }
+
+ @Test
+ public void testHandleCommandDimmerOutput12Value40() throws LcnException {
+ l.handleCommandDimmerOutput(new DimmerOutputCommand(BigDecimal.valueOf(40), false, true, 0), 0);
+ verify(handler).sendPck("AY040040");
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.java
new file mode 100644
index 0000000000000..25c181309b02e
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRelaySubHandlerTest.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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.PercentType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRelaySubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleRelaySubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleRelaySubHandler(handler, info);
+ }
+
+ @Test
+ public void testStatusAllOff() {
+ l.tryParse("=M000005Rx000");
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "1", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "2", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "3", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "4", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "5", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "6", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "7", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "8", OnOffType.OFF);
+ }
+
+ @Test
+ public void testStatusAllOn() {
+ l.tryParse("=M000005Rx255");
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "1", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "2", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "3", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "5", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "6", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "7", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "8", OnOffType.ON);
+ }
+
+ @Test
+ public void testStatusRelay1Relay7On() {
+ l.tryParse("=M000005Rx065");
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "1", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "2", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "3", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "4", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "5", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "6", OnOffType.OFF);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "7", OnOffType.ON);
+ verify(handler).updateChannel(LcnChannelGroup.RELAY, "8", OnOffType.OFF);
+ }
+
+ @Test
+ public void testHandleCommandRelay1On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RELAY, 0);
+ verify(handler).sendPck("R81-------");
+ }
+
+ @Test
+ public void testHandleCommandRelay8On() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RELAY, 7);
+ verify(handler).sendPck("R8-------1");
+ }
+
+ @Test
+ public void testHandleCommandRelay1Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RELAY, 0);
+ verify(handler).sendPck("R80-------");
+ }
+
+ @Test
+ public void testHandleCommandRelay8Off() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RELAY, 7);
+ verify(handler).sendPck("R8-------0");
+ }
+
+ @Test
+ public void testHandleCommandRelay8Percent1() throws LcnException {
+ l.handleCommandPercent(new PercentType(1), LcnChannelGroup.RELAY, 7);
+ verify(handler).sendPck("R8-------1");
+ }
+
+ @Test
+ public void testHandleCommandRelay1Percent0() throws LcnException {
+ l.handleCommandPercent(PercentType.ZERO, LcnChannelGroup.RELAY, 0);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java
new file mode 100644
index 0000000000000..3809f0a099788
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterOutputSubHandlerTest.java
@@ -0,0 +1,66 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.StopMoveType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRollershutterOutputSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleRollershutterOutputSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleRollershutterOutputSubHandler(handler, info);
+ }
+
+ @Test
+ public void testUp() throws LcnException {
+ l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0);
+ verify(handler).sendPck("A1DI100008");
+ }
+
+ @Test
+ public void testDown() throws LcnException {
+ l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0);
+ verify(handler).sendPck("A2DI100008");
+ }
+
+ @Test
+ public void testStop() throws LcnException {
+ l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0);
+ verify(handler).sendPck("A1DI000000");
+ verify(handler).sendPck("A2DI000000");
+ }
+
+ @Test
+ public void testMove() throws LcnException {
+ l.handleCommandStopMove(StopMoveType.MOVE, LcnChannelGroup.ROLLERSHUTTEROUTPUT, 0);
+ verify(handler).sendPck("A2DI100008");
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java
new file mode 100644
index 0000000000000..99816dc13d055
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRollershutterRelaySubHandlerTest.java
@@ -0,0 +1,89 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.StopMoveType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRollershutterRelaySubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleRollershutterRelaySubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleRollershutterRelaySubHandler(handler, info);
+ }
+
+ @Test
+ public void testUp1() throws LcnException {
+ l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 0);
+ verify(handler).sendPck("R810------");
+ }
+
+ @Test
+ public void testUp4() throws LcnException {
+ l.handleCommandUpDown(UpDownType.UP, LcnChannelGroup.ROLLERSHUTTERRELAY, 3);
+ verify(handler).sendPck("R8------10");
+ }
+
+ @Test
+ public void testDown1() throws LcnException {
+ l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 0);
+ verify(handler).sendPck("R811------");
+ }
+
+ @Test
+ public void testDown4() throws LcnException {
+ l.handleCommandUpDown(UpDownType.DOWN, LcnChannelGroup.ROLLERSHUTTERRELAY, 3);
+ verify(handler).sendPck("R8------11");
+ }
+
+ @Test
+ public void testStop1() throws LcnException {
+ l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTERRELAY, 0);
+ verify(handler).sendPck("R80-------");
+ }
+
+ @Test
+ public void testStop4() throws LcnException {
+ l.handleCommandStopMove(StopMoveType.STOP, LcnChannelGroup.ROLLERSHUTTERRELAY, 3);
+ verify(handler).sendPck("R8------0-");
+ }
+
+ @Test
+ public void testMove1() throws LcnException {
+ l.handleCommandStopMove(StopMoveType.MOVE, LcnChannelGroup.ROLLERSHUTTERRELAY, 0);
+ verify(handler).sendPck("R81-------");
+ }
+
+ @Test
+ public void testMove4() throws LcnException {
+ l.handleCommandStopMove(StopMoveType.MOVE, LcnChannelGroup.ROLLERSHUTTERRELAY, 3);
+ verify(handler).sendPck("R8------1-");
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java
new file mode 100644
index 0000000000000..8acf7e33e4c63
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarLockSubHandlerTest.java
@@ -0,0 +1,64 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRvarLockSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleRvarLockSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleRvarLockSubHandler(handler, info);
+ }
+
+ @Test
+ public void testLock1() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RVARLOCK, 0);
+ verify(handler).sendPck("REAXS");
+ }
+
+ @Test
+ public void testLock2() throws LcnException {
+ l.handleCommandOnOff(OnOffType.ON, LcnChannelGroup.RVARLOCK, 1);
+ verify(handler).sendPck("REBXS");
+ }
+
+ @Test
+ public void testUnlock1() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RVARLOCK, 0);
+ verify(handler).sendPck("REAXA");
+ }
+
+ @Test
+ public void testUnlock2() throws LcnException {
+ l.handleCommandOnOff(OnOffType.OFF, LcnChannelGroup.RVARLOCK, 1);
+ verify(handler).sendPck("REBXA");
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java
new file mode 100644
index 0000000000000..be0d3df53395b
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleRvarSetpointSubHandlerTest.java
@@ -0,0 +1,138 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.Variable;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleRvarSetpointSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleRvarSetpointSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleRvarSetpointSubHandler(handler, info);
+ }
+
+ @Test
+ public void testhandleCommandRvar1Positive() throws LcnException {
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(true);
+ l.handleCommandDecimal(new DecimalType(1000), LcnChannelGroup.RVARSETPOINT, 0);
+ verify(handler).sendPck("X2030032000");
+ }
+
+ @Test
+ public void testhandleCommandRvar2Positive() throws LcnException {
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(true);
+ l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 1);
+ verify(handler).sendPck("X2030096100");
+ }
+
+ @Test
+ public void testhandleCommandRvar1Negative() throws LcnException {
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(true);
+ l.handleCommandDecimal(new DecimalType(0), LcnChannelGroup.RVARSETPOINT, 0);
+ verify(handler).sendPck("X2030043232");
+ }
+
+ @Test
+ public void testhandleCommandRvar2Negative() throws LcnException {
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(true);
+ l.handleCommandDecimal(new DecimalType(999), LcnChannelGroup.RVARSETPOINT, 1);
+ verify(handler).sendPck("X2030104001");
+ }
+
+ @Test
+ public void testhandleCommandRvar1PositiveLegacy() throws LcnException {
+ when(info.getVariableValue(Variable.RVARSETPOINT1)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 0);
+ verify(handler).sendPck("REASA+100");
+ }
+
+ @Test
+ public void testhandleCommandRvar2PositiveLegacy() throws LcnException {
+ when(info.getVariableValue(Variable.RVARSETPOINT2)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.RVARSETPOINT, 1);
+ verify(handler).sendPck("REBSA+100");
+ }
+
+ @Test
+ public void testhandleCommandRvar1NegativeLegacy() throws LcnException {
+ when(info.getVariableValue(Variable.RVARSETPOINT1)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.RVARSETPOINT, 0);
+ verify(handler).sendPck("REASA-100");
+ }
+
+ @Test
+ public void testhandleCommandRvar2NegativeLegacy() throws LcnException {
+ when(info.getVariableValue(Variable.RVARSETPOINT2)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.RVARSETPOINT, 1);
+ verify(handler).sendPck("REBSA-100");
+ }
+
+ @Test
+ public void testRvar1() {
+ l.tryParse("=M000005.S11234");
+ verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "1", new DecimalType(1234));
+ verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "1", OnOffType.OFF);
+ }
+
+ @Test
+ public void testRvar2() {
+ l.tryParse("=M000005.S21234");
+ verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "2", new DecimalType(1234));
+ verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "2", OnOffType.OFF);
+ }
+
+ @Test
+ public void testRvar1SensorDefective() {
+ l.tryParse("=M000005.S132512");
+ verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "1", new StringType("DEFECTIVE"));
+ verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "1", OnOffType.OFF);
+ }
+
+ @Test
+ public void testRvar1Locked() {
+ l.tryParse("=M000005.S134002");
+ verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "1", new DecimalType(1234));
+ verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "1", OnOffType.ON);
+ }
+
+ @Test
+ public void testRvar2Locked() {
+ l.tryParse("=M000005.S234002");
+ verify(handler).updateChannel(LcnChannelGroup.RVARSETPOINT, "2", new DecimalType(1234));
+ verify(handler).updateChannel(LcnChannelGroup.RVARLOCK, "2", OnOffType.ON);
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java
new file mode 100644
index 0000000000000..4f5f900c17e35
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleS0CounterSubHandlerTest.java
@@ -0,0 +1,57 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleS0CounterSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleS0CounterSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleS0CounterSubHandler(handler, info);
+ }
+
+ @Test
+ public void testZero() {
+ l.tryParse("=M000005.C10");
+ verify(handler).updateChannel(LcnChannelGroup.S0INPUT, "1", new DecimalType(0));
+ }
+
+ @Test
+ public void testMaxValue() {
+ l.tryParse("=M000005.C14294967295");
+ verify(handler).updateChannel(LcnChannelGroup.S0INPUT, "1", new DecimalType(4294967295L));
+ }
+
+ @Test
+ public void test4() {
+ l.tryParse("=M000005.C412345");
+ verify(handler).updateChannel(LcnChannelGroup.S0INPUT, "4", new DecimalType(12345));
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java
new file mode 100644
index 0000000000000..ad3ee3bb85ab5
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleThresholdSubHandlerTest.java
@@ -0,0 +1,133 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.Variable;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleThresholdSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleThresholdSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleThresholdSubHandler(handler, info);
+ }
+
+ @Test
+ public void testThreshold11() {
+ l.tryParse("=M000005.T1112345");
+ verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "1", new DecimalType(12345));
+ }
+
+ @Test
+ public void testThreshold14() {
+ l.tryParse("=M000005.T140");
+ verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "4", new DecimalType(0));
+ }
+
+ @Test
+ public void testThreshold41() {
+ l.tryParse("=M000005.T4112345");
+ verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER4, "1", new DecimalType(12345));
+ }
+
+ @Test
+ public void testThresholdLegacy() {
+ l.tryParse("=M000005.S1123451123411123000000000112345");
+ verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "1", new DecimalType(12345));
+ verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "2", new DecimalType(11234));
+ verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "3", new DecimalType(11123));
+ verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "4", new DecimalType(0));
+ verify(handler).updateChannel(LcnChannelGroup.THRESHOLDREGISTER1, "5", new DecimalType(1));
+ }
+
+ @Test
+ public void testhandleCommandThreshold11Positive() throws LcnException {
+ when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(true);
+ l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 0);
+ verify(handler).sendPck("SSR0100AR11");
+ }
+
+ @Test
+ public void testhandleCommandThreshold11Negative() throws LcnException {
+ when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(true);
+ l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.THRESHOLDREGISTER1, 0);
+ verify(handler).sendPck("SSR0100SR11");
+ }
+
+ @Test
+ public void testhandleCommandThreshold44Positive() throws LcnException {
+ when(info.getVariableValue(Variable.THRESHOLDREGISTER44)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(true);
+ l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER4, 3);
+ verify(handler).sendPck("SSR0100AR44");
+ }
+
+ @Test
+ public void testhandleCommandThreshold44Negative() throws LcnException {
+ when(info.getVariableValue(Variable.THRESHOLDREGISTER44)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(true);
+ l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.THRESHOLDREGISTER4, 3);
+ verify(handler).sendPck("SSR0100SR44");
+ }
+
+ @Test
+ public void testhandleCommandThreshold11LegacyPositive() throws LcnException {
+ when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 0);
+ verify(handler).sendPck("SSR0100A10000");
+ }
+
+ @Test
+ public void testhandleCommandThreshold11LegacyNegative() throws LcnException {
+ when(info.getVariableValue(Variable.THRESHOLDREGISTER11)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ l.handleCommandDecimal(new DecimalType(900), LcnChannelGroup.THRESHOLDREGISTER1, 0);
+ verify(handler).sendPck("SSR0100S10000");
+ }
+
+ @Test
+ public void testhandleCommandThreshold14Legacy() throws LcnException {
+ when(info.getVariableValue(Variable.THRESHOLDREGISTER14)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 3);
+ verify(handler).sendPck("SSR0100A00010");
+ }
+
+ @Test
+ public void testhandleCommandThreshold15Legacy() throws LcnException {
+ when(info.getVariableValue(Variable.THRESHOLDREGISTER15)).thenReturn(1000L);
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.THRESHOLDREGISTER1, 4);
+ verify(handler).sendPck("SSR0100A00001");
+ }
+}
diff --git a/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java
new file mode 100644
index 0000000000000..de976ee4acb8d
--- /dev/null
+++ b/bundles/org.openhab.binding.lcn/src/test/java/org/openhab/binding/lcn/internal/subhandler/LcnModuleVariableSubHandlerTest.java
@@ -0,0 +1,89 @@
+/**
+ * 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.lcn.internal.subhandler;
+
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.junit.Before;
+import org.junit.Test;
+import org.openhab.binding.lcn.internal.common.LcnChannelGroup;
+import org.openhab.binding.lcn.internal.common.LcnException;
+import org.openhab.binding.lcn.internal.common.Variable;
+
+/**
+ * Test class.
+ *
+ * @author Fabian Wolter - Initial contribution
+ */
+@NonNullByDefault
+public class LcnModuleVariableSubHandlerTest extends AbstractTestLcnModuleSubHandler {
+ private @NonNullByDefault({}) LcnModuleVariableSubHandler l;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ l = new LcnModuleVariableSubHandler(handler, info);
+ }
+
+ @Test
+ public void testStatusVariable1() {
+ l.tryParse("=M000005.A00112345");
+ verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "1", new DecimalType(12345));
+ }
+
+ @Test
+ public void testStatusVariable12() {
+ l.tryParse("=M000005.A01212345");
+ verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "12", new DecimalType(12345));
+ }
+
+ @Test
+ public void testStatusLegacyVariable3() {
+ when(info.getLastRequestedVarWithoutTypeInResponse()).thenReturn(Variable.VARIABLE3);
+ l.tryParse("=M000005.12345");
+ verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "3", new DecimalType(12345));
+ }
+
+ @Test
+ public void testHandleCommandLegacyTvarPositive() throws LcnException {
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ when(info.getVariableValue(Variable.VARIABLE1)).thenReturn(1000L);
+ l.handleCommandDecimal(new DecimalType(1234), LcnChannelGroup.VARIABLE, 0);
+ verify(handler).sendPck("ZA234");
+ }
+
+ @Test
+ public void testHandleCommandLegacyTvarNegative() throws LcnException {
+ when(info.hasExtendedMeasurementProcessing()).thenReturn(false);
+ when(info.getVariableValue(Variable.VARIABLE1)).thenReturn(2000L);
+ l.handleCommandDecimal(new DecimalType(1100), LcnChannelGroup.VARIABLE, 0);
+ verify(handler).sendPck("ZS900");
+ }
+
+ @Test
+ public void testStatusVariable10SensorDefective() {
+ l.tryParse("=M000005.A01032512");
+ verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "10", new StringType("DEFECTIVE"));
+ }
+
+ @Test
+ public void testStatusVariable8NotConfigured() {
+ l.tryParse("=M000005.A00865535");
+ verify(handler).updateChannel(LcnChannelGroup.VARIABLE, "8", new StringType("Not configured in LCN-PRO"));
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 83c0d00f65d47..dec0ca57a7b4e 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -132,6 +132,7 @@
org.openhab.binding.konnected
org.openhab.binding.kostalinverter
org.openhab.binding.lametrictime
+ org.openhab.binding.lcn
org.openhab.binding.leapmotion
org.openhab.binding.lghombot
org.openhab.binding.lgtvserial