diff --git a/CODEOWNERS b/CODEOWNERS
index e15f903cd8b..c4a70a3c800 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -120,6 +120,7 @@
/bundles/org.openhab.binding.helios/ @kgoderis
/bundles/org.openhab.binding.heliosventilation/ @ramack
/bundles/org.openhab.binding.heos/ @Wire82
+/bundles/org.openhab.binding.herzborg/ @Sonic-Amiga
/bundles/org.openhab.binding.homeconnect/ @bruestel
/bundles/org.openhab.binding.homematic/ @FStolte @gerrieg @mdicke2s
/bundles/org.openhab.binding.homewizard/ @Daniel-42
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 639dcf9c3c6..7c2e899f57c 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -591,6 +591,11 @@
org.openhab.binding.heos
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.herzborg
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.homeconnect
diff --git a/bundles/org.openhab.binding.herzborg/NOTICE b/bundles/org.openhab.binding.herzborg/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.herzborg/README.md b/bundles/org.openhab.binding.herzborg/README.md
new file mode 100644
index 00000000000..e3eff366d83
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/README.md
@@ -0,0 +1,83 @@
+# Herzborg Binding
+
+This binding supports smart curtain motors by Herzborg (http://www.herzborg.com/pro_list.aspx?TypeID=1)
+
+## Supported Things
+
+- `herzborg` A bridge thing that connects to a RS485 serial bus.
+- `curtain` A curtain motor thing that can be controlled via the `herzborg` bridge .
+
+The binding was developed and tested using DT300TV-1.2/14 type motor; others are expected to be compatible
+
+## Discovery
+
+Due to nature of serial bus being used, no automatic discovery is possible.
+
+## Thing Configuration
+
+### Serial Bus Bridge (id "serial_bus")
+
+| Parameter | Meaning |
+|-----------|---------------------------------------------------------|
+| port | Serial port name to use |
+
+Herzborg devices appear to use fixed 9600 8n1 communication parameters, so no other parameters are needed
+
+### Curtain Motor Thing (id "curtain")
+
+| Parameter | Meaning |
+|---------------|---------------------------------------------------------|
+| address | Address of the motor on the serial bus. |
+| poll_interval | Polling interval in seconds |
+
+## Channels
+
+| channel | type | description | Read-only |
+|------------|---------------|-----------------------------------------------|-----------|
+| position | RollerShutter | Controls position of the curtain. Position reported back is in percents; 0 - fully closed; 100 - fully open | N |
+| mode | String | Reports current motor mode: | Y |
+| | | 0 - Stop | |
+| | | 1 - Open | |
+| | | 2 - Close | |
+| | | 3 - Setting | |
+| reverse | Switch | Reverses direction when switched on | N |
+| handStart | Switch | Enable / disable hand start function | N |
+| extSwitch | String | External (low-voltage) switch mode: | N |
+| | | 1 - dual channel biased switch | |
+| | | 2 - dual channel rocker switch | |
+| | | 3 - DC246 electronic switch | |
+| | | 4 - single button cyclic switch | |
+| hvSwitch | String | Main (high-voltage) switch mode: | N |
+| | | 0 - dual channel biased switch | |
+| | | 1 - hotel mode(power on while card in) | |
+| | | 2 - dual channel rocker switch | |
+
+All the channels are read-write
+
+## Example
+
+herzborg.things:
+
+```
+Bridge herzborg:serial_bus:my_herzborg_bus [ port="/dev/ttyAMA1" ]
+{
+ Thing herzborg:curtain:livingroom [ address=1234, poll_interval=1 ]
+}
+```
+
+herzborg.items:
+
+```
+Rollershutter LivingRoom_Window {channel="herzborg:curtain:livingroom:position"}
+```
+
+herzborg.sitemap:
+
+```
+Frame label="Living room curtain"
+{
+ Switch item=LivingRoom_Window label="Control" mappings=["DOWN"="Close", "STOP"="Stop", "UP"="Open"]
+ Slider item=LivingRoom_Window label="Position [%d %%]" minValue=0 maxValue=100
+}
+
+```
diff --git a/bundles/org.openhab.binding.herzborg/pom.xml b/bundles/org.openhab.binding.herzborg/pom.xml
new file mode 100644
index 00000000000..cad9633e2b1
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.3.0-SNAPSHOT
+
+
+ org.openhab.binding.herzborg
+
+ openHAB Add-ons :: Bundles :: Herzborg Binding
+
+
diff --git a/bundles/org.openhab.binding.herzborg/src/main/feature/feature.xml b/bundles/org.openhab.binding.herzborg/src/main/feature/feature.xml
new file mode 100644
index 00000000000..985285bbe63
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab-transport-serial
+ mvn:org.openhab.addons.bundles/org.openhab.binding.herzborg/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/Bus.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/Bus.java
new file mode 100644
index 00000000000..b53e13c997b
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/Bus.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.herzborg.internal.dto.HerzborgProtocol.Function;
+import org.openhab.binding.herzborg.internal.dto.HerzborgProtocol.Packet;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link Bus} is a handy base class, implementing data communication with Herzborg devices.
+ *
+ * @author Pavel Fedin - Initial contribution
+ */
+@NonNullByDefault
+public class Bus {
+ private final Logger logger = LoggerFactory.getLogger(Bus.class);
+
+ protected @Nullable InputStream dataIn;
+ protected @Nullable OutputStream dataOut;
+
+ public static class Result {
+ ThingStatusDetail code;
+ @Nullable
+ String message;
+
+ Result(ThingStatusDetail code, String msg) {
+ this.code = code;
+ this.message = msg;
+ }
+
+ Result(ThingStatusDetail code) {
+ this.code = code;
+ }
+ }
+
+ public Bus() {
+ // Nothing to do here
+ }
+
+ private void safeClose(@Nullable Closeable stream) {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ logger.debug("Error closing I/O stream: {}", e.getMessage());
+ }
+ }
+ }
+
+ public void dispose() {
+ safeClose(dataOut);
+ safeClose(dataIn);
+
+ dataOut = null;
+ dataIn = null;
+ }
+
+ public synchronized @Nullable Packet doPacket(Packet pkt) throws IOException {
+ OutputStream dataOut = this.dataOut;
+ InputStream dataIn = this.dataIn;
+
+ if (dataOut == null || dataIn == null) {
+ return null;
+ }
+
+ int readLength = Packet.MIN_LENGTH;
+
+ switch (pkt.getFunction()) {
+ case Function.READ:
+ // The reply will include data itself
+ readLength += pkt.getDataLength();
+ break;
+ case Function.WRITE:
+ // The reply is number of bytes written
+ readLength += 1;
+ break;
+ case Function.CONTROL:
+ // The whole packet will be echoed back
+ readLength = pkt.getBuffer().length;
+ break;
+ default:
+ // We must not have anything else here
+ throw new IllegalStateException("Unknown function code");
+ }
+
+ dataOut.write(pkt.getBuffer());
+
+ int readOffset = 0;
+ byte[] replyBuffer = new byte[readLength];
+
+ while (readLength > 0) {
+ int n = dataIn.read(replyBuffer, readOffset, readLength);
+
+ if (n < 0) {
+ throw new IOException("EOF from serial port");
+ } else if (n == 0) {
+ throw new IOException("Serial read timeout");
+ }
+
+ readOffset += n;
+ readLength -= n;
+ }
+
+ return new Packet(replyBuffer);
+ }
+
+ public void flush() throws IOException {
+ InputStream dataIn = this.dataIn;
+
+ if (dataIn != null) {
+ // Unfortunately Java streams can't be flushed. Just read and drop all the characters
+ while (dataIn.available() > 0) {
+ dataIn.read();
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/BusHandler.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/BusHandler.java
new file mode 100644
index 00000000000..a2eef7d5afe
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/BusHandler.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.types.Command;
+
+/**
+ * The {@link BusHandler} is a handy base class, implementing data communication with Herzborg devices.
+ *
+ * @author Pavel Fedin - Initial contribution
+ */
+@NonNullByDefault
+public abstract class BusHandler extends BaseBridgeHandler {
+ protected Bus bus;
+
+ public BusHandler(Bridge bridge, Bus bus) {
+ super(bridge);
+ this.bus = bus;
+ }
+
+ public Bus getBus() {
+ return bus;
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ // Nothing to do here, but we have to implement it
+ }
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/CurtainConfiguration.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/CurtainConfiguration.java
new file mode 100644
index 00000000000..1394ed9a3cd
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/CurtainConfiguration.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link CurtainConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Pavel Fedin - Initial contribution
+ */
+@NonNullByDefault
+public class CurtainConfiguration {
+ public int address;
+ public int pollInterval;
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/CurtainHandler.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/CurtainHandler.java
new file mode 100644
index 00000000000..3f1f057218d
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/CurtainHandler.java
@@ -0,0 +1,226 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal;
+
+import static org.openhab.binding.herzborg.internal.HerzborgBindingConstants.*;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.xml.bind.DatatypeConverter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.herzborg.internal.dto.HerzborgProtocol.ControlAddress;
+import org.openhab.binding.herzborg.internal.dto.HerzborgProtocol.DataAddress;
+import org.openhab.binding.herzborg.internal.dto.HerzborgProtocol.Function;
+import org.openhab.binding.herzborg.internal.dto.HerzborgProtocol.Packet;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StopMoveType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.types.UpDownType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link CurtainHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Pavel Fedin - Initial contribution
+ */
+@NonNullByDefault
+public class CurtainHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(CurtainHandler.class);
+
+ private CurtainConfiguration config = new CurtainConfiguration();
+ private @Nullable ScheduledFuture> pollFuture;
+ private @Nullable Bus bus;
+
+ public CurtainHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ String ch = channelUID.getId();
+ Packet pkt = null;
+
+ switch (ch) {
+ case CHANNEL_POSITION:
+ if (command instanceof UpDownType) {
+ pkt = buildPacket(Function.CONTROL,
+ (command == UpDownType.UP) ? ControlAddress.OPEN : ControlAddress.CLOSE);
+ } else if (command instanceof StopMoveType) {
+ pkt = buildPacket(Function.CONTROL, ControlAddress.STOP);
+ } else if (command instanceof DecimalType) {
+ pkt = buildPacket(Function.CONTROL, ControlAddress.PERCENT, ((DecimalType) command).byteValue());
+ }
+ break;
+ case CHANNEL_REVERSE:
+ if (command instanceof OnOffType) {
+ pkt = buildPacket(Function.WRITE, DataAddress.DEFAULT_DIR, command.equals(OnOffType.ON) ? 1 : 0);
+ }
+ break;
+ case CHANNEL_HAND_START:
+ if (command instanceof OnOffType) {
+ pkt = buildPacket(Function.WRITE, DataAddress.HAND_START, command.equals(OnOffType.ON) ? 0 : 1);
+ }
+ break;
+ case CHANNEL_EXT_SWITCH:
+ if (command instanceof StringType) {
+ pkt = buildPacket(Function.WRITE, DataAddress.EXT_SWITCH, Byte.valueOf(command.toString()));
+ }
+ break;
+ case CHANNEL_HV_SWITCH:
+ if (command instanceof StringType) {
+ pkt = buildPacket(Function.WRITE, DataAddress.EXT_HV_SWITCH, Byte.valueOf(command.toString()));
+ }
+ break;
+ }
+
+ if (pkt != null) {
+ final Packet p = pkt;
+ scheduler.schedule(() -> {
+ Packet reply = doPacket(p);
+
+ if (reply != null) {
+ logger.trace("Function {} addr {} reply {}", p.getFunction(), p.getDataAddress(),
+ DatatypeConverter.printHexBinary(reply.getBuffer()));
+ }
+ }, 0, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private Packet buildPacket(byte function, byte data_addr) {
+ return new Packet((short) config.address, function, data_addr);
+ }
+
+ private Packet buildPacket(byte function, byte data_addr, byte value) {
+ return new Packet((short) config.address, function, data_addr, value);
+ }
+
+ private Packet buildPacket(byte function, byte data_addr, int value) {
+ return buildPacket(function, data_addr, (byte) value);
+ }
+
+ @Override
+ public void initialize() {
+ Bridge bridge = getBridge();
+
+ if (bridge == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, "Bridge not present");
+ return;
+ }
+
+ BridgeHandler handler = bridge.getHandler();
+
+ if (handler == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, "Bridge has no handler");
+ return;
+ }
+
+ bus = ((BusHandler) handler).getBus();
+ config = getConfigAs(CurtainConfiguration.class);
+
+ updateStatus(ThingStatus.UNKNOWN);
+ logger.trace("Successfully initialized, starting poll");
+ pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 1, config.pollInterval, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void dispose() {
+ stopPoll();
+ }
+
+ private void stopPoll() {
+ ScheduledFuture> poll = pollFuture;
+ pollFuture = null;
+
+ if (poll != null) {
+ poll.cancel(true);
+ }
+ }
+
+ private @Nullable synchronized Packet doPacket(Packet pkt) {
+ Bus bus = this.bus;
+
+ if (bus == null) {
+ // This is an impossible situation but Eclipse forces us to handle it
+ logger.warn("No Bridge sending commands");
+ return null;
+ }
+
+ try {
+ Packet reply = bus.doPacket(pkt);
+
+ if (reply == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+ return null;
+ }
+
+ if (reply.isValid()) {
+ updateStatus(ThingStatus.ONLINE);
+ return reply;
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Invalid response received: " + DatatypeConverter.printHexBinary(reply.getBuffer()));
+ bus.flush();
+ }
+
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+ }
+
+ return null;
+ }
+
+ private void poll() {
+ Packet reply = doPacket(buildPacket(Function.READ, DataAddress.POSITION, 4));
+
+ if (reply != null) {
+ byte position = reply.getData(0);
+ byte reverse = reply.getData(1);
+ byte handStart = reply.getData(2);
+ byte mode = reply.getData(3);
+
+ // If calibration has been lost, position is reported as -1.
+ updateState(CHANNEL_POSITION,
+ (position > 100 || position < 0) ? UnDefType.UNDEF : new PercentType(position));
+ updateState(CHANNEL_REVERSE, reverse != 0 ? OnOffType.ON : OnOffType.OFF);
+ updateState(CHANNEL_HAND_START, handStart == 0 ? OnOffType.ON : OnOffType.OFF);
+ updateState(CHANNEL_MODE, new StringType(String.valueOf(mode)));
+ }
+
+ Packet extReply = doPacket(buildPacket(Function.READ, DataAddress.EXT_SWITCH, 2));
+
+ if (extReply != null) {
+ byte extSwitch = extReply.getData(0);
+ byte hvSwitch = extReply.getData(1);
+
+ updateState(CHANNEL_EXT_SWITCH, new StringType(String.valueOf(extSwitch)));
+ updateState(CHANNEL_HV_SWITCH, new StringType(String.valueOf(hvSwitch)));
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/HerzborgBindingConstants.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/HerzborgBindingConstants.java
new file mode 100644
index 00000000000..71663047c69
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/HerzborgBindingConstants.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link HerzborgBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Pavel Fedin - Initial contribution
+ */
+@NonNullByDefault
+public class HerzborgBindingConstants {
+
+ private static final String BINDING_ID = "herzborg";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_SERIAL_BUS = new ThingTypeUID(BINDING_ID, "serialBus");
+ public static final ThingTypeUID THING_TYPE_CURTAIN = new ThingTypeUID(BINDING_ID, "curtain");
+
+ // List of all Channel ids
+ public static final String CHANNEL_POSITION = "position";
+ public static final String CHANNEL_MODE = "mode";
+ public static final String CHANNEL_REVERSE = "reverse";
+ public static final String CHANNEL_HAND_START = "handStart";
+ public static final String CHANNEL_EXT_SWITCH = "extSwitch";
+ public static final String CHANNEL_HV_SWITCH = "hvSwitch";
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/HerzborgHandlerFactory.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/HerzborgHandlerFactory.java
new file mode 100644
index 00000000000..99b122d23be
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/HerzborgHandlerFactory.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal;
+
+import static org.openhab.binding.herzborg.internal.HerzborgBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link HerzborgHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Pavel Fedin - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.herzborg", service = ThingHandlerFactory.class)
+public class HerzborgHandlerFactory extends BaseThingHandlerFactory {
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SERIAL_BUS,
+ THING_TYPE_CURTAIN);
+
+ private final SerialPortManager serialPortManager;
+
+ @Activate
+ public HerzborgHandlerFactory(final @Reference SerialPortManager serialPortManager) {
+ this.serialPortManager = serialPortManager;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_CURTAIN.equals(thingTypeUID)) {
+ return new CurtainHandler(thing);
+ } else if (THING_TYPE_SERIAL_BUS.equals(thingTypeUID)) {
+ return new SerialBusHandler((Bridge) thing, serialPortManager);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBus.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBus.java
new file mode 100644
index 00000000000..814542837d0
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBus.java
@@ -0,0 +1,120 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.io.transport.serial.PortInUseException;
+import org.openhab.core.io.transport.serial.SerialPort;
+import org.openhab.core.io.transport.serial.SerialPortIdentifier;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
+import org.openhab.core.thing.ThingStatusDetail;
+
+/**
+ * The {@link SerialBus} implements specific handling for Herzborg serial bus,
+ * connected directly via a serial port.
+ *
+ * @author Pavel Fedin - Initial contribution
+ */
+@NonNullByDefault
+public class SerialBus extends Bus {
+ private SerialPortManager serialPortManager;
+ private @Nullable SerialPort serialPort;
+
+ public SerialBus(SerialPortManager manager) {
+ serialPortManager = manager;
+ }
+
+ public Result initialize(@Nullable String port) {
+ if (port == null) {
+ return new Result(ThingStatusDetail.CONFIGURATION_ERROR, "Port is not specified");
+ }
+ SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(port);
+ if (portIdentifier == null) {
+ return new Result(ThingStatusDetail.CONFIGURATION_ERROR, "No such port: " + port);
+ }
+
+ SerialPort commPort;
+ try {
+ commPort = portIdentifier.open(this.getClass().getName(), 2000);
+ } catch (PortInUseException e1) {
+ return new Result(ThingStatusDetail.CONFIGURATION_ERROR, "Port " + port + " is in use");
+ }
+
+ try {
+ // Herzborg serial bus operates with fixed parameters
+ commPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
+ commPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
+ } catch (UnsupportedCommOperationException e) {
+ return new Result(ThingStatusDetail.CONFIGURATION_ERROR, "Invalid port configuration");
+ }
+
+ try {
+ commPort.enableReceiveThreshold(8);
+ commPort.enableReceiveTimeout(1000);
+ } catch (UnsupportedCommOperationException e) {
+ // OpenHAB's serial-over-IP doesn't support these, so let's ignore the exception
+ }
+
+ InputStream dataIn = null;
+ OutputStream dataOut = null;
+ String error = null;
+
+ try {
+ dataIn = commPort.getInputStream();
+ dataOut = commPort.getOutputStream();
+
+ if (dataIn == null) {
+ error = "No input stream available on the serial port";
+ } else if (dataOut == null) {
+ error = "No output stream available on the serial port";
+ } else {
+ dataOut.flush();
+ if (dataIn.markSupported()) {
+ dataIn.reset();
+ }
+ }
+ } catch (IOException e) {
+ error = e.getMessage();
+ }
+
+ if (error != null) {
+ return new Result(ThingStatusDetail.HANDLER_INITIALIZING_ERROR, error);
+ }
+
+ this.serialPort = commPort;
+ this.dataIn = dataIn;
+ this.dataOut = dataOut;
+
+ return new Result(ThingStatusDetail.NONE);
+ }
+
+ @Override
+ public void dispose() {
+ SerialPort port = serialPort;
+
+ if (port == null) {
+ return; // Nothing to do in this case
+ }
+
+ port.removeEventListener();
+ super.dispose();
+ port.close();
+ serialPort = null;
+ }
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBusConfiguration.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBusConfiguration.java
new file mode 100644
index 00000000000..635c6e35b82
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBusConfiguration.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link SerialBusConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Pavel Fedin - Initial contribution
+ */
+@NonNullByDefault
+public class SerialBusConfiguration {
+ public @Nullable String port;
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBusHandler.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBusHandler.java
new file mode 100644
index 00000000000..93537cda9a5
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBusHandler.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.io.transport.serial.SerialPortManager;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+
+/**
+ * The {@link SerialBusHandler} implements specific handling for Herzborg serial bus,
+ * connected directly via a serial port.
+ *
+ * @author Pavel Fedin - Initial contribution
+ */
+@NonNullByDefault
+public class SerialBusHandler extends BusHandler {
+ private SerialBusConfiguration config = new SerialBusConfiguration();
+
+ public SerialBusHandler(Bridge bridge, SerialPortManager portManager) {
+ super(bridge, new SerialBus(portManager));
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(SerialBusConfiguration.class);
+
+ Bus.Result result = ((SerialBus) bus).initialize(config.port);
+
+ if (result.code == ThingStatusDetail.NONE) {
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, result.code, result.message);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ bus.dispose();
+ }
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/dto/HerzborgProtocol.java b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/dto/HerzborgProtocol.java
new file mode 100644
index 00000000000..3e9757b25d5
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/dto/HerzborgProtocol.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright (c) 2010-2022 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.herzborg.internal.dto;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Herzborg binary protocol
+ *
+ * @author Pavel Fedin - Initial contribution
+ *
+ */
+public class HerzborgProtocol {
+ public static class Function {
+ public static final byte READ = 0x01;
+ public static final byte WRITE = 0x02;
+ public static final byte CONTROL = 0x03;
+ public static final byte REQUEST = 0x04;
+ }
+
+ public static class ControlAddress {
+ public static final byte OPEN = 0x01;
+ public static final byte CLOSE = 0x02;
+ public static final byte STOP = 0x03;
+ public static final byte PERCENT = 0x04;
+ public static final byte DELETE_LIMIT = 0x07;
+ public static final byte DEFAULT = 0x08;
+ public static final byte SET_CONTEXT = 0x09;
+ public static final byte RUN_CONTEXT = 0x0A;
+ public static final byte DEL_CONTEXT = 0x0B;
+ }
+
+ public static class DataAddress {
+ public static final byte ID_L = 0x00;
+ public static final byte ID_H = 0x01;
+ public static final byte POSITION = 0x02;
+ public static final byte DEFAULT_DIR = 0x03;
+ public static final byte HAND_START = 0x04;
+ public static final byte MODE = 0x05;
+ public static final byte EXT_SWITCH = 0x27;
+ public static final byte EXT_HV_SWITCH = 0x28;
+ }
+
+ public static class Packet {
+ private static final int HEADER_LENGTH = 5;
+ private static final int CRC16_LENGTH = 2;
+ public static final int MIN_LENGTH = HEADER_LENGTH + CRC16_LENGTH;
+
+ private static final byte START = 0x55;
+
+ private ByteBuffer dataBuffer;
+ private int dataLength; // Packet length without CRC16
+
+ public Packet(byte[] data) {
+ dataBuffer = ByteBuffer.wrap(data);
+ dataBuffer.order(ByteOrder.LITTLE_ENDIAN);
+ dataLength = data.length - CRC16_LENGTH;
+ }
+
+ private void setHeader(short device_addr, byte function, byte data_addr, int data_length) {
+ dataLength = HEADER_LENGTH + data_length;
+
+ dataBuffer = ByteBuffer.allocate(dataLength + CRC16_LENGTH);
+ dataBuffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ dataBuffer.put(START);
+ dataBuffer.putShort(device_addr);
+ dataBuffer.put(function);
+ dataBuffer.put(data_addr);
+ }
+
+ private void setCrc16() {
+ dataBuffer.putShort(crc16(dataLength));
+ }
+
+ public Packet(short device_addr, byte function, byte data_addr) {
+ setHeader(device_addr, function, data_addr, 0);
+ setCrc16();
+ }
+
+ public Packet(short device_addr, byte function, byte data_addr, byte value) {
+ int dataLength = (function == Function.WRITE) ? 2 : 1;
+
+ setHeader(device_addr, function, data_addr, dataLength);
+ if (function == Function.WRITE) {
+ // WRITE command also requires length of data to be written
+ dataBuffer.put((byte) 1);
+ }
+ dataBuffer.put(value);
+ setCrc16();
+ }
+
+ public byte[] getBuffer() {
+ return dataBuffer.array();
+ }
+
+ public boolean isValid() {
+ return dataBuffer.get(0) == START && crc16(dataLength) == dataBuffer.getShort(dataLength);
+ }
+
+ public byte getFunction() {
+ return dataBuffer.get(3);
+ }
+
+ public byte getDataAddress() {
+ return dataBuffer.get(4);
+ }
+
+ public byte getDataLength() {
+ return dataBuffer.get(HEADER_LENGTH);
+ }
+
+ public byte getData(int offset) {
+ return dataBuffer.get(HEADER_LENGTH + offset);
+ }
+
+ // Herzborg uses modbus variant of CRC16
+ // Code adapted from https://habr.com/ru/post/418209/
+ private short crc16(int length) {
+ int crc = 0xFFFF;
+ for (int i = 0; i < length; i++) {
+ crc = crc ^ Byte.toUnsignedInt(dataBuffer.get(i));
+ for (int j = 0; j < 8; j++) {
+ int mask = ((crc & 0x1) != 0) ? 0xA001 : 0x0000;
+ crc = ((crc >> 1) & 0x7FFF) ^ mask;
+ }
+ }
+ return (short) crc;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.herzborg/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.herzborg/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 00000000000..52bf662abc9
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ Herzborg Binding
+ This is the binding for Herzborg smart curtain motors.
+
+
diff --git a/bundles/org.openhab.binding.herzborg/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.herzborg/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 00000000000..994ea745289
--- /dev/null
+++ b/bundles/org.openhab.binding.herzborg/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,102 @@
+
+
+
+
+ RS485 bus
+
+
+
+ serial-port
+ Serial port to use, for example /dev/ttyS0 or COM1
+ /dev/ttyS0
+
+
+
+
+
+
+
+
+
+ Curtain motor
+
+
+
+
+
+
+
+
+
+
+
+
+ Device address on the bus.
+ 65278
+
+
+
+ Poll interval in seconds
+ 1
+
+
+
+
+
+ Rollershutter
+
+ Curtain position control
+ Blinds
+
+
+ String
+
+ Motor mode
+
+
+
+
+
+
+
+
+
+
+ Switch
+
+ Reverse default motor direction
+
+
+ Switch
+
+ Enable or disable start by hand
+
+
+ String
+
+ External switch type
+
+
+
+
+
+
+
+
+
+
+ String
+
+ High-voltage switch type (only for EV motor)
+
+
+
+
+
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 5ef44f682d4..97ce006f0e7 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -152,6 +152,7 @@
org.openhab.binding.helios
org.openhab.binding.heliosventilation
org.openhab.binding.heos
+ org.openhab.binding.herzborg
org.openhab.binding.homeconnect
org.openhab.binding.homematic
org.openhab.binding.homewizard