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