added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
10
bundles/org.openhab.binding.upb/src/main/feature/feature.xml
Normal file
10
bundles/org.openhab.binding.upb/src/main/feature/feature.xml
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user