added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.upb</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

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,108 @@
# UPB Binding
Universal Powerline Bus (UPB) is a protocol for communication over household electrical wiring.
## Supported Things
The binding has not yet been tested with a variety of devices, so specific device support is limited.
Instead the binding provides some generic devices, and allows adding channels that match the type of device.
* `serial-pim` - Serial PIM
* `generic` - Generic UPB device
* `virtual` - "Virtual" device that allows scene selection
Specific devices that are supported:
* `leviton-38a00-1` - 6-button scene switch
## Binding Configuration
The following binding configuration parameters are supported:
| Parameter | Description | Config | Default |
| ------------------------ | ---------------------------------------------- |--------- | ------- |
| networkId | Default UPB network ID (0-255) | Optional | - |
## Thing Configuration
### Serial PIM
You need a Powerline Interface Module (PIM) for the binding to work.
This is a piece of equipment that connects a computer to the powerline.
There are a few different PIM interfaces but this binding only supports serial PIMs.
The `serial-pim` takes the following configuration parameters:
| Parameter | Description | Config | Default |
| ------------------------ | ---------------------------------------------- |--------- | ------- |
| port | Serial port where the PIM is connected | Required | - |
### Generic device
The `generic` thing type supports most UPB devices such as dimmers, light switches,
and appliance modules. It has the following configuration parameters:
| Parameter | Description | Config | Default |
| ------------------------ | ---------------------------------------------- |--------- | -------------- |
| networkId | ID of the UPB network (0-255) | Optional | binding config |
| unitId | Unit ID (unique address) of the device (1-250) | Required | - |
### Virtual device
The `virtual` pseudo-device does not correspond to any real device on the UPB network.
It is nevertheless useful for reading and setting the current scene.
The device has two channels, `linkActivated` and `linkDeactivated`.
If a device on the UPB network activates or de-activates a scene by broadcasting a link
activation command, the link ID (or scene number) can be read from
the corresponding channel.
Similarly, updating the channel with a link ID will send out the
corresponding link command on the UPB network.
## Channels
These channels are available for generic devices:
| Channel Type | Item type | Description |
| --------------- | --------- | ----------------------------------------------- |
| dimmer | Dimmer | Level/brightness, or on/off for switches |
| scene-selection | - | Trigger channel for scene selection |
The virtual device supports the `link` channel type:
| Channel Type | Item type | Description |
| ------------ | --------- | -------------------------------------- |
| link | Number | A scene to activate or deactivate |
## Full Example
Here is a sample `.things configuration file:
```
Bridge upb:serial-pim:pim "UPB PIM" @ "Basement" [port="/dev/ttyUSB0"] {
Thing generic light-switch "Living Room Light" [networkId=1, unitId=1]
Thing virtual upb-scene "UPB Scene Control" [networkId=1, unitId=250]
Thing leviton-38a00-1 scene-switch "Scene Switch" @ "Bedroom" [networkId=1, unitId=2] {
Channels:
Type scene-selection : btnOn [linkId=1]
Type scene-selection : btnOff [linkId=2]
Type scene-selection : btnA [linkId=3]
Type scene-selection : btnB [linkId=4]
Type scene-selection : btnC [linkId=5]
Type scene-selection : btnD [linkId=6]
}
}
```
And the items:
```
// Configure as either Switch or Dimmer
Dimmer LivingRoomLight "UPB Light Switch" {channel="upb:generic:pim:light-switch:dimmer"}
// A scene selector (does not correspond to a physical device)
Number UPB_Active_Scene "Active UPB Scene" {channel="upb:virtual:pim:upb-scene:linkActivated"}
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.upb</artifactId>
<name>openHAB Add-ons :: Bundles :: UPB Binding</name>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.upb-${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-upb" description="UPB 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.upb/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* Common constants used in the binding.
*
* @author Marcus Better - Initial contribution
*/
@NonNullByDefault
public final class Constants {
public static final String BINDING_ID = "upb";
public static final ThingTypeUID PIM_UID = new ThingTypeUID(BINDING_ID, "serial-pim");
public static final ThingTypeUID GENERIC_DEVICE_UID = new ThingTypeUID(BINDING_ID, "generic");
public static final ThingTypeUID VIRTUAL_DEVICE_UID = new ThingTypeUID(BINDING_ID, "virtual");
public static final ThingTypeUID LEVITON_38A00_DEVICE_UID = new ThingTypeUID(BINDING_ID, "leviton-38a00-1");
public static final String SCENE_CHANNEL_TYPE_ID = "scene-selection";
public static final String LINK_CHANNEL_TYPE_ID = "link";
public static final String DIMMER_TYPE_ID = "dimmer";
public static final ChannelTypeUID SCENE_CHANNEL_TYPE_UID = new ChannelTypeUID(BINDING_ID, SCENE_CHANNEL_TYPE_ID);
public static final ChannelTypeUID LINK_CHANNEL_TYPE_UID = new ChannelTypeUID(BINDING_ID, LINK_CHANNEL_TYPE_ID);
public static final String LINK_ACTIVATE_CHANNEL_ID = "linkActivated";
public static final String LINK_DEACTIVATE_CHANNEL_ID = "linkDeactivated";
public static final String CONFIGURATION_PORT = "port";
public static final String CONFIGURATION_UNIT_ID = "unitId";
public static final String CONFIGURATION_NETWORK_ID = "networkId";
public static final String CONFIGURATION_LINK_ID = "linkId";
public static final String OFFLINE_CTLR_OFFLINE = "@text/upb.thingstate.controller_offline";
public static final String OFFLINE_COMM_ERROR = "@text/upb.thingstate.controller_comm_error";
public static final String OFFLINE_NODE_DEAD = "@text/upb.thingstate.node_dead";
public static final String OFFLINE_NODE_NOTFOUND = "@text/upb.thingstate.node_notfound";
public static final String OFFLINE_SERIAL_EXISTS = "@text/upb.thingstate.serial_notfound";
public static final String OFFLINE_SERIAL_INUSE = "@text/upb.thingstate.serial_inuse";
public static final String OFFLINE_SERIAL_UNSUPPORTED = "@text/upb.thingstate.serial_unsupported";
public static final String OFFLINE_SERIAL_LISTENERS = "@text/upb.thingstate.serial_listeners";
public static final String OFFLINE_SERIAL_PORT_NOT_SET = "@text/upb.thingstate.serial_cfg_port";
private Constants() {
// static class
}
}

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal;
import static org.eclipse.jdt.annotation.DefaultLocation.*;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.upb.internal.UPBDevice.DeviceState;
import org.openhab.binding.upb.internal.handler.UPBThingHandler;
import org.openhab.binding.upb.internal.handler.VirtualThingHandler;
import org.openhab.binding.upb.internal.message.Command;
import org.openhab.binding.upb.internal.message.UPBMessage;
import org.openhab.binding.upb.internal.message.UPBMessage.Type;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Controller logic for UPB network communications.
*
* @author Marcus Better - Initial contribution
*
*/
@NonNullByDefault({ PARAMETER, RETURN_TYPE, FIELD })
public class UPBController {
private final Logger logger = LoggerFactory.getLogger(UPBController.class);
// Maps of devices and things keyed by (networkId, unitId)
private final ConcurrentHashMap<Integer, UPBDevice> devices = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, UPBThingHandler> things = new ConcurrentHashMap<>();
public void incomingMessage(final UPBMessage msg) {
if (msg.getType() != Type.MESSAGE_REPORT) {
return;
}
final byte networkId = msg.getNetwork();
final byte srcId = msg.getSource();
final byte dstId = msg.getDestination();
final Command cmd = msg.getCommand();
logger.debug("received message, network={} src={} dst={} cmd={}", networkId & 0xff, srcId & 0xff, dstId & 0xff,
cmd);
if (!isValidId(srcId)) {
return;
}
final int srcAddr = mkAddr(networkId, srcId);
final UPBDevice src = devices.getOrDefault(srcAddr, new UPBDevice(networkId, srcId));
src.setState(DeviceState.ALIVE);
final UPBThingHandler thingHnd = things.get(srcAddr);
if (thingHnd == null) {
logger.debug("unknown source device {}", srcId & 0xff);
return;
}
if (msg.getControlWord().isLink() || srcId == dstId) {
thingHnd.onMessageReceived(msg);
}
// link messages are additionally sent to any virtual devices
if (msg.getControlWord().isLink()) {
things.values().stream().filter(hnd -> hnd instanceof VirtualThingHandler)
.forEach(hnd -> hnd.onMessageReceived(msg));
}
}
private static boolean isValidId(final byte id) {
return id != 0 && id != -1;
}
public @Nullable UPBDevice getDevice(final byte networkId, final byte unitId) {
return devices.get(mkAddr(networkId, unitId));
}
public void deviceAdded(final ThingHandler childHandler, final Thing childThing) {
if (childHandler instanceof UPBThingHandler) {
final UPBThingHandler hnd = (UPBThingHandler) childHandler;
things.put(mkAddr(hnd.getNetworkId(), hnd.getUnitId()), hnd);
}
}
public void deviceRemoved(final ThingHandler childHandler, final Thing childThing) {
if (childHandler instanceof UPBThingHandler) {
final UPBThingHandler hnd = (UPBThingHandler) childHandler;
things.remove(mkAddr(hnd.getNetworkId(), hnd.getUnitId()), hnd);
}
}
// forms a device lookup key from a network and unit ID
private static int mkAddr(final byte networkId, final byte srcId) {
return (networkId & 0xff) << 8 | (srcId & 0xff);
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A device on the UPB network.
*
* @author Marcus Better - Initial contribution
*
*/
@NonNullByDefault
public class UPBDevice {
private final byte networkId;
private final byte unitId;
private DeviceState state = DeviceState.INITIALIZING;
public enum DeviceState {
INITIALIZING,
ALIVE,
DEAD,
FAILED
}
public UPBDevice(final byte networkId, final byte unitId) {
this.networkId = networkId;
this.unitId = unitId;
}
public byte getNetworkId() {
return networkId;
}
public byte getUnitId() {
return unitId;
}
public DeviceState getState() {
return state;
}
public void setState(final DeviceState state) {
this.state = state;
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal;
import java.math.BigDecimal;
import java.util.Dictionary;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.upb.internal.handler.SerialPIMHandler;
import org.openhab.binding.upb.internal.handler.UPBThingHandler;
import org.openhab.binding.upb.internal.handler.VirtualThingHandler;
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.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Factory for UPB handlers.
*
* @author Marcus Better - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.upb")
@NonNullByDefault
public class UPBHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(UPBHandlerFactory.class);
private final SerialPortManager serialPortManager;
private @Nullable Byte networkId;
@Activate
public UPBHandlerFactory(@Reference SerialPortManager serialPortManager) {
this.serialPortManager = serialPortManager;
}
@Override
@NonNullByDefault({})
protected void activate(final ComponentContext componentContext) {
super.activate(componentContext);
final Dictionary<String, Object> config = componentContext.getProperties();
final BigDecimal nid = (BigDecimal) config.get(Constants.CONFIGURATION_NETWORK_ID);
if (nid != null) {
if (nid.compareTo(BigDecimal.ZERO) < 0 || nid.compareTo(BigDecimal.valueOf(255)) > 0) {
logger.warn("invalid network ID {}", nid);
throw new IllegalArgumentException("network ID out of range");
}
networkId = nid.byteValue();
}
}
@Override
public boolean supportsThingType(final ThingTypeUID thingTypeUID) {
return Constants.BINDING_ID.equals(thingTypeUID.getBindingId());
}
@Override
protected @Nullable ThingHandler createHandler(final Thing thing) {
logger.debug("Creating thing {}", thing.getUID());
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(Constants.PIM_UID)) {
assert serialPortManager != null;
return new SerialPIMHandler((Bridge) thing, serialPortManager);
} else if (thingTypeUID.equals(Constants.VIRTUAL_DEVICE_UID)) {
return new VirtualThingHandler(thing, networkId);
} else if (thingTypeUID.equals(Constants.GENERIC_DEVICE_UID)
|| thingTypeUID.equals(Constants.LEVITON_38A00_DEVICE_UID)) {
// generic UPB thing handler
return new UPBThingHandler(thing, networkId);
}
return null;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.upb.internal.message.UPBMessage;
/**
* Callback interface for received UPB messages.
*
* @author Marcus Better - Initial contribution
*
*/
@NonNullByDefault
public interface MessageListener {
void incomingMessage(UPBMessage msg);
void onError(Throwable t);
}

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.upb.internal.Constants;
import org.openhab.binding.upb.internal.UPBController;
import org.openhab.binding.upb.internal.UPBDevice;
import org.openhab.binding.upb.internal.message.UPBMessage;
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.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for Powerline Interface Module handlers.
*
* @author Marcus Better - Initial contribution
*
*/
@NonNullByDefault
public abstract class PIMHandler extends BaseBridgeHandler implements MessageListener, UPBIoHandler {
private final Logger logger = LoggerFactory.getLogger(PIMHandler.class);
// volatile to ensure visibility for callbacks from the serial I/O thread
private volatile UPBController controller = new UPBController();
public PIMHandler(final Bridge bridge) {
super(bridge);
}
@Override
public void initialize() {
logger.debug("Initializing UPB PIM {}.", getThing().getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, Constants.OFFLINE_CTLR_OFFLINE);
controller = new UPBController();
}
@Override
public void dispose() {
logger.debug("UPB binding shutting down...");
super.dispose();
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command command) {
}
@Override
public void childHandlerInitialized(final ThingHandler childHandler, final Thing childThing) {
logger.debug("child handler initialized: {}", childThing.getUID());
controller.deviceAdded(childHandler, childThing);
super.childHandlerInitialized(childHandler, childThing);
}
@Override
public void childHandlerDisposed(final ThingHandler childHandler, final Thing childThing) {
logger.debug("child handler disposed: {}", childThing.getUID());
controller.deviceRemoved(childHandler, childThing);
super.childHandlerDisposed(childHandler, childThing);
}
@Override
public void incomingMessage(final UPBMessage msg) {
updateStatus(ThingStatus.ONLINE);
controller.incomingMessage(msg);
}
@Override
public void onError(final Throwable t) {
// Currently all PIM errors are unrecoverable, either a bug or
// the serial thread had an I/O error.
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, Constants.OFFLINE_COMM_ERROR);
}
public @Nullable UPBDevice getDevice(byte networkId, byte unitId) {
return controller.getDevice(networkId, unitId);
}
}

View File

@@ -0,0 +1,291 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.handler;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.upb.internal.handler.UPBIoHandler.CmdStatus;
import org.openhab.binding.upb.internal.message.MessageBuilder;
import org.openhab.binding.upb.internal.message.MessageParseException;
import org.openhab.binding.upb.internal.message.UPBMessage;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Event loop for serial communications. Handles sending and receiving UPB messages.
*
* @author Marcus Better - Initial contribution
*
*/
@NonNullByDefault
public class SerialIoThread extends Thread {
private static final int WRITE_QUEUE_LENGTH = 128;
private static final int ACK_TIMEOUT_MS = 500;
private static final byte[] ENABLE_MESSAGE_MODE_CMD = "\u001770028E\n".getBytes(StandardCharsets.US_ASCII);
private static final int MAX_READ_SIZE = 128;
private static final int CR = 13;
private final Logger logger = LoggerFactory.getLogger(SerialIoThread.class);
private final MessageListener listener;
// Single-threaded executor for writes that serves to serialize writes.
private final ExecutorService writeExecutor = new ThreadPoolExecutor(1, 1, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(WRITE_QUEUE_LENGTH), new NamedThreadFactory("upb-serial-writer", true));
private final SerialPort serialPort;
private volatile @Nullable WriteRunnable currentWrite;
private volatile boolean done;
public SerialIoThread(final SerialPort serialPort, final MessageListener listener, final ThingUID thingUID) {
this.serialPort = serialPort;
this.listener = listener;
setName("OH-binding-" + thingUID + "-serial-reader");
setDaemon(true);
}
@Override
public void run() {
enterMessageMode();
try (final InputStream in = serialPort.getInputStream()) {
if (in == null) {
// should never happen
throw new IllegalStateException("serial port is not readable");
}
try (final InputStream bufIn = new BufferedInputStream(in)) {
bufIn.mark(MAX_READ_SIZE);
int len = 0;
while (!done) {
final int b = bufIn.read();
if (b == -1) {
// the serial input returns -1 on receive timeout
continue;
}
len++;
if (b == CR) {
// message terminator read, rewind the stream and parse the buffered message
try {
bufIn.reset();
processBuffer(bufIn, len);
} catch (final IOException e) {
logger.warn("buffer overrun, dropped long message", e);
} finally {
bufIn.mark(MAX_READ_SIZE);
len = 0;
}
}
}
}
} catch (final IOException e) {
logger.warn("Exception in UPB read thread", e);
} finally {
logger.debug("shutting down receive thread");
shutdownAndAwaitTermination(writeExecutor);
try {
serialPort.close();
} catch (final RuntimeException e) {
// ignore
}
}
logger.debug("UPB read thread stopped");
}
/**
* Attempts to parse a message from the input stream.
*
* @param in the stream to read from
* @param len the number of bytes in the message
*/
private void processBuffer(final InputStream in, final int len) {
final byte[] buf = new byte[len];
final int n;
try {
n = in.read(buf);
} catch (final IOException e) {
logger.warn("error reading message", e);
return;
}
if (n < len) {
// should not happen when replaying the buffered input
logger.warn("truncated read, expected={} read={}", len, n);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("UPB Message: {}", HexUtils.bytesToHex(buf));
}
final UPBMessage msg;
try {
msg = UPBMessage.parse(buf);
} catch (final MessageParseException e) {
logger.warn("failed to parse message: {}", HexUtils.bytesToHex(buf), e);
return;
}
handleMessage(msg);
}
private void handleMessage(final UPBMessage msg) {
final WriteRunnable writeRunnable = currentWrite;
switch (msg.getType()) {
case ACK:
if (writeRunnable != null) {
writeRunnable.ackReceived(true);
}
break;
case NAK:
if (writeRunnable != null) {
writeRunnable.ackReceived(false);
}
break;
case ACCEPT:
break;
case ERROR:
logger.debug("received ERROR response from PIM");
break;
default:
// ignore
}
listener.incomingMessage(msg);
}
public CompletionStage<CmdStatus> enqueue(final MessageBuilder msg) {
final CompletableFuture<CmdStatus> completion = new CompletableFuture<>();
final Runnable task = new WriteRunnable(msg.build(), completion);
try {
writeExecutor.execute(task);
} catch (final RejectedExecutionException e) {
completion.completeExceptionally(e);
}
return completion;
}
// puts the PIM is in message mode
private void enterMessageMode() {
try {
final OutputStream out = serialPort.getOutputStream();
if (out == null) {
throw new IOException("serial port is not writable");
}
out.write(ENABLE_MESSAGE_MODE_CMD);
out.flush();
} catch (final IOException e) {
logger.warn("error setting message mode", e);
}
}
void shutdownAndAwaitTermination(final ExecutorService pool) {
pool.shutdown();
try {
if (!pool.awaitTermination(1, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(1, TimeUnit.SECONDS)) {
logger.warn("executor did not terminate");
}
}
} catch (final InterruptedException ie) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
public void terminate() {
done = true;
try {
serialPort.close();
} catch (final RuntimeException e) {
logger.warn("failed to close serial port", e);
}
}
private class WriteRunnable implements Runnable {
private static final int MAX_RETRIES = 3;
private final String msg;
private final CompletableFuture<CmdStatus> completion;
private final CountDownLatch ackLatch = new CountDownLatch(1);
private @Nullable Boolean ack;
public WriteRunnable(final String msg, final CompletableFuture<CmdStatus> completion) {
this.msg = msg;
this.completion = completion;
}
// called by reader thread on ACK or NAK
public void ackReceived(final boolean ack) {
if (logger.isDebugEnabled()) {
if (ack) {
logger.debug("ACK received");
} else {
logger.debug("NAK received");
}
}
this.ack = ack;
ackLatch.countDown();
}
@Override
public void run() {
currentWrite = this;
try {
logger.debug("Writing bytes: {}", msg);
final OutputStream out = serialPort.getOutputStream();
if (out == null) {
throw new IOException("serial port is not writable");
}
for (int tries = 0; tries < MAX_RETRIES && ack == null; tries++) {
out.write(0x14);
out.write(msg.getBytes(US_ASCII));
out.write(0x0d);
out.flush();
final boolean acked = ackLatch.await(ACK_TIMEOUT_MS, MILLISECONDS);
if (acked) {
break;
}
logger.debug("ack timed out, retrying ({} of {})", tries + 1, MAX_RETRIES);
}
final Boolean ack = this.ack;
if (ack == null) {
logger.debug("write not acked");
completion.complete(CmdStatus.WRITE_FAILED);
} else if (ack) {
completion.complete(CmdStatus.ACK);
} else {
completion.complete(CmdStatus.NAK);
}
} catch (final IOException | InterruptedException e) {
logger.warn("error writing message", e);
completion.complete(CmdStatus.WRITE_FAILED);
}
}
}
}

View File

@@ -0,0 +1,181 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.handler;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.upb.internal.Constants;
import org.openhab.binding.upb.internal.message.MessageBuilder;
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.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Bridge handler responsible for serial PIM communications.
*
* @author Marcus Better - Initial contribution
*
*/
@NonNullByDefault
public class SerialPIMHandler extends PIMHandler {
private static final int SERIAL_RECEIVE_TIMEOUT_MS = 100;
private static final int BAUD_RATE = 4800;
private static final int SERIAL_PORT_OPEN_INIT_DELAY_MS = 500;
private static final int SERIAL_PORT_OPEN_RETRY_DELAY_MS = 30_000;
private final Logger logger = LoggerFactory.getLogger(SerialPIMHandler.class);
private SerialPortManager serialPortManager;
private volatile @Nullable SerialIoThread receiveThread;
private volatile @Nullable ScheduledFuture<?> futSerialPortInit;
public SerialPIMHandler(final Bridge thing, final SerialPortManager serialPortManager) {
super(thing);
this.serialPortManager = serialPortManager;
}
@Override
public void initialize() {
logger.debug("Initializing Serial UPB PIM {}.", getThing().getUID());
super.initialize();
final String portId = (String) getConfig().get(Constants.CONFIGURATION_PORT);
if (portId == null || portId.isEmpty()) {
logger.debug("serial port is not set");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
Constants.OFFLINE_SERIAL_PORT_NOT_SET);
return;
}
futSerialPortInit = scheduler.schedule(() -> openSerialPort(portId), SERIAL_PORT_OPEN_INIT_DELAY_MS,
TimeUnit.MILLISECONDS);
}
@Override
public void dispose() {
final ScheduledFuture<?> futSerialPortInit = this.futSerialPortInit;
if (futSerialPortInit != null) {
futSerialPortInit.cancel(true);
this.futSerialPortInit = null;
}
final SerialIoThread receiveThread = this.receiveThread;
if (receiveThread != null) {
receiveThread.terminate();
try {
receiveThread.join(1000);
} catch (final InterruptedException e) {
// ignore
}
this.receiveThread = null;
}
logger.debug("Stopped UPB serial handler");
super.dispose();
}
private void openSerialPort(final String portId) {
try {
final SerialPort serialPort = tryOpenSerialPort(portId);
if (serialPort == null) {
futSerialPortInit = scheduler.schedule(() -> openSerialPort(portId), SERIAL_PORT_OPEN_RETRY_DELAY_MS,
TimeUnit.MILLISECONDS);
return;
}
logger.debug("Starting receive thread");
final SerialIoThread receiveThread = new SerialIoThread(serialPort, this, getThing().getUID());
this.receiveThread = receiveThread;
// Once the receiver starts, it may set the PIM status to ONLINE
// so we must ensure all initialization is finished at that point.
receiveThread.start();
updateStatus(ThingStatus.ONLINE);
} catch (final RuntimeException e) {
logger.warn("failed to open serial port", e);
}
}
private @Nullable SerialPort tryOpenSerialPort(final String portId) {
logger.debug("opening serial port {}", portId);
final SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(portId);
if (portIdentifier == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
Constants.OFFLINE_SERIAL_EXISTS);
return null;
}
final SerialPort serialPort;
try {
serialPort = portIdentifier.open("org.openhab.binding.upb", 1000);
} catch (final PortInUseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
Constants.OFFLINE_SERIAL_INUSE);
return null;
}
try {
serialPort.setSerialPortParams(BAUD_RATE, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
try {
serialPort.enableReceiveThreshold(1);
serialPort.enableReceiveTimeout(SERIAL_RECEIVE_TIMEOUT_MS);
} catch (final UnsupportedCommOperationException e) {
// ignore - not supported for RFC2217 ports
}
} catch (final UnsupportedCommOperationException e) {
logger.debug("cannot open serial port", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
Constants.OFFLINE_SERIAL_UNSUPPORTED);
return null;
}
logger.debug("Serial port is initialized");
return serialPort;
}
@Override
public CompletionStage<CmdStatus> sendPacket(final MessageBuilder msg) {
final SerialIoThread receiveThread = this.receiveThread;
if (receiveThread != null) {
return receiveThread.enqueue(msg);
} else {
return exceptionallyCompletedFuture(new IllegalStateException("I/O thread not active"));
}
}
/**
* Returns a new {@code CompletableFuture} that is already exceptionally completed with
* the given exception.
*
* @param throwable the exception
* @param <T> an arbitrary type for the returned future; can be anything since the future
* will be exceptionally completed and thus there will never be a value of type
* {@code T}
* @return a future that exceptionally completed with the supplied exception
* @throws NullPointerException if the supplied throwable is {@code null}
* @since 0.1.0
*/
public static <T> CompletableFuture<T> exceptionallyCompletedFuture(final Throwable throwable) {
final CompletableFuture<T> future = new CompletableFuture<>();
future.completeExceptionally(throwable);
return future;
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.handler;
import java.util.concurrent.CompletionStage;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.upb.internal.message.MessageBuilder;
/**
* Handler for PIM communications.
*
* @author Marcus Better - Initial contribution
*/
@NonNullByDefault
public interface UPBIoHandler {
enum CmdStatus {
ACK,
NAK,
WRITE_FAILED
}
CompletionStage<CmdStatus> sendPacket(MessageBuilder message);
}

View File

@@ -0,0 +1,295 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.handler;
import static org.openhab.binding.upb.internal.message.Command.*;
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.upb.internal.Constants;
import org.openhab.binding.upb.internal.UPBDevice;
import org.openhab.binding.upb.internal.handler.UPBIoHandler.CmdStatus;
import org.openhab.binding.upb.internal.message.MessageBuilder;
import org.openhab.binding.upb.internal.message.UPBMessage;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
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.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler for things representing devices on an UPB network.
*
* @author Marcus Better - Initial contribution
*
*/
@NonNullByDefault
public class UPBThingHandler extends BaseThingHandler {
// Time to wait between attempts to poll the device to refresh state
private static final long REFRESH_INTERVAL_MS = 3_000;
private final Logger logger = LoggerFactory.getLogger(UPBThingHandler.class);
private final @Nullable Byte defaultNetworkId;
protected volatile byte networkId;
protected volatile int unitId;
private volatile long lastRefreshMillis;
public UPBThingHandler(final Thing device, final @Nullable Byte defaultNetworkId) {
super(device);
this.defaultNetworkId = defaultNetworkId;
}
@Override
public void initialize() {
logger.debug("initializing UPB thing handler {}", getThing().getUID());
final BigDecimal val = (BigDecimal) getConfig().get(Constants.CONFIGURATION_NETWORK_ID);
if (val == null) {
// use value from binding config
final Byte defaultNetworkId = this.defaultNetworkId;
if (defaultNetworkId == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing network ID");
return;
}
networkId = defaultNetworkId.byteValue();
} else if (val.compareTo(BigDecimal.ZERO) < 0 || val.compareTo(BigDecimal.valueOf(255)) > 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "invalid network ID");
return;
} else {
networkId = val.byteValue();
}
final BigDecimal cfgUnitId = (BigDecimal) getConfig().get(Constants.CONFIGURATION_UNIT_ID);
if (cfgUnitId == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing unit ID");
return;
}
unitId = cfgUnitId.intValue();
if (unitId < 1 || unitId > 250) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "invalid unit ID");
return;
}
final Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, Constants.OFFLINE_CTLR_OFFLINE);
return;
}
bridgeStatusChanged(bridge.getStatusInfo());
}
@Override
public void bridgeStatusChanged(final ThingStatusInfo bridgeStatusInfo) {
logger.debug("DEV {}: Controller status is {}", unitId, bridgeStatusInfo.getStatus());
if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, Constants.OFFLINE_CTLR_OFFLINE);
return;
}
logger.debug("DEV {}: Controller is ONLINE. Starting device initialisation.", unitId);
final Bridge bridge = getBridge();
if (bridge == null) {
logger.debug("DEV {}: bridge is null!", unitId);
return;
}
final PIMHandler bridgeHandler = (PIMHandler) bridge.getHandler();
if (bridgeHandler == null) {
logger.debug("DEV {}: bridge handler is null!", unitId);
return;
}
updateDeviceStatus(bridgeHandler);
pingDevice();
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command cmd) {
final PIMHandler pimHandler = getPIMHandler();
if (pimHandler == null) {
logger.warn("DEV {}: received cmd {} but no bridge handler", unitId, cmd);
return;
}
final MessageBuilder message;
if (cmd == OnOffType.ON) {
message = MessageBuilder.forCommand(ACTIVATE);
} else if (cmd == OnOffType.OFF) {
message = MessageBuilder.forCommand(DEACTIVATE);
} else if (cmd instanceof PercentType) {
message = MessageBuilder.forCommand(GOTO).args(((PercentType) cmd).byteValue());
} else if (cmd == RefreshType.REFRESH) {
refreshDeviceState();
return;
} else {
logger.warn("channel {}: unsupported cmd {}", channelUID, cmd);
return;
}
message.network(networkId).destination(getUnitId());
pimHandler.sendPacket(message).thenAccept(this::updateStatus);
}
public void onMessageReceived(final UPBMessage msg) {
updateStatus(ThingStatus.ONLINE);
if (msg.getControlWord().isLink()) {
handleLinkMessage(msg);
} else {
handleDirectMessage(msg);
}
}
private void handleDirectMessage(final UPBMessage msg) {
final State state;
switch (msg.getCommand()) {
case ACTIVATE:
state = OnOffType.ON;
break;
case DEACTIVATE:
state = OnOffType.OFF;
break;
case GOTO:
if (msg.getArguments().length == 0) {
logger.warn("DEV {}: malformed GOTO cmd", unitId);
return;
}
final int level = msg.getArguments()[0];
state = new PercentType(level);
break;
default:
logger.debug("DEV {}: Message {} ignored", unitId, msg.getCommand());
return;
}
updateState(Constants.DIMMER_TYPE_ID, state);
}
private void handleLinkMessage(final UPBMessage msg) {
final byte linkId = msg.getDestination();
for (final Channel ch : getThing().getChannels()) {
ChannelTypeUID channelTypeUID = ch.getChannelTypeUID();
if (channelTypeUID != null && Constants.SCENE_CHANNEL_TYPE_ID.equals(channelTypeUID.getId())) {
final BigDecimal channelLinkId = (BigDecimal) ch.getConfiguration()
.get(Constants.CONFIGURATION_LINK_ID);
if (channelLinkId == null || channelLinkId.byteValue() != linkId) {
continue;
}
switch (msg.getCommand()) {
case ACTIVATE:
case DEACTIVATE:
triggerChannel(ch.getUID(), msg.getCommand().name());
break;
default:
logger.debug("DEV {}: Message {} ignored for link {}", unitId, linkId & 0xff, msg.getCommand());
return;
}
}
}
}
private void updateDeviceStatus(final PIMHandler bridgeHandler) {
final UPBDevice device = bridgeHandler.getDevice(getNetworkId(), getUnitId());
if (device == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, Constants.OFFLINE_NODE_NOTFOUND);
} else {
switch (device.getState()) {
case INITIALIZING:
case ALIVE:
updateStatus(ThingStatus.ONLINE);
break;
case DEAD:
case FAILED:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
Constants.OFFLINE_NODE_DEAD);
break;
}
}
}
protected void pingDevice() {
final PIMHandler pimHandler = getPIMHandler();
if (pimHandler != null) {
pimHandler.sendPacket(
MessageBuilder.forCommand(NULL).ackMessage(true).network(networkId).destination((byte) unitId))
.thenAccept(this::updateStatus);
}
}
private void updateStatus(final CmdStatus result) {
switch (result) {
case WRITE_FAILED:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, Constants.OFFLINE_NODE_DEAD);
break;
case ACK:
case NAK:
updateStatus(ThingStatus.ONLINE);
break;
}
}
private void refreshDeviceState() {
// This polls the device to see if it is alive. Since the REFRESH command is sent
// for each channel and we want to avoid unnecessary traffic, we only ping the device
// if some time has elapsed since the last refresh.
final long now = System.currentTimeMillis();
if (now - lastRefreshMillis > REFRESH_INTERVAL_MS) {
lastRefreshMillis = now;
final PIMHandler pimHandler = getPIMHandler();
if (pimHandler != null) {
pimHandler
.sendPacket(MessageBuilder.forCommand(REPORT_STATE).network(networkId).destination(getUnitId()))
.thenAccept(this::updateStatus);
}
}
}
protected @Nullable PIMHandler getPIMHandler() {
final Bridge bridge = getBridge();
if (bridge == null) {
logger.debug("DEV {}: bridge is null!", unitId);
return null;
}
final PIMHandler bridgeHandler = (PIMHandler) bridge.getHandler();
if (bridgeHandler == null) {
logger.debug("DEV {}: bridge handler is null!", unitId);
return null;
}
return bridgeHandler;
}
public byte getNetworkId() {
return networkId;
}
public byte getUnitId() {
return (byte) unitId;
}
}

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.handler;
import static org.openhab.binding.upb.internal.message.Command.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.upb.internal.Constants;
import org.openhab.binding.upb.internal.message.MessageBuilder;
import org.openhab.binding.upb.internal.message.UPBMessage;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Thing handler for a virtual device.
*
* @author Marcus Better - Initial contribution
*
*/
@NonNullByDefault
public class VirtualThingHandler extends UPBThingHandler {
private final Logger logger = LoggerFactory.getLogger(VirtualThingHandler.class);
public VirtualThingHandler(final Thing device, final @Nullable Byte defaultNetworkId) {
super(device, defaultNetworkId);
}
@Override
protected void pingDevice() {
// always succeeds for virtual device
updateStatus(ThingStatus.ONLINE);
}
@Override
public void handleCommand(final ChannelUID channelUID, final Command cmd) {
final PIMHandler pimHandler = getPIMHandler();
if (pimHandler == null) {
logger.info("DEV {}: received cmd {} but no bridge handler", unitId, cmd);
return;
}
if (cmd == RefreshType.REFRESH) {
// there is no way to read the currently active scene
return;
} else if (!(cmd instanceof DecimalType)) {
logger.info("channel {}: unsupported cmd {}", channelUID, cmd);
return;
}
final MessageBuilder message;
if (channelUID.getId().equals(Constants.LINK_ACTIVATE_CHANNEL_ID)) {
message = MessageBuilder.forCommand(ACTIVATE);
} else if (channelUID.getId().equals(Constants.LINK_DEACTIVATE_CHANNEL_ID)) {
message = MessageBuilder.forCommand(DEACTIVATE);
} else {
logger.warn("channel {}: unexpected channel type", channelUID);
return;
}
final byte dst = ((DecimalType) cmd).byteValue();
message.network(networkId).destination(dst).link(true);
pimHandler.sendPacket(message);
}
@Override
public void onMessageReceived(final UPBMessage msg) {
final byte linkId = msg.getDestination();
final String channelId;
switch (msg.getCommand()) {
case ACTIVATE:
channelId = Constants.LINK_ACTIVATE_CHANNEL_ID;
break;
case DEACTIVATE:
channelId = Constants.LINK_DEACTIVATE_CHANNEL_ID;
break;
default:
logger.info("DEV {}: Message {} ignored for link {}", unitId, linkId & 0xff, msg.getCommand());
return;
}
final Channel ch = getThing().getChannel(channelId);
if (ch == null) {
return;
}
updateState(ch.getUID(), new DecimalType(linkId));
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.message;
/**
* An enum of possible commands.
*
* @author cvanorman - Initial contribution
*/
public enum Command {
NULL(0),
ACTIVATE(0x20),
DEACTIVATE(0x21),
GOTO(0x22),
START_FADE(0x23),
STOP_FADE(0x24),
BLINK(0x25),
REPORT_STATE(0x30),
STORE_STATE(0x31),
DEVICE_STATE(0x86);
private final byte mdid;
Command(final int mdid) {
this.mdid = (byte) mdid;
}
/**
* @return the protocol Message Data ID (MDID) for this Command
*/
public byte toByte() {
return mdid;
}
/**
* Returns the Command for a given Message Data ID byte.
*
* @param value the MDID byte
* @return the Command for the given MDID
*/
public static Command valueOf(final byte value) {
for (final Command cmd : values()) {
if (cmd.toByte() == value) {
return cmd;
}
}
return NULL;
}
}

View File

@@ -0,0 +1,186 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Model for the first two bytes of UPB messages.
*
* @author cvanorman - Initial contribution
* @since 1.9.0
*/
@NonNullByDefault
public class ControlWord {
private static final int TRANSMIT_COUNT_SHIFT = 2;
private static final int TRANSMIT_COUNT_MASK = 0b00001100;
private static final int TRANSMIT_SEQUENCE_MASK = 0b00000011;
private static final int ACK_PULSE_MASK = 0b00010000;
private static final int ID_PULSE_MASK = 0b00100000;
private static final int ACK_MESSAGE_MASK = 0b01000000;
private static final int REPEATER_COUNT_SHIFT = 5;
private static final int REPEATER_COUNT_MASK = 0b01100000;
private static final int PACKET_LENGTH_MASK = 0b00011111;
private static final int LINK_MASK = 0b10000000;
private byte hi = 0;
private byte lo = 0;
/**
* Sets the two bytes of the control word.
*
* @param lo
* the low-order byte.
* @param hi
* the high-order byte.
*/
public void setBytes(final byte hi, final byte lo) {
this.hi = hi;
this.lo = lo;
}
/**
* @return the high byte of the control word
*/
public byte getHi() {
return hi;
}
/**
* @return the low byte of the control word
*/
public byte getLo() {
return lo;
}
/**
* @return the LNK bit
*/
public boolean isLink() {
return (hi & LINK_MASK) > 0;
}
/**
* @param link
* the link to set
*/
public void setLink(boolean link) {
hi = (byte) (link ? hi | LINK_MASK : hi & ~LINK_MASK);
}
/**
* @return the repeaterCount
*/
public int getRepeaterCount() {
return (hi & REPEATER_COUNT_MASK) >> REPEATER_COUNT_SHIFT;
}
/**
* @param repeaterCount
* the repeaterCount to set
*/
public void setRepeaterCount(int repeaterCount) {
hi = (byte) (hi | (repeaterCount << REPEATER_COUNT_SHIFT));
}
/**
* @return the packetLength
*/
public int getPacketLength() {
return hi & PACKET_LENGTH_MASK;
}
/**
* @param packetLength
* the packetLength to set
*/
public void setPacketLength(int packetLength) {
hi = (byte) (hi | packetLength);
}
/**
* @return the transmitCount
*/
public int getTransmitCount() {
return (lo & TRANSMIT_COUNT_MASK) >> TRANSMIT_COUNT_SHIFT;
}
/**
* @param transmitCount
* the transmitCount to set
*/
public void setTransmitCount(int transmitCount) {
lo = (byte) (lo | (transmitCount << TRANSMIT_COUNT_SHIFT));
}
/**
* @return the transmitSequence
*/
public int getTransmitSequence() {
return lo & TRANSMIT_SEQUENCE_MASK;
}
/**
* @param transmitSequence
* the transmitSequence to set
*/
public void setTransmitSequence(int transmitSequence) {
lo = (byte) (lo | transmitSequence);
}
/**
* @return the ackPulse
*/
public boolean isAckPulse() {
return (lo & ACK_PULSE_MASK) > 0;
}
/**
* @param ackPulse
* the ackPulse to set
*/
public void setAckPulse(boolean ackPulse) {
lo = (byte) (ackPulse ? lo | ACK_PULSE_MASK : lo & ~ACK_PULSE_MASK);
}
/**
* @return the idPulse
*/
public boolean isIdPulse() {
return (lo & ID_PULSE_MASK) > 0;
}
/**
* @param idPulse
* the idPulse to set
*/
public void setIdPulse(boolean idPulse) {
lo = (byte) (idPulse ? lo | ID_PULSE_MASK : lo & ~ID_PULSE_MASK);
}
/**
* @return the ackMessage
*/
public boolean isAckMessage() {
return (lo & ACK_MESSAGE_MASK) > 0;
}
/**
* @param ackMessage
* the ackMessage to set
*/
public void setAckMessage(boolean ackMessage) {
lo = (byte) (ackMessage ? lo | ACK_MESSAGE_MASK : lo & ~ACK_MESSAGE_MASK);
}
}

View File

@@ -0,0 +1,152 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.util.HexUtils;
/**
* Builder class for building UPB messages.
*
* @author cvanorman - Initial contribution
* @since 1.9.0
*/
@NonNullByDefault
public final class MessageBuilder {
private byte network;
private byte source = -1;
private byte destination;
private byte command;
private byte[] args = new byte[0];
private boolean link;
private boolean ackMessage;
private MessageBuilder(final Command cmd) {
this.command = cmd.toByte();
}
/**
* @return a new MessageBuilder for the specified command
*/
public static MessageBuilder forCommand(final Command cmd) {
return new MessageBuilder(cmd);
}
/**
* Sets where this message is for a device or a link.
*
* @param link
* set to true if this message is for a link.
* @return this builder
*/
public MessageBuilder link(boolean link) {
this.link = link;
return this;
}
/**
* Sets the UPB network of the message.
*
* @param network
* the network of the message.
* @return this builder
*/
public MessageBuilder network(byte network) {
this.network = network;
return this;
}
/**
* Sets the source id of the message (defaults to 0xFF).
*
* @param source
* the source if of the message.
* @return this builder
*/
public MessageBuilder source(byte source) {
this.source = source;
return this;
}
/**
* Sets the destination id of the message.
*
* @param destination
* the destination id.
* @return this builder
*/
public MessageBuilder destination(byte destination) {
this.destination = destination;
return this;
}
/**
* Sets any command arguments.
*
* @param args the arguments (bytes following the command byte)
* @return this builder
*/
public MessageBuilder args(byte... args) {
this.args = args;
return this;
}
/**
* Sets whether an Acknowledgement Response message should be requested
* (by setting the the MSG-bit in the control word).
*
* @param ackMessage {@code true} if the MSG-bit should be set
* @return this builder
*/
public MessageBuilder ackMessage(final boolean ackMessage) {
this.ackMessage = ackMessage;
return this;
}
/**
* Builds the message as a HEX string.
*
* @return a HEX string of the message.
*/
public String build() {
ControlWord controlWord = new ControlWord();
int packetLength = args.length + 7;
controlWord.setPacketLength(packetLength);
controlWord.setAckPulse(true);
controlWord.setAckMessage(ackMessage);
controlWord.setLink(link);
byte[] bytes = new byte[packetLength];
bytes[0] = controlWord.getHi();
bytes[1] = controlWord.getLo();
bytes[2] = network;
bytes[3] = destination;
bytes[4] = source;
bytes[5] = command;
System.arraycopy(args, 0, bytes, 6, args.length);
// Calculate the checksum
// The checksum is the 2's complement of the sum.
int sum = 0;
for (byte b : bytes) {
sum += b;
}
bytes[bytes.length - 1] = (byte) (-sum >>> 0);
return HexUtils.bytesToHex(bytes);
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception indicating a message parsing error.
*
* @author Marcus Better - Initial contribution
*/
@NonNullByDefault
public class MessageParseException extends RuntimeException {
private static final long serialVersionUID = 1L;
public MessageParseException(final String message) {
super(message);
}
public MessageParseException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,222 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.message;
import static java.nio.charset.StandardCharsets.US_ASCII;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.util.HexUtils;
/**
* Model for a message sent or received from a UPB modem.
*
* @author cvanorman - Initial contribution
*/
@NonNullByDefault
public class UPBMessage {
/**
* An enum of possible modem response types.
*/
public enum Type {
ACCEPT("PA"),
BUSY("PB"),
ERROR("PE"),
ACK("PK"),
NAK("PN"),
MESSAGE_REPORT("PU"),
NONE("");
private final byte[] prefix;
Type(final String prefix) {
this.prefix = prefix.getBytes(US_ASCII);
}
/**
* Returns the message type for a message buffer.
*
* @param prefix the byte array to check for a matching type prefix
* @return the matching message type, or {@code NONE}
*/
public static Type forPrefix(final byte[] buf) {
if (buf.length >= 2) {
for (final Type t : values()) {
if (t.prefix.length >= 2 && buf[0] == t.prefix[0] && buf[1] == t.prefix[1]) {
return t;
}
}
}
return NONE;
}
}
private final Type type;
private ControlWord controlWord = new ControlWord();
private byte network;
private byte destination;
private byte source;
private Command command = Command.NULL;
private byte[] arguments = new byte[0];
private UPBMessage(final Type type) {
this.type = type;
}
/**
* Converts a hex string into a {@link UPBMessage}.
*
* @param commandString
* the string as returned by the modem.
* @return a new UPBMessage.
*/
public static UPBMessage parse(final byte[] buf) {
if (buf.length < 2) {
throw new MessageParseException("message too short");
}
final UPBMessage msg = new UPBMessage(Type.forPrefix(buf));
try {
if (buf.length >= 15) {
byte[] data = unhex(buf, 2, buf.length - 1);
msg.getControlWord().setBytes(data[0], data[1]);
int index = 2;
msg.setNetwork(data[index++]);
msg.setDestination(data[index++]);
msg.setSource(data[index++]);
byte commandCode = data[index++];
msg.setCommand(Command.valueOf(commandCode));
if (index <= data.length - 1) {
msg.setArguments(Arrays.copyOfRange(data, index, data.length - 1));
}
}
} catch (final RuntimeException e) {
throw new MessageParseException("failed to parse message", e);
}
return msg;
}
private static byte[] unhex(final byte[] buf, final int start, final int end) {
final byte[] res = new byte[(end - start) / 2];
int i = start;
int j = 0;
while (i < end - 1) {
res[j++] = HexUtils.hexToByte(buf[i++], buf[i++]);
}
return res;
}
/**
* @return the type
*/
public Type getType() {
return type;
}
/**
* @return the controlWord
*/
public ControlWord getControlWord() {
return controlWord;
}
/**
* @param controlWord
* the controlWord to set
*/
public void setControlWord(ControlWord controlWord) {
this.controlWord = controlWord;
}
/**
* @return the network
*/
public byte getNetwork() {
return network;
}
/**
* @param network
* the network to set
*/
public void setNetwork(byte network) {
this.network = network;
}
/**
* @return the destination
*/
public byte getDestination() {
return destination;
}
/**
* @param destination
* the destination to set
*/
public void setDestination(byte destination) {
this.destination = destination;
}
/**
* @return the source
*/
public byte getSource() {
return source;
}
/**
* @param source
* the source to set
*/
public void setSource(byte source) {
this.source = source;
}
/**
* @return the command
*/
public Command getCommand() {
return command;
}
/**
* @param command
* the command to set
*/
public void setCommand(Command command) {
this.command = command;
}
/**
* @return the arguments
*/
public byte[] getArguments() {
return arguments;
}
/**
* @param arguments
* the arguments to set
*/
public void setArguments(byte[] arguments) {
this.arguments = arguments;
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="upb" 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>UPB Binding</name>
<description>The Universal Powerline Bus (UPB) binding reads and writes messages to and from a UPB modem</description>
<author>Marcus Better</author>
<service-id>org.openhab.upb</service-id>
<config-description>
<parameter name="networkId" type="integer" min="0" max="255" required="false">
<label>Default Network ID for Devices</label>
<description>The ID of the primary UPB network. May be overridden on a per-device basis.</description>
</parameter>
</config-description>
</binding:binding>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="upb:device:address">
<parameter name="networkId" type="integer" min="0" max="255" required="false">
<label>Network ID</label>
<description>The ID of the UPB network that the device belongs to</description>
</parameter>
<parameter name="unitId" type="integer" min="1" max="250" required="true">
<label>Unit ID</label>
<description>The unit ID of the device on the UPB network</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,9 @@
upb.thingstate.controller_offline=PIM is offline
upb.thingstate.controller_comm_error=PIM communication error
upb.thingstate.node_dead=Device is not communicating with controller
upb.thingstate.node_notfound=Node not found in network
upb.thingstate.serial_notfound=Serial Error: Port {0} does not exist
upb.thingstate.serial_inuse=Serial Error: Port {0} is in use
upb.thingstate.serial_unsupported=Serial Error: Unsupported operation on port {0}
upb.thingstate.serial_listeners=Serial Error: Too many listeners on port {0}
upb.thingstate.serial_cfg_port=Serial port not configured

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="upb" 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">
<channel-type id="link">
<item-type>Number</item-type>
<label>Scene</label>
<description>Selected scene</description>
<state min="1" max="250" step="1" readOnly="false"/>
</channel-type>
<channel-type id="scene-selection">
<kind>trigger</kind>
<label>Scene Selection Events</label>
<event>
<options>
<option value="ACTIVATED">activated</option>
<option value="DEACTIVATED">deactivated</option>
</options>
</event>
<config-description>
<parameter name="linkId" type="integer" min="1" max="250" required="true">
<label>Link ID</label>
<description>The link ID or scene number that this channel corresponds to</description>
</parameter>
</config-description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="upb" 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">
<thing-type id="leviton-38a00-1">
<supported-bridge-type-refs>
<bridge-type-ref id="serial-pim"/>
</supported-bridge-type-refs>
<label>Leviton UPB 6-Button Scene Switch</label>
<description>A wall-mounted panel with six pushbuttons labeled ON, A, B, C, D, and OFF.</description>
<category>WallSwitch</category>
<channels>
<channel id="btnOn" typeId="scene-selection">
<label>ON Button</label>
</channel>
<channel id="btnOff" typeId="scene-selection">
<label>OFF Button</label>
</channel>
<channel id="btnA" typeId="scene-selection">
<label>Scene A</label>
</channel>
<channel id="btnB" typeId="scene-selection">
<label>Scene B</label>
</channel>
<channel id="btnC" typeId="scene-selection">
<label>Scene C</label>
</channel>
<channel id="btnD" typeId="scene-selection">
<label>Scene D</label>
</channel>
</channels>
<config-description-ref uri="upb:device:address"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="upb" 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="serial-pim">
<label>Serial PIM</label>
<description>A serial Powerline Interface Module (PIM) is a modem for UPB</description>
<config-description>
<parameter name="port" type="text" required="true">
<label>Serial port</label>
<context>serial-port</context>
<description>The file name of the serial port to use to communicate with the PIM.</description>
<limitToOptions>false</limitToOptions>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="upb" 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">
<thing-type id="generic">
<supported-bridge-type-refs>
<bridge-type-ref id="serial-pim"/>
</supported-bridge-type-refs>
<label>Generic Powerline Device</label>
<description>A generic device in a UPB network</description>
<category>WallSwitch</category>
<channels>
<channel id="dimmer" typeId="system.brightness"/>
</channels>
<config-description-ref uri="upb:device:address"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="upb" 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">
<thing-type id="virtual">
<supported-bridge-type-refs>
<bridge-type-ref id="serial-pim"/>
</supported-bridge-type-refs>
<label>Virtual UPB Device</label>
<description>This pseudo-device is useful for switching scenes and receiving scene updates.
It does not correspond to
any physical device on the network.
</description>
<category>WallSwitch</category>
<channels>
<channel id="linkActivated" typeId="link">
<label>Link Activated</label>
</channel>
<channel id="linkDeactivated" typeId="link">
<label>Link Deactivated</label>
</channel>
</channels>
<config-description-ref uri="upb:device:address"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.upb.internal.message;
import static org.junit.Assert.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
/**
* @author Marcus Better - Initial contribution
*/
@NonNullByDefault
public class MessageBuilderTest {
@Test
public void testActivateCmd() {
final MessageBuilder msg = MessageBuilder.forCommand(Command.ACTIVATE).network((byte) 1).destination((byte) 2);
assertEquals("07100102FF20C7", msg.build());
}
@Test
public void testGoto() {
final MessageBuilder msg = MessageBuilder.forCommand(Command.GOTO).args((byte) 0x32).network((byte) 1)
.destination((byte) 2);
assertEquals("08100102FF223292", msg.build());
}
@Test
public void testDeactivateLink() {
final MessageBuilder msg = MessageBuilder.forCommand(Command.DEACTIVATE).network((byte) 1).destination((byte) 2)
.link(true);
assertEquals("87100102FF2146", msg.build());
}
}