]> git.basschouten.com Git - openhab-addons.git/commitdiff
[herzborg] Herzborg binding (#9327)
authorSonic-Amiga <48695031+Sonic-Amiga@users.noreply.github.com>
Sat, 5 Feb 2022 18:08:05 +0000 (21:08 +0300)
committerGitHub <noreply@github.com>
Sat, 5 Feb 2022 18:08:05 +0000 (19:08 +0100)
Supports Herzborg curtain motors over RS-485 network

Signed-off-by: Pavel Fedin <pavel_fedin@mail.ru>
Co-authored-by: Matthew Skinner <matt@pcmus.com>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Matthew Skinner <matt@pcmus.com>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
19 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.herzborg/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/README.md [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/Bus.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/BusHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/CurtainConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/CurtainHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/HerzborgBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/HerzborgHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBus.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBusConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/SerialBusHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/java/org/openhab/binding/herzborg/internal/dto/HerzborgProtocol.java [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.herzborg/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index e15f903cd8b032dea2c367be85bc620790f4f7ef..c4a70a3c8009915244425659ac24dc0c0f1e0a87 100644 (file)
 /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
index 639dcf9c3c6e7047240b169cc64a7b482244a48c..7c2e899f57cbd182926bc3eebc69e7b8bc0c8f18 100644 (file)
       <artifactId>org.openhab.binding.heos</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.herzborg</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.homeconnect</artifactId>
diff --git a/bundles/org.openhab.binding.herzborg/NOTICE b/bundles/org.openhab.binding.herzborg/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..e3eff36
--- /dev/null
@@ -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 (file)
index 0000000..cad9633
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.3.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.herzborg</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Herzborg Binding</name>
+
+</project>
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 (file)
index 0000000..985285b
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.herzborg-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-herzborg" description="Herzborg Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <feature>openhab-transport-serial</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.herzborg/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..b53e13c
--- /dev/null
@@ -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 (file)
index 0000000..a2eef7d
--- /dev/null
@@ -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 (file)
index 0000000..1394ed9
--- /dev/null
@@ -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 (file)
index 0000000..3f1f057
--- /dev/null
@@ -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 (file)
index 0000000..7166304
--- /dev/null
@@ -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 (file)
index 0000000..99b122d
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..8145428
--- /dev/null
@@ -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 (file)
index 0000000..635c6e3
--- /dev/null
@@ -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 (file)
index 0000000..93537cd
--- /dev/null
@@ -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 (file)
index 0000000..3e9757b
--- /dev/null
@@ -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 (file)
index 0000000..52bf662
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="herzborg" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Herzborg Binding</name>
+       <description>This is the binding for Herzborg smart curtain motors.</description>
+
+</binding:binding>
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 (file)
index 0000000..994ea74
--- /dev/null
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="herzborg"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <bridge-type id="serialBus">
+               <label>Herzborg Serial Bus</label>
+               <description>RS485 bus</description>
+               <config-description>
+                       <parameter name="port" type="text" required="true">
+                               <label>Serial Port</label>
+                               <context>serial-port</context>
+                               <description>Serial port to use, for example /dev/ttyS0 or COM1</description>
+                               <default>/dev/ttyS0</default>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+
+       <thing-type id="curtain">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="serialBus"/>
+               </supported-bridge-type-refs>
+               <label>Herzborg Curtain Motor</label>
+               <description>Curtain motor</description>
+               <channels>
+                       <channel id="position" typeId="position"/>
+                       <channel id="mode" typeId="mode"/>
+                       <channel id="reverse" typeId="reverse"/>
+                       <channel id="handStart" typeId="handStart"/>
+                       <channel id="extSwitch" typeId="extSwitch"/>
+                       <channel id="hwSwitch" typeId="hwSwitch"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="address" type="integer">
+                               <label>Address</label>
+                               <description>Device address on the bus.</description>
+                               <default>65278</default>
+                       </parameter>
+                       <parameter name="pollInterval" type="integer" unit="s">
+                               <label>Poll Interval</label>
+                               <description>Poll interval in seconds</description>
+                               <default>1</default>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-type id="position">
+               <item-type>Rollershutter</item-type>
+               <label>Position</label>
+               <description>Curtain position control</description>
+               <category>Blinds</category>
+       </channel-type>
+       <channel-type id="mode">
+               <item-type>String</item-type>
+               <label>Mode</label>
+               <description>Motor mode</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="0">Stop</option>
+                               <option value="1">Open</option>
+                               <option value="2">Close</option>
+                               <option value="3">Setting</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="reverse" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Reverse Motor</label>
+               <description>Reverse default motor direction</description>
+       </channel-type>
+       <channel-type id="handStart" advanced="true">
+               <item-type>Switch</item-type>
+               <label>Start By Hand</label>
+               <description>Enable or disable start by hand</description>
+       </channel-type>
+       <channel-type id="extSwitch" advanced="true">
+               <item-type>String</item-type>
+               <label>External Switch</label>
+               <description>External switch type</description>
+               <state>
+                       <options>
+                               <option value="1">2-channel biased</option>
+                               <option value="2">2-channel rocker</option>
+                               <option value="3">DC246</option>
+                               <option value="4">single button</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="hwSwitch" advanced="true">
+               <item-type>String</item-type>
+               <label>HV Switch</label>
+               <description>High-voltage switch type (only for EV motor)</description>
+               <state>
+                       <options>
+                               <option value="0">2-channel biased</option>
+                               <option value="1">hotel mode</option>
+                               <option value="2">2-channel rocker</option>
+                       </options>
+               </state>
+       </channel-type>
+</thing:thing-descriptions>
index 5ef44f682d4931bc1b1cc9989c57e1865872045e..97ce006f0e7079dcfc58bf582c1ac80d92306ea1 100644 (file)
     <module>org.openhab.binding.helios</module>
     <module>org.openhab.binding.heliosventilation</module>
     <module>org.openhab.binding.heos</module>
+    <module>org.openhab.binding.herzborg</module>
     <module>org.openhab.binding.homeconnect</module>
     <module>org.openhab.binding.homematic</module>
     <module>org.openhab.binding.homewizard</module>