diff --git a/bundles/binding/org.openhab.binding.upb/.classpath b/bundles/binding/org.openhab.binding.upb/.classpath new file mode 100644 index 00000000000..7bc456b9c25 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/bundles/binding/org.openhab.binding.upb/.project b/bundles/binding/org.openhab.binding.upb/.project new file mode 100644 index 00000000000..9fcb319f30f --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/.project @@ -0,0 +1,39 @@ + + + org.openhab.binding.upb + This is the UPB binding of the open Home Automation Bus (openHAB) + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + org.eclipse.pde.PluginNature + + diff --git a/bundles/binding/org.openhab.binding.upb/META-INF/MANIFEST.MF b/bundles/binding/org.openhab.binding.upb/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..f4c6e4ce467 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/META-INF/MANIFEST.MF @@ -0,0 +1,31 @@ +Manifest-Version: 1.0 +Private-Package: org.openhab.binding.upb.internal +Ignore-Package: org.openhab.binding.upb.internal +Bundle-License: http://www.eclipse.org/legal/epl-v10.html +Bundle-Name: openHAB UPB Binding +Bundle-SymbolicName: org.openhab.binding.upb +Bundle-Vendor: openHAB.org +Bundle-Version: 1.9.0.qualifier +Bundle-ManifestVersion: 2 +Bundle-Description: This is the UPB binding of the open Home Aut + omation Bus (openHAB) +Import-Package: + org.apache.commons.lang, + org.openhab.core.binding, + org.openhab.core.events, + org.openhab.core.items, + org.openhab.core.library.items, + org.openhab.core.library.types, + org.openhab.core.types, + org.openhab.model.item.binding, + org.osgi.framework, + org.osgi.service.cm, + org.osgi.service.component, + org.osgi.service.event, + org.slf4j +Export-Package: org.openhab.binding.upb +Bundle-DocURL: http://www.openhab.org +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Service-Component: OSGI-INF/binding.xml, OSGI-INF/genericbindingprovider.xml +Bundle-ClassPath: ., lib/nrjavaserial-3.11.0.jar +Bundle-ActivationPolicy: lazy diff --git a/bundles/binding/org.openhab.binding.upb/OSGI-INF/binding.xml b/bundles/binding/org.openhab.binding.upb/OSGI-INF/binding.xml new file mode 100644 index 00000000000..72975dc5b66 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/OSGI-INF/binding.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/bundles/binding/org.openhab.binding.upb/OSGI-INF/genericbindingprovider.xml b/bundles/binding/org.openhab.binding.upb/OSGI-INF/genericbindingprovider.xml new file mode 100644 index 00000000000..79538baa98e --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/OSGI-INF/genericbindingprovider.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/bundles/binding/org.openhab.binding.upb/build.properties b/bundles/binding/org.openhab.binding.upb/build.properties new file mode 100644 index 00000000000..45e1fea35d6 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/build.properties @@ -0,0 +1,7 @@ +source.. = src/main/java/,\ + src/main/resources/ +bin.includes = META-INF/,\ + .,\ + OSGI-INF/,\ + lib/nrjavaserial-3.11.0.jar +output.. = target/classes/ diff --git a/bundles/binding/org.openhab.binding.upb/lib/nrjavaserial-3.11.0.jar b/bundles/binding/org.openhab.binding.upb/lib/nrjavaserial-3.11.0.jar new file mode 100644 index 00000000000..6e7e7c46ba2 Binary files /dev/null and b/bundles/binding/org.openhab.binding.upb/lib/nrjavaserial-3.11.0.jar differ diff --git a/bundles/binding/org.openhab.binding.upb/pom.xml b/bundles/binding/org.openhab.binding.upb/pom.xml new file mode 100644 index 00000000000..b64a6530ba3 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/pom.xml @@ -0,0 +1,35 @@ + + + + + org.openhab.bundles + binding + 1.9.0-SNAPSHOT + + + + org.openhab.binding.upb + org.openhab.binding.upb + openhab-addon-binding-upb + ${project.name} + + + 4.0.0 + org.openhab.binding + org.openhab.binding.upb + + openHAB UPB Binding + + eclipse-plugin + + + + + org.vafer + jdeb + + + + + diff --git a/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/UPBBindingProvider.java b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/UPBBindingProvider.java new file mode 100644 index 00000000000..6f5c72dc501 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/UPBBindingProvider.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2016, openHAB.org and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.upb; + +import org.openhab.binding.upb.internal.UPBBindingConfig; +import org.openhab.core.binding.BindingProvider; + +/** + * Interface for the {@link BindingProvider} for the UPB binding. + * + * @author cvanorman + * @since 1.9.0 + */ +public interface UPBBindingProvider extends BindingProvider { + + /** + * Gets the configuration of an item. + * + * @param itemName + * the name of the item. + * @return the {@link UPBBindingConfig} for the given item or null if one + * does not exist. + */ + UPBBindingConfig getConfig(String itemName); +} diff --git a/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/ControlWord.java b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/ControlWord.java new file mode 100644 index 00000000000..44f9cb2fc74 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/ControlWord.java @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2010-2016, openHAB.org and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.upb.internal; + +/** + * Model for the first two bytes of UPB messages. + * + * @author cvanorman + * @since 1.9.0 + */ +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 one = 0; + private byte two = 0; + + /** + * Sets the two bytes of the control word. + * + * @param one + * the first byte. + * @param two + * the second byte. + */ + public void setBytes(byte one, byte two) { + this.one = one; + this.two = two; + } + + /** + * @return the two bytes of the control word. + */ + public byte[] getBytes() { + return new byte[] { two, one }; + } + + /** + * @return the link + */ + public boolean isLink() { + return (two & LINK_MASK) > 0; + } + + /** + * @param link + * the link to set + */ + public void setLink(boolean link) { + two = (byte) (link ? two | LINK_MASK : two & ~LINK_MASK); + } + + /** + * @return the repeaterCount + */ + public int getRepeaterCount() { + return (two & REPEATER_COUNT_MASK) >> REPEATER_COUNT_SHIFT; + } + + /** + * @param repeaterCount + * the repeaterCount to set + */ + public void setRepeaterCount(int repeaterCount) { + two = (byte) (two | (repeaterCount << REPEATER_COUNT_SHIFT)); + } + + /** + * @return the packetLength + */ + public int getPacketLength() { + return two & PACKET_LENGTH_MASK; + } + + /** + * @param packetLength + * the packetLength to set + */ + public void setPacketLength(int packetLength) { + two = (byte) (two | packetLength); + } + + /** + * @return the transmitCount + */ + public int getTransmitCount() { + return (one & TRANSMIT_COUNT_MASK) >> TRANSMIT_COUNT_SHIFT; + } + + /** + * @param transmitCount + * the transmitCount to set + */ + public void setTransmitCount(int transmitCount) { + one = (byte) (one | (transmitCount << TRANSMIT_COUNT_SHIFT)); + } + + /** + * @return the transmitSequence + */ + public int getTransmitSequence() { + return one & TRANSMIT_SEQUENCE_MASK; + } + + /** + * @param transmitSequence + * the transmitSequence to set + */ + public void setTransmitSequence(int transmitSequence) { + one = (byte) (one | transmitSequence); + } + + /** + * @return the ackPulse + */ + public boolean isAckPulse() { + return (one & ACK_PULSE_MASK) > 0; + } + + /** + * @param ackPulse + * the ackPulse to set + */ + public void setAckPulse(boolean ackPulse) { + one = (byte) (ackPulse ? one | ACK_PULSE_MASK : one & ~ACK_PULSE_MASK); + } + + /** + * @return the idPulse + */ + public boolean isIdPulse() { + return (one & ID_PULSE_MASK) > 0; + } + + /** + * @param idPulse + * the idPulse to set + */ + public void setIdPulse(boolean idPulse) { + one = (byte) (idPulse ? one | ID_PULSE_MASK : one & ~ID_PULSE_MASK); + } + + /** + * @return the ackMessage + */ + public boolean isAckMessage() { + return (one & ACK_MESSAGE_MASK) > 0; + } + + /** + * @param ackMessage + * the ackMessage to set + */ + public void setAckMessage(boolean ackMessage) { + one = (byte) (ackMessage ? one | ACK_MESSAGE_MASK : one & ~ACK_MESSAGE_MASK); + } +} diff --git a/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/MessageBuilder.java b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/MessageBuilder.java new file mode 100644 index 00000000000..0997e31a4bc --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/MessageBuilder.java @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2010-2016, openHAB.org and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.upb.internal; + +import javax.xml.bind.DatatypeConverter; + +/** + * Builder class for building UPB messages. + * + * @author cvanorman + * @since 1.9.0 + */ +public final class MessageBuilder { + + private byte network; + private byte source = -1; + private byte destination; + private byte[] commands; + private boolean link = false; + + /** + * Gets a new {@link MessageBuilder} instance. + * + * @return a new MessageBuilder. + */ + public static MessageBuilder create() { + return new MessageBuilder(); + } + + private MessageBuilder() { + + } + + /** + * Sets where this message is for a device or a link. + * + * @param link + * set to true if this message is for a link. + * @return the same MessageBuilder instance. + */ + 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 the same MessageBuilder instance. + */ + 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 the same MessageBuilder instance. + */ + public MessageBuilder source(byte source) { + this.source = source; + return this; + } + + /** + * Sets the destination id of the message. + * + * @param destination + * the destination id. + * @return the same MessageBuilder instance. + */ + public MessageBuilder destination(byte destination) { + this.destination = destination; + return this; + } + + /** + * Sets the command and any arguments of the message. + * + * @param commands + * the command followed by any arguments. + * @return the same MessageBuilder instance. + */ + public MessageBuilder command(byte... commands) { + this.commands = commands; + 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 = 6 + commands.length; + + controlWord.setPacketLength(packetLength); + controlWord.setAckPulse(true); + controlWord.setLink(link); + + int index = 2; + byte[] bytes = new byte[packetLength]; + bytes[index++] = network; + bytes[index++] = destination; + bytes[index++] = source; + + // Copy in the header + System.arraycopy(controlWord.getBytes(), 0, bytes, 0, 2); + + // Copy in the actual command and arguments being sent. + System.arraycopy(commands, 0, bytes, index, commands.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] = new Integer(-sum >>> 0).byteValue(); + + return DatatypeConverter.printHexBinary(bytes); + } +} diff --git a/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBBinding.java b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBBinding.java new file mode 100644 index 00000000000..3faf40ff770 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBBinding.java @@ -0,0 +1,320 @@ +/** + * Copyright (c) 2010-2016, openHAB.org and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.upb.internal; + +import java.util.Map; + +import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang.StringUtils; +import org.openhab.binding.upb.UPBBindingProvider; +import org.openhab.binding.upb.internal.UPBMessage.Type; +import org.openhab.core.binding.AbstractActiveBinding; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import gnu.io.CommPortIdentifier; +import gnu.io.NoSuchPortException; +import gnu.io.PortInUseException; +import gnu.io.SerialPort; +import gnu.io.UnsupportedCommOperationException; + +/** + * Binding for Universal Powerline Bus (UPB) that reads and writes messages to + * and from the UPB modem. + * + * @author cvanorman + * @since 1.9.0 + */ +public class UPBBinding extends AbstractActiveBinding implements UPBReader.Listener { + + private static final Logger logger = LoggerFactory.getLogger(UPBBinding.class); + + private String port; + private byte network = 0; + private SerialPort serialPort; + private UPBReader upbReader; + private UPBWriter upbWriter; + + /** + * the refresh interval which is used to poll values from the UPB server + * (optional, defaults to 3600000ms) + */ + private long refreshInterval = 3600000; + + /** + * Called by the SCR to activate the component with its configuration read + * from CAS + * + * @param bundleContext + * BundleContext of the Bundle that defines this component + * @param configuration + * Configuration properties for this component obtained from the + * ConfigAdmin service + */ + public void activate(final BundleContext bundleContext, final Map configuration) { + + String refreshIntervalString = (String) configuration.get("refresh"); + if (StringUtils.isNotBlank(refreshIntervalString)) { + refreshInterval = Long.parseLong(refreshIntervalString); + } + + parseConfiguration(configuration); + + logger.info("UPB binding starting up..."); + + try { + serialPort = openSerialPort(); + upbReader = new UPBReader(serialPort.getInputStream()); + upbWriter = new UPBWriter(serialPort.getOutputStream(), upbReader); + } catch (Exception e) { + throw new RuntimeException("Failed to open serial port.", e); + } + + upbReader.addListener(this); + + setProperlyConfigured(true); + } + + /** + * Called by the SCR when the configuration of a binding has been changed + * through the ConfigAdmin service. + * + * @param configuration + * Updated configuration properties + */ + public void modified(final Map configuration) { + parseConfiguration(configuration); + } + + private void parseConfiguration(final Map configuration) { + port = ObjectUtils.toString(configuration.get("port"), null); + network = Integer.valueOf(ObjectUtils.toString(configuration.get("network"), "0")).byteValue(); + + logger.debug("Parsed UPB configuration:"); + logger.debug("Serial port: {}", port); + logger.debug("UPB Network: {}", network & 0xff); + + } + + /** + * Called by the SCR to deactivate the component when either the + * configuration is removed or mandatory references are no longer satisfied + * or the component has simply been stopped. + * + * @param reason + * Reason code for the deactivation:
+ *
    + *
  • 0 – Unspecified + *
  • 1 – The component was disabled + *
  • 2 – A reference became unsatisfied + *
  • 3 – A configuration was changed + *
  • 4 – A configuration was deleted + *
  • 5 – The component was disposed + *
  • 6 – The bundle was stopped + *
+ */ + public void deactivate(final int reason) { + logger.info("UPB binding shutting down..."); + + if (upbReader != null) { + upbReader.shutdown(); + } + + if (upbWriter != null) { + upbWriter.shutdown(); + } + + if (serialPort != null) { + logger.debug("Closing serial port"); + serialPort.close(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected long getRefreshInterval() { + return refreshInterval; + } + + /** + * {@inheritDoc} + */ + @Override + protected String getName() { + return "UPB Service"; + } + + /** + * {@inheritDoc} + */ + @Override + protected void execute() { + // the frequently executed code (polling) goes here ... + for (UPBBindingProvider p : providers) { + for (String s : p.getItemNames()) { + UPBBindingConfig config = p.getConfig(s); + if (!config.isLink()) { + MessageBuilder message = MessageBuilder.create().network(network).destination(config.getId()) + .command(UPBMessage.Command.REPORT_STATE.toByte()); + // Here we write the command to the PIM. + upbWriter.queueMessage(message); + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void internalReceiveCommand(String itemName, Command command) { + UPBBindingConfig config = getConfig(itemName); + + if (config != null) { + byte[] commandByte = { UPBMessage.Command.DEACTIVATE.toByte() }; + if (command == OnOffType.ON) { + commandByte = new byte[] { UPBMessage.Command.ACTIVATE.toByte() }; + } else if (command instanceof PercentType) { + commandByte = new byte[] { UPBMessage.Command.GOTO.toByte(), ((PercentType) command).byteValue() }; + } + + MessageBuilder message = MessageBuilder.create().network(network).destination(config.getId()) + .link(config.isLink()).command(commandByte); + + // Here we write the command to the PIM. + upbWriter.queueMessage(message); + } + } + + private String getItemName(byte id, boolean link) { + for (UPBBindingProvider p : providers) { + for (String itemName : p.getItemNames()) { + UPBBindingConfig config = p.getConfig(itemName); + + if (config != null && config.getId() == id && config.isLink() == link) { + return itemName; + } + } + } + + return null; + } + + private UPBBindingConfig getConfig(String itemName) { + for (UPBBindingProvider p : providers) { + UPBBindingConfig config = p.getConfig(itemName); + + if (config != null) { + return config; + } + } + + return null; + } + + private SerialPort openSerialPort() { + SerialPort serialPort = null; + CommPortIdentifier portId; + try { + portId = CommPortIdentifier.getPortIdentifier(port); + } catch (NoSuchPortException e1) { + throw new RuntimeException("Port does not exist", e1); + } + + if (portId.getPortType() == CommPortIdentifier.PORT_SERIAL) { + if (portId.getName().equals(port)) { + try { + serialPort = portId.open("UPB", 1000); + } catch (PortInUseException e) { + throw new RuntimeException("Port is in use", e); + } + try { + serialPort.setSerialPortParams(4800, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, + SerialPort.PARITY_NONE); + serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); + serialPort.enableReceiveTimeout(100); + } catch (UnsupportedCommOperationException e) { + throw new RuntimeException("Failed to configure serial port"); + } + } + } + + return serialPort; + } + + /** + * {@inheritDoc} + */ + @Override + public void messageReceived(UPBMessage message) { + if (message.getType() != Type.MESSAGE_REPORT) { + return; + } + + String sourceName = getItemName(message.getSource(), false); + String destinationName = getItemName(message.getDestination(), message.getControlWord().isLink()); + UPBBindingConfig sourceConfig = getConfig(sourceName); + UPBBindingConfig destinationConfig = getConfig(destinationName); + + String itemName = isValidId(message.getDestination()) ? destinationName : sourceName; + UPBBindingConfig config = isValidId(message.getDestination()) ? destinationConfig : sourceConfig; + + if (itemName == null || config == null) { + logger.debug("Received message for unknown {} with id {}.", + message.getControlWord().isLink() ? "Link" : "Device", message.getDestination() & 0xff); + return; + } + + State newState = null; + byte level = 100; + + switch (message.getCommand()) { + case GOTO: + case DEVICE_STATE: + case ACTIVATE: + + if (message.getArguments() != null && message.getArguments().length > 0) { + level = message.getArguments()[0]; + } else { + level = (byte) (message.getCommand() == UPBMessage.Command.ACTIVATE ? 100 : 0); + } + + // Links will send FF (-1) for their level. + if (level == -1 || level >= 100 || (level > 0 && !config.isDimmable())) { + newState = OnOffType.ON; + } else if (level == 0) { + newState = OnOffType.OFF; + } else { + newState = new PercentType(level); + } + break; + case DEACTIVATE: + newState = OnOffType.OFF; + break; + default: + break; + } + + if (newState != null) { + logger.debug("Posting update: {},{}", itemName, newState); + eventPublisher.postUpdate(itemName, newState); + } + } + + private boolean isValidId(byte id) { + return id != 0 && id != -1; + } +} diff --git a/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBBindingConfig.java b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBBindingConfig.java new file mode 100644 index 00000000000..f023b3602fe --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBBindingConfig.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2016, openHAB.org and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.upb.internal; + +import java.util.HashMap; +import java.util.Map; + +import org.openhab.core.binding.BindingConfig; + +/** + * This is a helper class holding binding specific configuration details. + * + * @author cvanorman + * @since 1.9.0 + */ +public class UPBBindingConfig implements BindingConfig { + private Byte id; + private boolean dimmable; + private boolean link; + private Map properties = new HashMap<>(); + + /** + * Instantiate a new UPBBindingConfig + * + * @param properties + * @param dimmable + */ + public UPBBindingConfig(String[] properties, boolean dimmable) { + this.dimmable = dimmable; + for (String s : properties) { + String[] entry = s.split("="); + + if (entry.length == 2) { + setProperty(entry[0], entry[1]); + } + } + } + + /** + * @return the id + */ + public Byte getId() { + return id; + } + + /** + * @return the requested property or null if it is not specified. + */ + public String getProperty(String property) { + return properties.get(property); + } + + /** + * @return the link + */ + public boolean isLink() { + return link; + } + + /** + * @return the dimmable + */ + public boolean isDimmable() { + return dimmable; + } + + private void setProperty(String prop, String value) { + + if ("id".equals(prop)) { + this.id = Integer.valueOf(value).byteValue(); + } else if ("link".equals(prop)) { + this.link = Boolean.valueOf(value); + } else { + properties.put(prop, value); + } + } +} \ No newline at end of file diff --git a/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBGenericBindingProvider.java b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBGenericBindingProvider.java new file mode 100644 index 00000000000..9bf171c5403 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBGenericBindingProvider.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2016, openHAB.org and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.upb.internal; + +import org.openhab.binding.upb.UPBBindingProvider; +import org.openhab.core.items.Item; +import org.openhab.core.library.items.DimmerItem; +import org.openhab.core.library.items.SwitchItem; +import org.openhab.model.item.binding.AbstractGenericBindingProvider; +import org.openhab.model.item.binding.BindingConfigParseException; + +/** + * This class is responsible for parsing the binding configuration. + * + * @author cvanorman + * @since 1.9.0 + */ +public class UPBGenericBindingProvider extends AbstractGenericBindingProvider implements UPBBindingProvider { + + /** + * {@inheritDoc} + */ + @Override + public String getBindingType() { + return "UPB"; + } + + /** + * @{inheritDoc + */ + @Override + public void validateItemType(Item item, String bindingConfig) throws BindingConfigParseException { + if (!(item instanceof SwitchItem || item instanceof DimmerItem)) { + throw new BindingConfigParseException( + "item '" + item.getName() + "' is of type '" + item.getClass().getSimpleName() + + "', only Switch- and DimmerItems are allowed - please check your *.items configuration"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void processBindingConfiguration(String context, Item item, String bindingConfig) + throws BindingConfigParseException { + super.processBindingConfiguration(context, item, bindingConfig); + + // parse bindingconfig here ... + String[] properties = bindingConfig.split(" "); + UPBBindingConfig config = new UPBBindingConfig(properties, item instanceof DimmerItem); + + if (config.getId() == null) { + throw new BindingConfigParseException("item config must have an id value"); + } + addBindingConfig(item, config); + } + + @Override + public UPBBindingConfig getConfig(String itemName) { + if (itemName != null) { + return (UPBBindingConfig) bindingConfigs.get(itemName); + } else { + return null; + } + } +} diff --git a/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBMessage.java b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBMessage.java new file mode 100644 index 00000000000..74fdf0c9cb3 --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBMessage.java @@ -0,0 +1,276 @@ +/** + * Copyright (c) 2010-2016, openHAB.org and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.upb.internal; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map.Entry; + +import javax.xml.bind.DatatypeConverter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Model for a message sent or received from a UPB modem. + * + * @author cvanorman + * @since 1.9.0 + */ +public class UPBMessage { + + /** + * An enum of possible commands. + * + * @author cvanorman + * + */ + public enum Command { + ACTIVATE, + DEACTIVATE, + GOTO, + START_FADE, + STOP_FADE, + BLINK, + REPORT_STATE, + STORE_STATE, + DEVICE_STATE, + NONE; + + /** + * Gets the protocol byte code for this Command. + * + * @return + */ + public byte toByte() { + for (Entry e : commandMap.entrySet()) { + if (e.getValue() == this) { + return e.getKey().byteValue(); + } + } + + return 0; + } + + /** + * Converts a byte value into a Command. + * + * @param value + * the byte value. + * @return the Command that is represented by the given byte value. + */ + public static Command valueOf(byte value) { + return commandMap.get(value); + } + } + + /** + * An enum of possible modem response types. + * + * @author cvanorman + * + */ + public enum Type { + ACCEPT, + BUSY, + ERROR, + ACK, + NAK, + MESSAGE_REPORT, + NONE; + } + + private static final Logger logger = LoggerFactory.getLogger(UPBMessage.class); + + private static HashMap commandMap = new HashMap<>(); + + static { + commandMap.put(0x20, Command.ACTIVATE); + commandMap.put(0x21, Command.DEACTIVATE); + commandMap.put(0x22, Command.GOTO); + commandMap.put(0x23, Command.START_FADE); + commandMap.put(0x24, Command.STOP_FADE); + commandMap.put(0x25, Command.BLINK); + commandMap.put(0x30, Command.REPORT_STATE); + commandMap.put(0x31, Command.STORE_STATE); + commandMap.put(0x86, Command.DEVICE_STATE); + } + + private static HashMap typeMap = new HashMap<>(); + + static { + typeMap.put("PA", Type.ACCEPT); + typeMap.put("PB", Type.BUSY); + typeMap.put("PE", Type.ERROR); + typeMap.put("PK", Type.ACK); + typeMap.put("PN", Type.NAK); + typeMap.put("PU", Type.MESSAGE_REPORT); + } + + /** + * Converts a hex string into a {@link UPBMessage}. + * + * @param commandString + * the string as returned by the modem. + * @return a new UPBMessage. + */ + public static UPBMessage fromString(String commandString) { + UPBMessage command = new UPBMessage(); + + String typeString = commandString.substring(0, 2); + Type type = Type.NONE; + + if (typeMap.containsKey(typeString)) { + type = typeMap.get(typeString); + } + + command.setType(type); + + try { + if (commandString.length() > 2) { + byte[] data = DatatypeConverter.parseHexBinary(commandString.substring(2)); + command.getControlWord().setBytes(data[1], data[0]); + int index = 2; + command.setNetwork(data[index++]); + command.setDestination(data[index++]); + command.setSource(data[index++]); + + int commandCode = data[index++] & 0xFF; + + if (commandMap.containsKey(commandCode)) { + command.setCommand(commandMap.get(commandCode)); + } else { + command.setCommand(Command.NONE); + } + + if (index < data.length - 1) { + command.setArguments(Arrays.copyOfRange(data, index, data.length - 1)); + } + } + } catch (Exception e) { + logger.error("Attempted to parse invalid message: {}", commandString, e); + } + + return command; + } + + private Type type; + private ControlWord controlWord = new ControlWord(); + private byte network; + private byte destination; + private byte source; + + private Command command = Command.NONE; + private byte[] arguments; + + /** + * @return the type + */ + public Type getType() { + return type; + } + + /** + * @param type + * the type to set + */ + public void setType(Type type) { + this.type = 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; + } +} diff --git a/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBReader.java b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBReader.java new file mode 100644 index 00000000000..e93f25d8fcb --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBReader.java @@ -0,0 +1,190 @@ +/** + * Copyright (c) 2010-2016, openHAB.org and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.upb.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; + +import org.apache.commons.lang.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class monitors the input stream of a UPB modem. This is done + * asynchronously. When messages are received, they are broadcast to all + * subscribed {@link Listener listeners}. + * + * @author cvanorman + * @since 1.9.0 + */ +public class UPBReader implements Runnable { + + /** + * Listener class for handling received messages. A listener can be added by + * calling {@link UPBReader#addListener(Listener)}. + * + * @author cvanorman + * + */ + public interface Listener { + + /** + * Called whenever a message has been received from the UPB modem. + * + * @param message + * the message that was received. + */ + void messageReceived(UPBMessage message); + } + + private static final Logger logger = LoggerFactory.getLogger(UPBReader.class); + + private Collection listeners = new LinkedHashSet<>(); + private byte[] buffer = new byte[512]; + private int bufferLength = 0; + private InputStream inputStream; + private Thread thread; + + /** + * Instantiates a new {@link UPBReader}. + * + * @param inputStream + * the inputStream from the UPB modem. + */ + public UPBReader(InputStream inputStream) { + this.inputStream = inputStream; + + thread = new Thread(this); + thread.start(); + } + + /** + * Subscribes the listener to any future message events. + * + * @param listener + * the listener to add. + */ + public synchronized void addListener(Listener listener) { + listeners.add(listener); + } + + /** + * Removes the listener from further messages. + * + * @param listener + * the listener to remove. + */ + public synchronized void removeListener(Listener listener) { + listeners.remove(listener); + } + + /** + * Adds data to the buffer. + * + * @param data + * the data to add. + * @param length + * the length of data to add. + */ + private void addData(byte[] data, int length) { + + if (bufferLength + length > buffer.length) { + // buffer overflow discard entire buffer + bufferLength = 0; + } + + System.arraycopy(data, 0, buffer, bufferLength, length); + + bufferLength += length; + + interpretBuffer(); + } + + /** + * Shuts the reader down. + */ + public void shutdown() { + if (thread != null) { + thread.interrupt(); + } + + try { + inputStream.close(); + } catch (IOException e) { + } + } + + private int findMessageLength(byte[] buffer, int bufferLength) { + int messageLength = ArrayUtils.INDEX_NOT_FOUND; + + for (int i = 0; i < bufferLength; i++) { + if (buffer[i] == 13) { + messageLength = i; + break; + } + } + + return messageLength; + } + + /** + * Attempts to interpret any messages that may be contained in the buffer. + */ + private void interpretBuffer() { + int messageLength = findMessageLength(buffer, bufferLength); + + while (messageLength != ArrayUtils.INDEX_NOT_FOUND) { + String message = new String(Arrays.copyOfRange(buffer, 0, messageLength)); + logger.debug("UPB Message: {}", message); + + int remainingBuffer = bufferLength - messageLength - 1; + + if (remainingBuffer > 0) { + System.arraycopy(buffer, messageLength + 1, buffer, 0, remainingBuffer); + } + bufferLength = remainingBuffer; + + notifyListeners(UPBMessage.fromString(message)); + + messageLength = findMessageLength(buffer, bufferLength); + } + } + + private synchronized void notifyListeners(UPBMessage message) { + for (Listener l : new ArrayList<>(listeners)) { + l.messageReceived(message); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + byte[] buffer = new byte[256]; + try { + for (int len = -1; (len = inputStream.read(buffer)) >= 0;) { + if (len > 0) { + logger.debug("Received: {}", ArrayUtils.subarray(buffer, 0, len)); + } + addData(buffer, len); + if (Thread.interrupted()) { + break; + } + } + } catch (Exception e) { + logger.debug("Failed to read input stream.", e); + } + logger.debug("UPBReader stopped."); + } +} diff --git a/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBWriter.java b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBWriter.java new file mode 100644 index 00000000000..cd165047bac --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/java/org/openhab/binding/upb/internal/UPBWriter.java @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2010-2016, openHAB.org and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.upb.internal; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.openhab.binding.upb.internal.UPBReader.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Used to write data to the UPB modem. + * + * @author cvanorman + * @since 1.9.0 + */ +public class UPBWriter { + + /** + * Time in milliseconds to wait for an ACK from the modem after writing a + * message. + */ + private static long ACK_TIMEOUT = 500; + + private static final Logger logger = LoggerFactory.getLogger(UPBWriter.class); + + /** + * Asynchronous queue for writing data to the UPB modem. + */ + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + /** + * The UPB modem's OutputStream. + */ + private OutputStream outputStream; + + /** + * UPBReader that is monitoring the modem's InputStream. + */ + private UPBReader upbReader; + + /** + * Instantiates a new {@link UPBWriter} using the given modem + * {@link OutputStream}. + * + * @param outputStream + * the {@link OutputStream} from the UPB modem. + * @param upbReader + * the {@link UPBReader} that is monitoring the same UPB modem. + */ + public UPBWriter(OutputStream outputStream, UPBReader upbReader) { + this.outputStream = outputStream; + this.upbReader = upbReader; + } + + /** + * Queues a message to be written to the modem. + * + * @param message + * the message to be written. + */ + public void queueMessage(MessageBuilder message) { + String data = message.build(); + logger.debug("Queueing message {}.", data); + executor.execute(new Message(data.getBytes())); + } + + /** + * Cancels all queued messages and releases resources. This instance cannot + * be used again and a new {@link UPBWriter} must be instantiated after + * calling this method. + */ + public void shutdown() { + executor.shutdownNow(); + + try { + outputStream.close(); + } catch (IOException e) { + } + logger.debug("UPBWriter shutdown"); + } + + /** + * {@link Runnable} implementation used to write data to the UPB modem. + * + * @author cvanorman + * + */ + private class Message implements Runnable, Listener { + + private boolean waitingOnAck = true; + private boolean ackReceived = false; + private byte[] data; + + private Message(byte[] data) { + this.data = data; + } + + private synchronized void ackReceived(boolean ack) { + waitingOnAck = false; + ackReceived = ack; + notify(); + } + + private synchronized boolean waitForAck(int retryCount) { + long start = System.currentTimeMillis(); + while (waitingOnAck && (System.currentTimeMillis() - start) < ACK_TIMEOUT) { + try { + wait(ACK_TIMEOUT); + } catch (InterruptedException e) { + + } + + if (!waitingOnAck) { + if (ackReceived) { + logger.debug("Message {} ack received.", new String(data)); + } else { + logger.debug("Message {} not ack'd.", new String(data)); + } + } else { + logger.debug("Message {} ack timed out.", new String(data)); + } + } + + return ackReceived || retryCount == 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void messageReceived(UPBMessage message) { + switch (message.getType()) { + case BUSY: + case NAK: + ackReceived(false); + break; + case ACK: + ackReceived(true); + break; + default: + } + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + try { + upbReader.addListener(this); + int retryCount = 3; + do { + ackReceived = false; + waitingOnAck = true; + logger.debug("Writing bytes: {}", new String(data)); + outputStream.write(0x14); + outputStream.write(data); + outputStream.write(0x0d); + } while (!waitForAck(retryCount--)); + } catch (IOException e) { + } finally { + upbReader.removeListener(this); + } + } + } +} diff --git a/bundles/binding/org.openhab.binding.upb/src/main/resources/readme.txt b/bundles/binding/org.openhab.binding.upb/src/main/resources/readme.txt new file mode 100644 index 00000000000..98698c670dc --- /dev/null +++ b/bundles/binding/org.openhab.binding.upb/src/main/resources/readme.txt @@ -0,0 +1 @@ +Bundle resources go in here! \ No newline at end of file diff --git a/bundles/binding/pom.xml b/bundles/binding/pom.xml index bbc84ef4e96..1e1dd62dd7f 100644 --- a/bundles/binding/pom.xml +++ b/bundles/binding/pom.xml @@ -183,5 +183,6 @@ org.openhab.binding.smarthomatic org.openhab.binding.gc100ir org.openhab.binding.powermax + org.openhab.binding.upb