[herzborg] Herzborg binding (#9327)

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>
This commit is contained in:
Sonic-Amiga 2022-02-05 21:08:05 +03:00 committed by GitHub
parent 8ebd4e9047
commit 7a407523dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1120 additions and 0 deletions

View File

@ -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

View File

@ -591,6 +591,11 @@
<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>

View File

@ -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

View File

@ -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 modepower 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
}
```

View File

@ -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>

View File

@ -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>

View File

@ -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();
}
}
}
}

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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)));
}
}
}

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -152,6 +152,7 @@
<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>